Simplify default and support custom notifications for nodes.
[pithos-macos] / pithos-macos / PithosBrowserController.m
1 //
2 //  PithosBrowserController.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosBrowserController.h"
39 #import "PithosNode.h"
40 #import "PithosAccountNode.h"
41 #import "PithosContainerNode.h"
42 #import "PithosSubdirNode.h"
43 #import "PithosObjectNode.h"
44 #import "PithosSharingAccountsNode.h"
45 #import "PithosEmptyNode.h"
46 #import "ImageAndTextCell.h"
47 #import "FileSystemBrowserCell.h"
48 #import "ASINetworkQueue.h"
49 #import "ASIPithosRequest.h"
50 #import "ASIPithos.h"
51 #import "ASIPithosContainerRequest.h"
52 #import "ASIPithosObjectRequest.h"
53 #import "ASIPithosAccount.h"
54 #import "ASIPithosContainer.h"
55 #import "ASIPithosObject.h"
56 #import "PithosUtilities.h"
57 #import "UsingSizeTransformer.h"
58
59 @interface PithosBrowserCell : FileSystemBrowserCell {}
60 @end
61
62 @implementation PithosBrowserCell
63
64 - (id)init {
65     if ((self = [super init])) {
66         [self setLineBreakMode:NSLineBreakByTruncatingMiddle];
67         [self setEditable:YES];
68     }
69     return self;
70 }
71
72 - (void)setObjectValue:(id)object {
73     if ([object isKindOfClass:[PithosNode class]]) {
74         PithosNode *node = (PithosNode *)object;
75         [self setStringValue:node.displayName];
76         [self setImage:node.icon];
77     } else {
78         [super setObjectValue:object];
79     }
80 }
81
82 @end
83
84 @interface PithosOutlineViewCell : ImageAndTextCell {}
85 @end
86
87 @implementation PithosOutlineViewCell
88
89 - (void)setObjectValue:(id)object {
90     if ([object isKindOfClass:[PithosNode class]]) {
91         PithosNode *node = (PithosNode *)object;
92         [self setStringValue:node.displayName];
93         [self setImage:node.icon];
94         [self setEditable:NO];
95     } else {
96         [super setObjectValue:object];
97     }
98 }
99
100 @end
101
102 @interface PithosBrowserController (Private)
103 - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode;
104 - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode;
105 - (BOOL)copyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode;
106 @end
107
108 @implementation PithosBrowserController
109 @synthesize pithos;
110 @synthesize accountNode;
111 @synthesize verticalSplitView, horizontalSplitView, leftTopView, leftBottomView, outlineView, browser, outlineViewMenu, browserMenu;
112 @synthesize draggedNodes, draggedParentNode;
113 @synthesize clipboardNodes, clipboardParentNode, clipboardCopy;
114 @synthesize activityTextField, activityProgressIndicator;
115
116 #pragma mark -
117 #pragma Object Lifecycle
118
119 - (id)init {
120     return [super initWithWindowNibName:@"PithosBrowserController"];
121 }
122
123 - (void)windowDidLoad {
124     [super windowDidLoad];
125     if (browser && !browserInitialized) {
126         browserInitialized = YES;
127         [self initBrowser];
128     }
129 }
130
131 - (void)initBrowser {
132     [browser registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]];
133     [browser setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES];
134     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
135     
136     [outlineView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]];
137     [outlineView setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES];
138     [outlineView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
139     
140     [browser setCellClass:[PithosBrowserCell class]];
141     [browser setAllowsBranchSelection:YES];
142     [browser setAllowsMultipleSelection:YES];
143     [browser setAllowsEmptySelection:YES];
144     [browser setAllowsTypeSelect:YES];
145     
146     moveNetworkQueue = [[ASINetworkQueue alloc] init];
147     moveNetworkQueue.shouldCancelAllRequestsOnFailure = NO;
148 //    moveNetworkQueue.maxConcurrentOperationCount = 1;
149     copyNetworkQueue = [[ASINetworkQueue alloc] init];
150     copyNetworkQueue.shouldCancelAllRequestsOnFailure = NO;
151 //    copyNetworkQueue.maxConcurrentOperationCount = 1;
152     deleteNetworkQueue = [[ASINetworkQueue alloc] init];
153     deleteNetworkQueue.shouldCancelAllRequestsOnFailure = NO;
154 //    deleteNetworkQueue.maxConcurrentOperationCount = 1;
155     uploadNetworkQueue = [[ASINetworkQueue alloc] init];
156     uploadNetworkQueue.showAccurateProgress = YES;
157     uploadNetworkQueue.shouldCancelAllRequestsOnFailure = NO;
158 //    uploadNetworkQueue.maxConcurrentOperationCount = 1;
159     downloadNetworkQueue = [[ASINetworkQueue alloc] init];
160     downloadNetworkQueue.showAccurateProgress = YES;
161     downloadNetworkQueue.shouldCancelAllRequestsOnFailure = NO;
162 //    downloadNetworkQueue.maxConcurrentOperationCount = 1;
163     
164     moveQueue = [[NSOperationQueue alloc] init];
165     [moveQueue setSuspended:YES];
166     moveQueue.name = @"gr.grnet.pithos.MoveQueue";
167 //    moveQueue.maxConcurrentOperationCount = 1;
168     copyQueue = [[NSOperationQueue alloc] init];
169     [copyQueue setSuspended:YES];
170     copyQueue.name = @"gr.grnet.pithos.CopyQueue";
171 //    copyQueue.maxConcurrentOperationCount = 1;
172     deleteQueue = [[NSOperationQueue alloc] init];
173     [deleteQueue setSuspended:YES];
174     deleteQueue.name = @"gr.grnet.pithos.DeleteQueue";
175 //    deleteQueue.maxConcurrentOperationCount = 1;
176     uploadQueue = [[NSOperationQueue alloc] init];
177     [uploadQueue setSuspended:YES];
178     uploadQueue.name = @"gr.grnet.pithos.UploadQueue";
179 //    uploadQueue.maxConcurrentOperationCount = 1;
180     downloadQueue = [[NSOperationQueue alloc] init];
181     [downloadQueue setSuspended:YES];
182     downloadQueue.name = @"gr.grnet.pithos.DownloadQueue";
183 //    downloadQueue.maxConcurrentOperationCount = 1;
184     
185     moveCallbackQueue = [[NSOperationQueue alloc] init];
186     [moveCallbackQueue setSuspended:YES];
187     moveCallbackQueue.name = @"gr.grnet.pithos.MoveCallbackQueue";
188 //    moveCallbackQueue.maxConcurrentOperationCount = 1;
189     copyCallbackQueue = [[NSOperationQueue alloc] init];
190     [copyCallbackQueue setSuspended:YES];
191     copyCallbackQueue.name = @"gr.grnet.pithos.CopyCallbackQueue";
192 //    copyCallbackQueue.maxConcurrentOperationCount = 1;
193     deleteCallbackQueue = [[NSOperationQueue alloc] init];
194     [deleteCallbackQueue setSuspended:YES];
195     deleteCallbackQueue.name = @"gr.grnet.pithos.DeleteCallbackQueue";
196 //    deleteCallbackQueue.maxConcurrentOperationCount = 1;
197     uploadCallbackQueue = [[NSOperationQueue alloc] init];
198     [uploadCallbackQueue setSuspended:YES];
199     uploadCallbackQueue.name = @"gr.grnet.pithos.UploadCallbackQueue";
200 //    uploadCallbackQueue.maxConcurrentOperationCount = 1;
201     downloadCallbackQueue = [[NSOperationQueue alloc] init];
202     [downloadCallbackQueue setSuspended:YES];
203     downloadCallbackQueue.name = @"gr.grnet.pithos.DownloadCallbackQueue";
204 //    downloadCallbackQueue.maxConcurrentOperationCount = 1;
205     
206     [activityProgressIndicator setUsesThreadedAnimation:YES];
207     [activityProgressIndicator setMinValue:0.0];
208     [activityProgressIndicator setMaxValue:1.0];
209     activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
210     
211     self.accountNode = [[[PithosAccountNode alloc] initWithPithos:pithos] autorelease];
212     containersNode = [[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil];
213     containersNodeChildren = [[NSMutableArray alloc] init];
214     sharedNode = [[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil];
215     mySharedNode = [[PithosAccountNode alloc] initWithPithos:pithos];
216     mySharedNode.displayName = @"my shared";
217     mySharedNode.shared = YES;
218     mySharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)];
219     othersSharedNode = [[PithosSharingAccountsNode alloc] initWithPithos:pithos];
220     othersSharedNode.displayName = @"others shared";
221     othersSharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)];
222     
223     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
224     
225     // Register for updates
226     // PithosAccountNode accountNode updates outlineView container nodes 
227     [[NSNotificationCenter defaultCenter] addObserver:self 
228                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
229                                                  name:@"PithosNodeChildrenUpdated" 
230                                                object:accountNode];
231     // PithosNode updates browser nodes
232     [[NSNotificationCenter defaultCenter] addObserver:self 
233                                              selector:@selector(pithosNodeChildrenUpdated:) 
234                                                  name:@"PithosNodeChildrenUpdated" 
235                                                object:nil];
236     // Request for browser refresh 
237     [[NSNotificationCenter defaultCenter] addObserver:self 
238                                              selector:@selector(pithosBrowserRefreshNeeded:) 
239                                                  name:@"PithosBrowserRefreshNeeeded" 
240                                                object:nil];
241 }
242
243 - (void)resetBrowser {
244     @synchronized(self) {
245         if (!browserActive)
246             return;
247     }
248
249     [refreshTimer invalidate];
250     [refreshTimer release];
251     
252     [moveNetworkQueue reset];
253     [copyNetworkQueue reset];
254     [deleteNetworkQueue reset];
255     [uploadNetworkQueue reset];
256     [downloadNetworkQueue reset];
257     
258     [moveQueue cancelAllOperations];
259     [moveQueue setSuspended:YES];
260     [copyQueue cancelAllOperations];
261     [copyQueue setSuspended:YES];
262     [deleteQueue cancelAllOperations];
263     [deleteQueue setSuspended:YES];
264     [uploadQueue cancelAllOperations];
265     [uploadQueue setSuspended:YES];
266     [downloadQueue cancelAllOperations];
267     [downloadQueue setSuspended:YES];
268
269     [moveCallbackQueue cancelAllOperations];
270     [moveCallbackQueue setSuspended:YES];
271     [copyCallbackQueue cancelAllOperations];
272     [copyCallbackQueue setSuspended:YES];
273     [deleteCallbackQueue cancelAllOperations];
274     [deleteCallbackQueue setSuspended:YES];
275     [uploadCallbackQueue cancelAllOperations];
276     [uploadCallbackQueue setSuspended:YES];
277     [downloadCallbackQueue cancelAllOperations];
278     [downloadCallbackQueue setSuspended:YES];
279
280     rootNode = nil;
281     [browser loadColumnZero];
282     [containersNodeChildren removeAllObjects];
283     [outlineView reloadData];
284     // Expand the folder outline view
285     [outlineView expandItem:nil expandChildren:YES];
286     [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
287     
288     activityFacility.delegate = nil;
289     [activityProgressIndicator setDoubleValue:1.0];
290     [activityProgressIndicator stopAnimation:self];
291     
292     @synchronized(self) {
293         browserActive = NO;
294     }
295 }
296
297 - (void)startBrowser {
298     @synchronized(self) {
299         if (browserActive)
300             return;
301     }
302     
303     // In the improbable case of leftover operations
304     [moveNetworkQueue reset];
305     [copyNetworkQueue reset];
306     [deleteNetworkQueue reset];
307     [uploadNetworkQueue reset];
308     [downloadNetworkQueue reset];
309     [moveQueue cancelAllOperations];
310     [copyQueue cancelAllOperations];
311     [deleteQueue cancelAllOperations];
312     [uploadQueue cancelAllOperations];
313     [downloadQueue cancelAllOperations];
314     [moveCallbackQueue cancelAllOperations];
315     [copyCallbackQueue cancelAllOperations];
316     [deleteCallbackQueue cancelAllOperations];
317     [uploadCallbackQueue cancelAllOperations];
318     [downloadCallbackQueue cancelAllOperations];
319
320     [moveNetworkQueue go];
321     [copyNetworkQueue go];
322     [deleteNetworkQueue go];
323     [uploadNetworkQueue go];
324     [downloadNetworkQueue go];
325     [moveQueue setSuspended:NO];
326     [copyQueue setSuspended:NO];
327     [deleteQueue setSuspended:NO];
328     [uploadQueue setSuspended:NO];
329     [downloadQueue setSuspended:NO];
330     [moveCallbackQueue setSuspended:NO];
331     [copyCallbackQueue setSuspended:NO];
332     [deleteCallbackQueue setSuspended:NO];
333     [uploadCallbackQueue setSuspended:NO];
334     [downloadCallbackQueue setSuspended:NO];
335
336     accountNode.pithos = pithos;
337     [accountNode forceRefresh];
338     mySharedNode.pithos = pithos;
339     [mySharedNode forceRefresh];
340     othersSharedNode.pithos = pithos;
341     [othersSharedNode forceRefresh];
342             
343 //    [activityFacility reset];
344     activityFacility.delegate = self;
345             
346     refreshTimer = [[NSTimer scheduledTimerWithTimeInterval:30 
347                                                      target:self 
348                                                    selector:@selector(forceRefresh:) 
349                                                    userInfo:self 
350                                                     repeats:YES] retain];
351     @synchronized(self) {
352         browserActive = YES;
353     }
354 }
355
356 - (BOOL)operationsPending {
357     return ([moveNetworkQueue operationCount] || 
358             [copyNetworkQueue operationCount] || 
359             [deleteNetworkQueue operationCount] || 
360             [uploadNetworkQueue operationCount] || 
361             [downloadNetworkQueue operationCount] || 
362             [moveQueue operationCount] || 
363             [copyQueue operationCount] || 
364             [deleteQueue operationCount] || 
365             [uploadQueue operationCount] || 
366             [downloadQueue operationCount] || 
367             [moveCallbackQueue operationCount] || 
368             [copyCallbackQueue operationCount] || 
369             [deleteCallbackQueue operationCount] || 
370             [uploadCallbackQueue operationCount] || 
371             [downloadCallbackQueue operationCount]);
372 }
373
374 - (void)dealloc {
375     [[NSNotificationCenter defaultCenter] removeObserver:self];
376     [self resetBrowser];
377     [moveQueue release];
378     [copyQueue release];
379     [deleteQueue release];
380     [uploadQueue release];
381     [downloadQueue release];
382     [moveNetworkQueue release];
383     [copyNetworkQueue release];
384     [deleteNetworkQueue release];
385     [uploadNetworkQueue release];
386     [downloadNetworkQueue release];
387     [clipboardParentNode release];
388     [clipboardNodes release];
389     [draggedParentNode release];
390     [draggedNodes release];
391     [sharedPreviewController release];
392     [othersSharedNode release];
393     [mySharedNode release];
394     [sharedNode release];
395     [containersNodeChildren release];
396     [containersNode release];
397     [accountNode release];
398     [rootNode release];
399     [pithos release];
400     [super dealloc];
401 }
402
403 - (void)setPithos:(ASIPithos *)aPithos {
404     if (aPithos) {
405         if (![aPithos.authUser isEqualToString:pithos.authUser] || 
406             ![aPithos.authToken isEqualToString:pithos.authToken] || 
407             ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix] ||
408             ![aPithos.publicURLPrefix isEqualToString:pithos.publicURLPrefix]) {
409             [self resetBrowser];
410             [pithos release];
411             pithos = [aPithos retain];
412             [self startBrowser];
413         } else {
414             [self startBrowser];
415         }
416     }
417 }
418
419
420 #pragma mark -
421 #pragma mark Observers
422
423 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
424     PithosNode *node = (PithosNode *)[notification object];
425     if ((node == accountNode) || ![node.pithos isEqualTo:pithos])
426         return;
427     NSLog(@"pithosNodeChildrenUpdated:%@", node.url);
428     NSInteger lastColumn = [browser lastColumn];
429     for (NSInteger column = lastColumn; column >= 0; column--) {
430         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
431             [browser reloadColumn:column];
432             return;
433         }
434     }
435 }
436
437 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
438     BOOL containerPithosFound = NO;
439     BOOL containerTrashFound = NO;
440     NSMutableIndexSet *removedContainersNodeChildren = [NSMutableIndexSet indexSet];
441     for (NSUInteger i = 0 ; i < [containersNodeChildren count] ; i++) {
442         if (![accountNode.children containsObject:[containersNodeChildren objectAtIndex:i]])
443             [removedContainersNodeChildren addIndex:i];
444     }
445     [containersNodeChildren removeObjectsAtIndexes:removedContainersNodeChildren];
446     for (PithosContainerNode *containerNode in accountNode.children) {
447         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
448             if (![containersNodeChildren containsObject:containerNode])
449                 [containersNodeChildren insertObject:containerNode atIndex:0];
450             containerPithosFound = YES;
451         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
452             NSUInteger insertIndex = 1;
453             if (!containerPithosFound)
454                 insertIndex = 0;
455             if (![containersNodeChildren containsObject:containerNode])
456                 [containersNodeChildren insertObject:containerNode atIndex:insertIndex];
457             containerTrashFound = YES;
458         } else if (![containersNodeChildren containsObject:containerNode]) {
459             [containersNodeChildren addObject:containerNode];
460         }
461     }
462     BOOL refreshAccountNode = NO;
463     if (!containerPithosFound) {
464         // Create pithos node
465         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithPithos:pithos 
466                                                                                                             containerName:@"pithos"];
467         ASINetworkQueue *networkQueue = [ASINetworkQueue queue];
468         [networkQueue go];
469         [networkQueue addOperations:[NSArray arrayWithObject:[PithosUtilities prepareRequest:containerRequest]] waitUntilFinished:YES];
470         if ([containerRequest error]) {
471             dispatch_async(dispatch_get_main_queue(), ^{
472                 [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
473             });
474         } else {
475             refreshAccountNode = YES;
476         }
477     }
478     if (!containerTrashFound) {
479         // Create trash node
480         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithPithos:pithos 
481                                                                                                             containerName:@"trash"];
482         ASINetworkQueue *networkQueue = [ASINetworkQueue queue];
483         [networkQueue go];
484         [networkQueue addOperations:[NSArray arrayWithObject:[PithosUtilities prepareRequest:containerRequest]] waitUntilFinished:YES];
485         if ([containerRequest error]) {
486             dispatch_async(dispatch_get_main_queue(), ^{
487                 [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
488             });
489         } else {
490             refreshAccountNode = YES;
491         }
492     }
493     
494     if (refreshAccountNode)
495         [accountNode refresh];
496     
497     [outlineView reloadData];
498     
499     // Expand the folder outline view
500     [outlineView expandItem:nil expandChildren:YES];
501     
502     if (((rootNode == nil) || (rootNode == containersNode) || (rootNode == sharedNode)) && [containersNodeChildren count]) {
503         rootNode = [containersNodeChildren objectAtIndex:0];
504         [browser loadColumnZero];
505     }
506     
507     if (notification)
508         [self refresh:nil];
509 }
510
511 - (void)pithosBrowserRefreshNeeded:(NSNotification *)notification {
512     [self refresh:nil];
513 }
514
515 #pragma mark -
516 #pragma mark Actions
517
518 - (IBAction)forceRefresh:(id)sender {
519     if (sender)
520         [accountNode forceRefresh];
521     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
522         PithosNode *node = (PithosNode *)[browser parentForItemsInColumn:column];
523         node.forcedRefresh = YES;
524         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];                      
525     }
526     [browser validateVisibleColumns];
527 }
528
529 - (IBAction)refresh:(id)sender {
530     if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) {
531         [self forceRefresh:sender];
532     } else {
533         if (sender)
534             [accountNode refresh];
535         for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
536             [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
537         }
538         [browser validateVisibleColumns];
539     }
540 }
541
542 #pragma mark -
543 #pragma mark NSBrowserDelegate
544
545 - (id)rootItemForBrowser:(NSBrowser *)browser {
546     return rootNode;    
547 }
548
549 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
550     PithosNode *node = (PithosNode *)item;
551     return node.children.count;
552 }
553
554 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
555     PithosNode *node = (PithosNode *)item;
556     return [node.children objectAtIndex:index];
557 }
558
559 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
560     PithosNode *node = (PithosNode *)item;
561     return node.isLeafItem;
562 }
563
564 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
565     PithosNode *node = (PithosNode *)item;
566     return node;
567 }
568
569 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
570     if (sharedPreviewController == nil)
571         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
572     return sharedPreviewController;
573 }
574
575 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
576 //    if (!forUserResize) {
577 //        id item = [browser parentForItemsInColumn:columnIndex]; 
578 //        if ([self browser:browser isLeafItem:item]) {
579 //            suggestedWidth = 200; 
580 //        }
581 //    }
582 //    return suggestedWidth;
583 //}
584
585 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
586     return NO;
587 }
588
589 #pragma mark Editing
590
591 - (BOOL)browser:(NSBrowser *)browser shouldEditItem:(id)item {
592     PithosNode *node = (PithosNode *)item;
593     if (node.shared || node.sharingAccount || 
594         ([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class]))
595         return NO;
596     return YES;
597 }
598
599 - (void)browser:(NSBrowser *)browser setObjectValue:(id)object forItem:(id)item {
600     PithosNode *node = (PithosNode *)item;
601     NSString *newName = (NSString *)object;
602     NSUInteger newNameLength = [newName length];
603     NSRange firstSlashRange = [newName rangeOfString:@"/"];
604     if ((newNameLength == 0) || 
605         ((firstSlashRange.length == 1) && (firstSlashRange.location != (newNameLength - 1))) || 
606         ([newName isEqualToString:node.displayName])) {
607         return;
608     }
609     if (([node class] == [PithosObjectNode class]) || 
610         (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
611         // Operation: Rename (move) an object or subdir/ node
612         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
613             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
614             if (operation.isCancelled) {
615                 [pool drain];
616                 return;
617             }
618             NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName];
619             if ([newName hasSuffix:@"/"])
620                 destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
621             NSError *error = nil;
622             BOOL isDirectory;
623             if ([PithosUtilities objectExistsAtPithos:pithos 
624                                         containerName:node.pithosContainer.name 
625                                            objectName:destinationObjectName 
626                                                 error:&error 
627                                           isDirectory:&isDirectory 
628                                        sharingAccount:nil]) {
629                 dispatch_async(dispatch_get_main_queue(), ^{
630                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
631                     [alert setMessageText:@"Name Taken"];
632                     [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]];
633                     [alert addButtonWithTitle:@"OK"];
634                     [alert runModal];
635                 });
636                 [pool drain];
637                 return;
638             } else if (error) {
639                 [pool drain];
640                 return;
641             }
642             if (operation.isCancelled) {
643                 [pool drain];
644                 return;
645             }
646             ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
647                                                                                    containerName:node.pithosContainer.name 
648                                                                                       objectName:node.pithosObject.name 
649                                                                         destinationContainerName:node.pithosContainer.name 
650                                                                            destinationObjectName:destinationObjectName 
651                                                                                    checkIfExists:NO];
652             if (!operation.isCancelled && objectRequest) {
653                 objectRequest.delegate = self;
654                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
655                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
656                 NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
657                                            [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
658                                            [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
659                                            [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
660                                            [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
661                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
662                                                                            message:messagePrefix];
663                 [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
664                  [NSDictionary dictionaryWithObjectsAndKeys:
665                   [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
666                   [NSNumber numberWithBool:YES], @"refresh", 
667                   activity, @"activity", 
668                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
669                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
670                   [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
671                   [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
672                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
673                   NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
674                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
675                   moveNetworkQueue, @"networkQueue", 
676                   @"move", @"operationType", 
677                   nil]];
678                 [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
679             }
680             [pool drain];
681         }];
682         [moveQueue addOperation:operation];
683     } else if ([node class] == [PithosSubdirNode class]) {
684         if (firstSlashRange.length == 1)
685             return;
686         // Operation: Rename (move) a subdir node and its descendants
687         // The resulting ASIPithosObjectRequests are chained through dependencies
688         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
689             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
690             if (operation.isCancelled) {
691                 [pool drain];
692                 return;
693             }
694             NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName];
695             NSError *error = nil;
696             BOOL isDirectory;
697             if ([PithosUtilities objectExistsAtPithos:pithos 
698                                         containerName:node.pithosContainer.name 
699                                            objectName:destinationObjectName 
700                                                 error:&error 
701                                           isDirectory:&isDirectory 
702                                        sharingAccount:nil]) {
703                 dispatch_async(dispatch_get_main_queue(), ^{
704                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
705                     [alert setMessageText:@"Name Taken"];
706                     [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]];
707                     [alert addButtonWithTitle:@"OK"];
708                     [alert runModal];
709                 });
710                 [pool drain];
711                 return;
712             } else if (error) {
713                 [pool drain];
714                 return;
715             }
716             if (operation.isCancelled) {
717                 [pool drain];
718                 return;
719             }
720             if (node.pithosObject.subdir)
721                 destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
722             NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos 
723                                                                                containerName:node.pithosContainer.name 
724                                                                                   objectName:node.pithosObject.name 
725                                                                     destinationContainerName:node.pithosContainer.name 
726                                                                        destinationObjectName:destinationObjectName 
727                                                                                checkIfExists:NO];
728             if (!operation.isCancelled && objectRequests) {
729                 ASIPithosObjectRequest *previousObjectRequest = nil;
730                 for (ASIPithosObjectRequest *objectRequest in objectRequests) {
731                     if (operation.isCancelled) {
732                         [pool drain];
733                         return;
734                     }
735                     objectRequest.delegate = self;
736                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
737                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
738                     NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
739                                                [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
740                                                [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
741                                                [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
742                                                [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
743                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
744                                                                                message:messagePrefix];
745                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
746                      [NSDictionary dictionaryWithObjectsAndKeys:
747                       [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
748                       [NSNumber numberWithBool:YES], @"refresh", 
749                       activity, @"activity", 
750                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
751                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
752                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
753                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
754                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
755                       NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
756                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
757                       moveNetworkQueue, @"networkQueue", 
758                       @"move", @"operationType", 
759                       nil]];
760                     if (previousObjectRequest)
761                         [objectRequest addDependency:previousObjectRequest];
762                     previousObjectRequest = objectRequest;
763                     [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
764                 }
765             }
766             [pool drain];
767         }];
768         [moveQueue addOperation:operation];
769     }
770 }
771
772 #pragma mark Drag and Drop source
773
774 - (BOOL)browser:(NSBrowser *)aBrowser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
775       withEvent:(NSEvent *)event {
776     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column];
777     __block BOOL result = YES;
778     [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
779         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]];
780         if (([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class])) {
781             result = NO;
782             *stop = YES;
783         }
784     }];
785     return result;
786 }
787
788 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
789    toPasteboard:(NSPasteboard *)pasteboard {
790     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
791     NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
792     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column];
793     [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
794         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]];
795         [propertyList addObject:[node.pithosObject.name pathExtension]];
796         [nodes addObject:node];
797     }];
798
799     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
800     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
801     self.draggedNodes = nodes;
802     self.draggedParentNode = [browser parentForItemsInColumn:column];
803     return YES;
804 }
805
806 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
807 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
808     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[draggedNodes count]];
809     for (PithosNode *node in draggedNodes) {        
810         // If the node is a subdir ask if the whole tree should be downloaded
811         if ([node class] == [PithosSubdirNode class]) {
812             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
813             [alert setMessageText:@"Download directory"];
814             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
815             [alert addButtonWithTitle:@"OK"];
816             [alert addButtonWithTitle:@"Cancel"];
817             NSInteger choice = [alert runModal];
818             if (choice == NSAlertFirstButtonReturn) {
819                 // Operation: Download a subdir node and its descendants
820                 // The resulting ASIPithosObjectRequests are chained through dependencies
821                 __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
822                     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
823                     if (operation.isCancelled) {
824                         [pool drain];
825                         return;
826                     }
827                     NSArray *objectRequests = [PithosUtilities objectDataRequestsForSubdirWithPithos:pithos 
828                                                                                        containerName:node.pithosContainer.name 
829                                                                                           objectName:node.pithosObject.name 
830                                                                                          toDirectory:[dropDestination path] 
831                                                                                        checkIfExists:YES 
832                                                                                       sharingAccount:node.sharingAccount];
833                     if (!operation.isCancelled && objectRequests) {
834                         ASIPithosObjectRequest *previousObjectRequest = nil;
835                         for (__block ASIPithosObjectRequest *objectRequest in objectRequests) {
836                             if (operation.isCancelled) {
837                                 [pool drain];
838                                 return;
839                             }
840                             [names addObject:[objectRequest.userInfo objectForKey:@"fileName"]];
841                             objectRequest.delegate = self;
842                             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
843                             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
844                             NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
845                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
846                                                                                        message:[messagePrefix stringByAppendingString:@" (0%)"]
847                                                                                     totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] 
848                                                                                   currentBytes:0];
849                             dispatch_async(dispatch_get_main_queue(), ^{
850                                 [activityFacility updateActivity:activity withMessage:activity.message];  
851                             });
852                             [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
853                              [NSDictionary dictionaryWithObjectsAndKeys:
854                               activity, @"activity", 
855                               [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
856                               [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
857                               [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
858                               [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
859                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
860                               NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", 
861                               NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
862                               downloadNetworkQueue, @"networkQueue", 
863                               @"download", @"operationType", 
864                               nil]];
865                             [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
866                                 [activityFacility updateActivity:activity 
867                                                      withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
868                                                       totalBytes:activity.totalBytes 
869                                                     currentBytes:(activity.currentBytes + size)];
870                             }];
871                             if (previousObjectRequest)
872                                 [objectRequest addDependency:previousObjectRequest];
873                             previousObjectRequest = objectRequest;
874                             [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
875                         }
876                     }
877                     [pool drain];
878                 }];
879                 [downloadQueue addOperation:operation];
880             }
881         } else {
882             // Operation: Download an object node
883             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
884                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
885                 if (operation.isCancelled) {
886                     [pool drain];
887                     return;
888                 }
889                 __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectDataRequestWithPithos:pithos 
890                                                                                                containerName:node.pithosContainer.name 
891                                                                                                   objectName:node.pithosObject.name 
892                                                                                                  toDirectory:[dropDestination path] 
893                                                                                                checkIfExists:YES 
894                                                                                               sharingAccount:node.sharingAccount];
895                 if (!operation.isCancelled && objectRequest) {
896                     [names addObject:[objectRequest.userInfo objectForKey:@"fileName"]];
897                     objectRequest.delegate = self;
898                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
899                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
900                     NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
901                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
902                                                                                message:[messagePrefix stringByAppendingString:@" (0%)"]
903                                                                             totalBytes:node.pithosObject.bytes 
904                                                                           currentBytes:0];
905                     dispatch_async(dispatch_get_main_queue(), ^{
906                         [activityFacility updateActivity:activity withMessage:activity.message];  
907                     });
908                     [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
909                      [NSDictionary dictionaryWithObjectsAndKeys:
910                       activity, @"activity", 
911                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
912                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
913                       [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
914                       [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
915                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
916                       NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", 
917                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
918                       downloadNetworkQueue, @"networkQueue", 
919                       @"download", @"operationType", 
920                       nil]];
921                     [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
922                         [activityFacility updateActivity:activity 
923                                              withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
924                                               totalBytes:activity.totalBytes 
925                                             currentBytes:(activity.currentBytes + size)];
926                     }];
927                     [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
928                     [pool drain];
929                 }
930             }];
931             [downloadQueue addOperation:operation];
932         }
933     }
934     return names;
935 }
936
937 #pragma mark Drag and Drop destination
938
939 - (NSDragOperation)browser:aBrowser 
940               validateDrop:(id<NSDraggingInfo>)info 
941                proposedRow:(NSInteger *)row 
942                     column:(NSInteger *)column 
943              dropOperation:(NSBrowserDropOperation *)dropOperation {
944     NSDragOperation result = NSDragOperationNone;
945     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
946         // For a drop above, the drop is redirected to the parent item
947         if (*dropOperation == NSBrowserDropAbove)
948             *row = -1;
949         // Only allow dropping in folders
950         if (*column != -1) {
951             PithosNode *dropNode;
952             if (*row != -1) {
953                 // Check if the node is not a folder and if so redirect to the parent item
954                 dropNode = [browser itemAtRow:*row inColumn:*column];
955                 if ([dropNode class] == [PithosObjectNode class])
956                     *row = -1;
957             }
958             if (*row == -1)
959                 dropNode = [browser parentForItemsInColumn:*column];
960             
961             if (!dropNode.shared && 
962                 (!dropNode.sharingAccount || 
963                  ([dropNode class] == [PithosSubdirNode class]) || 
964                  ([dropNode class] == [PithosContainerNode class]))) {
965                 *dropOperation = NSBrowserDropOn;
966                 result = NSDragOperationCopy;
967             }
968         }
969     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
970         // For a drop above, the drop is redirected to the parent item
971         if (*dropOperation == NSBrowserDropAbove) 
972             *row = -1;
973         // Only allow dropping in folders
974         if (*column != -1) {
975             PithosNode *dropNode;
976             if (*row != -1) {
977                 // Check if the node is not a folder and if so redirect to the parent item
978                 dropNode = [browser itemAtRow:*row inColumn:*column];
979                 if ([dropNode class] == [PithosObjectNode class])
980                     *row = -1;
981             }
982             if (*row == -1)
983                 dropNode = [browser parentForItemsInColumn:*column];
984             
985             if (!dropNode.shared && !dropNode.sharingAccount) {
986                 if ([info draggingSourceOperationMask] & NSDragOperationMove) {
987                     // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
988                     if ((([dropNode class] == [PithosContainerNode class]) || 
989                          dropNode.pithosObject.subdir || 
990                          ![dropNode.pithosObject.name hasSuffix:@"/"]) && 
991                         ![dropNode isEqualTo:draggedParentNode]) { 
992     //                    ![dropNode isEqualTo:draggedParentNode] && 
993     //                    ![draggedNodes containsObject:dropNode]) {                
994                         result = NSDragOperationMove;
995                     }
996                 } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
997                     // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
998                     if (([dropNode class] == [PithosContainerNode class]) || 
999                         dropNode.pithosObject.subdir || 
1000                         ![dropNode.pithosObject.name hasSuffix:@"/"]) { 
1001                         result = NSDragOperationCopy;
1002                     }
1003                 }
1004             }
1005         }
1006     }
1007     return result;
1008 }
1009
1010 - (BOOL)browser:(NSBrowser *)aBrowser 
1011      acceptDrop:(id<NSDraggingInfo>)info 
1012           atRow:(NSInteger)row 
1013          column:(NSInteger)column 
1014   dropOperation:(NSBrowserDropOperation)dropOperation {
1015     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
1016         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
1017         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
1018         if ((column != -1) && (filenames != nil)) {
1019             PithosNode *node;
1020             if (row != -1)
1021                 node = [browser itemAtRow:row inColumn:column];
1022             else
1023                 node = [browser parentForItemsInColumn:column];
1024             NSLog(@"drag in node: %@", node.url);
1025             return [self uploadFiles:filenames toNode:node];
1026         }
1027     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
1028         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
1029         if ((column != -1) && (draggedNodes != nil)) {
1030             PithosNode *node;
1031             if (row != -1)
1032                 node = [browser itemAtRow:row inColumn:column];
1033             else
1034                 node = [browser parentForItemsInColumn:column];
1035             NSLog(@"drag local node: %@", node.url);
1036             if ([info draggingSourceOperationMask] & NSDragOperationMove)
1037                 return [self moveNodes:draggedNodes toNode:node];
1038             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
1039                 return [self copyNodes:draggedNodes toNode:node];
1040         }
1041     }
1042     return NO;
1043 }
1044
1045 #pragma mark -
1046 #pragma mark Drag and Drop methods
1047
1048 - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode {
1049     if (([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class]))
1050         return NO;
1051     NSFileManager *fileManager = [NSFileManager defaultManager];
1052     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1053     NSString *objectNamePrefix;
1054     if ([destinationNode class] == [PithosSubdirNode class])
1055         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1056     else
1057         objectNamePrefix = [NSString string];
1058     if ((destinationNode.pithosContainer.blockHash == nil) || (destinationNode.pithosContainer.blockSize == 0)) {
1059         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest containerMetadataRequestWithPithos:pithos 
1060                                                                                                       containerName:containerName];
1061         ASINetworkQueue *networkQueue = [ASINetworkQueue queue];
1062         [networkQueue go];
1063         [networkQueue addOperations:[NSArray arrayWithObject:[PithosUtilities prepareRequest:containerRequest]] waitUntilFinished:YES];
1064         if ([containerRequest error]) {
1065             [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
1066             return NO;
1067         } else if (containerRequest.responseStatusCode != 204) {
1068             [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest];
1069             return NO;
1070         }
1071         destinationNode.pithosContainer.blockHash = [containerRequest blockHash];
1072         destinationNode.pithosContainer.blockSize = [containerRequest blockSize];
1073     }    
1074     NSUInteger blockSize = destinationNode.pithosContainer.blockSize;
1075     NSString *blockHash = destinationNode.pithosContainer.blockHash;
1076     
1077     for (NSString *filePath in filenames) {
1078         BOOL isDirectory;
1079         if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
1080             if (!isDirectory) {
1081                 // Upload file
1082                 NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
1083                 // Operation: Upload a local file
1084                 __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1085                     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1086                     if (operation.isCancelled) {
1087                         [pool drain];
1088                         return;
1089                     }
1090                     NSError *error = nil;
1091                     NSString *contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
1092                     if (contentType == nil)
1093                         contentType = @"application/octet-stream";
1094                     if (error)
1095                         NSLog(@"contentType detection error: %@", error);
1096                     NSArray *hashes = nil;
1097                     if (operation.isCancelled) {
1098                         [pool drain];
1099                         return;
1100                     }
1101                     ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1102                                                                                                 containerName:containerName 
1103                                                                                                    objectName:objectName 
1104                                                                                                   contentType:contentType 
1105                                                                                                     blockSize:blockSize 
1106                                                                                                     blockHash:blockHash 
1107                                                                                                       forFile:filePath 
1108                                                                                                 checkIfExists:YES 
1109                                                                                                        hashes:&hashes 
1110                                                                                                sharingAccount:destinationNode.sharingAccount];
1111                     if (!operation.isCancelled && objectRequest) {
1112                         objectRequest.delegate = self;
1113                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1114                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1115                         NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1116                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1117                                                                                    message:[messagePrefix stringByAppendingString:@" (0%)"]
1118                                                                                 totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1119                                                                               currentBytes:0];
1120                         [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1121                          [NSDictionary dictionaryWithObjectsAndKeys:
1122                           containerName, @"containerName", 
1123                           objectName, @"objectName", 
1124                           contentType, @"contentType", 
1125                           [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
1126                           blockHash, @"blockHash", 
1127                           filePath, @"filePath", 
1128                           hashes, @"hashes", 
1129                           [NSArray arrayWithObject:destinationNode], @"refreshNodes", 
1130                           [NSNumber numberWithBool:YES], @"refresh", 
1131                           [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1132                           activity, @"activity", 
1133                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1134                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1135                           [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
1136                           [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1137                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1138                           NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1139                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1140                           uploadNetworkQueue, @"networkQueue", 
1141                           @"upload", @"operationType", 
1142                           nil]];
1143                         if (destinationNode.sharingAccount)
1144                             [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
1145                         [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1146                     }
1147                     [pool drain];
1148                 }];
1149                 [uploadQueue addOperation:operation];
1150             } else {
1151                 // Upload directory, confirm first
1152                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1153                 [alert setMessageText:@"Upload directory"];
1154                 [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]];
1155                 [alert addButtonWithTitle:@"OK"];
1156                 [alert addButtonWithTitle:@"Cancel"];
1157                 NSInteger choice = [alert runModal];
1158                 if (choice == NSAlertFirstButtonReturn) {
1159                     NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
1160                     // Operation: Upload a local directory and its descendants
1161                     // The resulting ASIPithosObjectRequests are chained through dependencies
1162                     __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1163                         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1164                         if (operation.isCancelled) {
1165                             [pool drain];
1166                             return;
1167                         }
1168                         NSMutableArray *objectNames = nil;
1169                         NSMutableArray *contentTypes = nil;
1170                         NSMutableArray *filePaths = nil;
1171                         NSMutableArray *hashesArrays = nil;
1172                         NSMutableArray *directoryObjectRequests = nil;
1173                         NSArray *objectRequests = [PithosUtilities writeObjectDataRequestsWithPithos:pithos 
1174                                                                                        containerName:containerName 
1175                                                                                           objectName:objectName 
1176                                                                                            blockSize:blockSize 
1177                                                                                            blockHash:blockHash 
1178                                                                                         forDirectory:filePath 
1179                                                                                        checkIfExists:YES 
1180                                                                                          objectNames:&objectNames
1181                                                                                         contentTypes:&contentTypes
1182                                                                                            filePaths:&filePaths
1183                                                                                         hashesArrays:&hashesArrays 
1184                                                                              directoryObjectRequests:&directoryObjectRequests 
1185                                                                                       sharingAccount:destinationNode.sharingAccount];
1186                         if (operation.isCancelled) {
1187                             [pool drain];
1188                             return;
1189                         }
1190                         ASIPithosObjectRequest *previousObjectRequest = nil;
1191                         for (ASIPithosObjectRequest *objectRequest in directoryObjectRequests) {
1192                             if (operation.isCancelled) {
1193                                 [pool drain];
1194                                 return;
1195                             }
1196                             objectRequest.delegate = self;
1197                             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1198                             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1199                             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1200                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1201                                                                                        message:messagePrefix];
1202                             [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1203                              [NSDictionary dictionaryWithObjectsAndKeys:
1204                               [NSNumber numberWithBool:YES], @"refresh", 
1205                               activity, @"activity", 
1206                               [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1207                               [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1208                               [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1209                               [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1210                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
1211                               NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
1212                               NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1213                               uploadNetworkQueue, @"networkQueue", 
1214                               @"upload", @"queue", 
1215                               nil]];
1216                             if (previousObjectRequest)
1217                                 [objectRequest addDependency:previousObjectRequest];
1218                             previousObjectRequest = objectRequest;
1219                             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1220                         }
1221                         if (!operation.isCancelled && objectRequests) {
1222                             for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) {
1223                                 if (operation.isCancelled) {
1224                                     [pool drain];
1225                                     return;
1226                                 }
1227                                 ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i];
1228                                 objectRequest.delegate = self;
1229                                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1230                                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1231                                 NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1232                                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1233                                                                                            message:[messagePrefix stringByAppendingString:@" (0%)"]
1234                                                                                         totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1235                                                                                       currentBytes:0];
1236                                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1237                                  [NSDictionary dictionaryWithObjectsAndKeys:
1238                                   containerName, @"containerName", 
1239                                   [objectNames objectAtIndex:i], @"objectName", 
1240                                   [contentTypes objectAtIndex:i], @"contentType", 
1241                                   [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
1242                                   blockHash, @"blockHash", 
1243                                   [filePaths objectAtIndex:i], @"filePath", 
1244                                   [hashesArrays objectAtIndex:i], @"hashes", 
1245                                   [NSNumber numberWithBool:YES], @"refresh", 
1246                                   [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1247                                   activity, @"activity", 
1248                                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1249                                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1250                                   [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
1251                                   [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1252                                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
1253                                   NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1254                                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1255                                   uploadNetworkQueue, @"networkQueue", 
1256                                   @"upload", @"queue", 
1257                                   nil]];
1258                                 if (destinationNode.sharingAccount)
1259                                     [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
1260                                 if (previousObjectRequest)
1261                                     [objectRequest addDependency:previousObjectRequest];
1262                                 previousObjectRequest = objectRequest;
1263                                 [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1264                             }
1265                         }
1266                         [pool drain];
1267                     }];
1268                     [uploadQueue addOperation:operation];
1269                 }
1270             }
1271         }
1272     }
1273     return YES;
1274 }
1275
1276 - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {
1277     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
1278         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
1279         return NO;
1280     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1281     NSString *objectNamePrefix;
1282     if ([destinationNode class] == [PithosSubdirNode class])
1283         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1284     else
1285         objectNamePrefix = [NSString string];
1286
1287     for (PithosNode *node in nodes) {
1288         if (([node class] == [PithosObjectNode class]) || 
1289             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1290             // Operation: Move an object or subdir/ node
1291             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1292                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1293                 if (operation.isCancelled) {
1294                     [pool drain];
1295                     return;
1296                 }
1297                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1298                 if ([node.pithosObject.name hasSuffix:@"/"])
1299                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1300                 ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
1301                                                                                        containerName:node.pithosContainer.name 
1302                                                                                           objectName:node.pithosObject.name 
1303                                                                             destinationContainerName:containerName 
1304                                                                                destinationObjectName:destinationObjectName 
1305                                                                                        checkIfExists:YES];
1306                 if (!operation.isCancelled && objectRequest) {
1307                     objectRequest.delegate = self;
1308                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1309                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1310                     NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1311                                                [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1312                                                [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1313                                                [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1314                                                [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1315                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1316                                                                                message:messagePrefix];
1317                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1318                      [NSDictionary dictionaryWithObjectsAndKeys:
1319                       [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
1320                       activity, @"activity", 
1321                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1322                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1323                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1324                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1325                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1326                       NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
1327                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1328                       moveNetworkQueue, @"networkQueue", 
1329                       @"move", @"operationType", 
1330                       nil]];
1331                     [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1332                 }
1333                 [pool drain];
1334             }];
1335             [moveQueue addOperation:operation];
1336         } else if ([node class] == [PithosSubdirNode class]) {
1337             // Operation: Move a subdir node and its descendants
1338             // The resulting ASIPithosObjectRequests are chained through dependencies
1339             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1340                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1341                 if (operation.isCancelled) {
1342                     [pool drain];
1343                     return;
1344                 }
1345                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1346                 if (node.pithosObject.subdir)
1347                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1348                 NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos 
1349                                                                                    containerName:node.pithosContainer.name 
1350                                                                                       objectName:node.pithosObject.name 
1351                                                                         destinationContainerName:containerName 
1352                                                                            destinationObjectName:destinationObjectName 
1353                                                                                    checkIfExists:YES];
1354                 if (!operation.isCancelled && objectRequests) {
1355                     ASIPithosObjectRequest *previousObjectRequest = nil;
1356                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1357                         if (operation.isCancelled) {
1358                             [pool drain];
1359                             return;
1360                         }
1361                         objectRequest.delegate = self;
1362                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1363                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1364                         NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1365                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1366                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1367                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1368                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1369                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1370                                                                                    message:messagePrefix];
1371                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1372                          [NSDictionary dictionaryWithObjectsAndKeys:
1373                           [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
1374                           [NSNumber numberWithBool:YES], @"refresh", 
1375                           activity, @"activity", 
1376                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1377                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1378                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1379                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1380                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1381                           NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
1382                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1383                           moveNetworkQueue, @"networkQueue", 
1384                           @"move", @"operationType", 
1385                           nil]];
1386                         if (previousObjectRequest)
1387                             [objectRequest addDependency:previousObjectRequest];
1388                         previousObjectRequest = objectRequest;
1389                         [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1390                     }
1391                 }
1392                 [pool drain];
1393             }];
1394             [moveQueue addOperation:operation];
1395         }
1396     }
1397     return YES;
1398 }
1399
1400 - (BOOL)copyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {    
1401     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
1402         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
1403         return NO;
1404     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1405     NSString *objectNamePrefix;
1406     if ([destinationNode class] == [PithosSubdirNode class])
1407         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1408     else
1409         objectNamePrefix = [NSString string];
1410     
1411     for (PithosNode *node in nodes) {
1412         if (([node class] == [PithosObjectNode class]) || 
1413             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1414             // Operation: Copy an object or subdir/ node
1415             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1416                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1417                 if (operation.isCancelled) {
1418                     [pool drain];
1419                     return;
1420                 }
1421                 NSString *destinationObjectName;
1422                 if (![destinationNode isEqualTo:node.parent]) {
1423                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1424                     if ([node.pithosObject.name hasSuffix:@"/"])
1425                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1426                 } else {
1427                     destinationObjectName = [PithosUtilities safeObjectNameForPithos:pithos 
1428                                                                        containerName:containerName 
1429                                                                           objectName:node.pithosObject.name];
1430                 }
1431                 if (operation.isCancelled) {
1432                     [pool drain];
1433                     return;
1434                 }
1435                 ASIPithosObjectRequest *objectRequest = [PithosUtilities copyObjectRequestWithPithos:pithos 
1436                                                                                        containerName:node.pithosContainer.name 
1437                                                                                           objectName:node.pithosObject.name 
1438                                                                             destinationContainerName:containerName 
1439                                                                                destinationObjectName:destinationObjectName 
1440                                                                                        checkIfExists:YES 
1441                                                                                       sharingAccount:node.sharingAccount];
1442                 if (!operation.isCancelled && objectRequest) {
1443                     objectRequest.delegate = self;
1444                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1445                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1446                     NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1447                                                [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1448                                                [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1449                                                [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1450                                                [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1451                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1452                                                                                message:messagePrefix];
1453                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1454                      [NSDictionary dictionaryWithObjectsAndKeys:
1455                       [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1456                       activity, @"activity", 
1457                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1458                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1459                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1460                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1461                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1462                       NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", 
1463                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1464                       copyNetworkQueue, @"networkQueue", 
1465                       @"copy", @"operationType", 
1466                       nil]];
1467                     [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1468                 }
1469                 [pool drain];
1470             }];
1471             [copyQueue addOperation:operation];
1472         } else if ([node class] == [PithosSubdirNode class]) {
1473             // Operation: Copy a subdir node and its descendants
1474             // The resulting ASIPithosObjectRequests are chained through dependencies
1475             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1476                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1477                 if (operation.isCancelled) {
1478                     [pool drain];
1479                     return;
1480                 }
1481                 NSString *destinationObjectName;
1482                 if (![destinationNode isEqualTo:node.parent]) {
1483                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1484                     if (node.pithosObject.subdir)
1485                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1486                 } else {
1487                     destinationObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
1488                                                                        containerName:containerName 
1489                                                                           subdirName:node.pithosObject.name];
1490                 }
1491                 if (operation.isCancelled) {
1492                     [pool drain];
1493                     return;
1494                 }
1495                 NSArray *objectRequests = [PithosUtilities copyObjectRequestsForSubdirWithPithos:pithos 
1496                                                                                    containerName:node.pithosContainer.name 
1497                                                                                       objectName:node.pithosObject.name 
1498                                                                         destinationContainerName:containerName 
1499                                                                            destinationObjectName:destinationObjectName 
1500                                                                                    checkIfExists:YES 
1501                                                                                   sharingAccount:node.sharingAccount];
1502                 if (!operation.isCancelled && objectRequests) {
1503                     ASIPithosObjectRequest *previousObjectRequest = nil;
1504                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1505                         if (operation.isCancelled) {
1506                             [pool drain];
1507                             return;
1508                         }
1509                         objectRequest.delegate = self;
1510                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1511                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1512                         NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1513                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1514                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1515                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1516                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1517                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1518                                                                                    message:messagePrefix];
1519                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1520                          [NSDictionary dictionaryWithObjectsAndKeys:
1521                           [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1522                           activity, @"activity", 
1523                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1524                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1525                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1526                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1527                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1528                           NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", 
1529                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1530                           copyNetworkQueue, @"networkQueue", 
1531                           @"copy", @"operationType", 
1532                           nil]];
1533                         if (previousObjectRequest)
1534                             [objectRequest addDependency:previousObjectRequest];
1535                         previousObjectRequest = objectRequest;
1536                         [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1537                     }
1538                 }
1539                 [pool drain];
1540             }];
1541             [copyQueue addOperation:operation];
1542         }
1543     }
1544     return YES;
1545 }
1546
1547 #pragma mark -
1548 #pragma mark ASIHTTPRequestDelegate
1549
1550 - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
1551     NSOperationQueue *callbackQueue;
1552     NSString *operationType = [request.userInfo objectForKey:@"operationType"];
1553     if ([operationType isEqualToString:@"move"])
1554         callbackQueue = moveCallbackQueue;
1555     else if ([operationType isEqualToString:@"copy"])
1556         callbackQueue = copyCallbackQueue;
1557     else if ([operationType isEqualToString:@"delete"])
1558         callbackQueue = deleteCallbackQueue;
1559     else if ([operationType isEqualToString:@"upload"])
1560         callbackQueue = uploadCallbackQueue;
1561     else if ([operationType isEqualToString:@"download"])
1562         callbackQueue = downloadCallbackQueue;
1563     else {
1564         dispatch_async(dispatch_get_main_queue(), ^{
1565             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1566                               withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1567         });
1568         return;
1569     }
1570     // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1571     NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
1572                                                                              selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) 
1573                                                                                object:request] autorelease];
1574     operation.completionBlock = ^{
1575         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1576         if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1577             dispatch_async(dispatch_get_main_queue(), ^{
1578                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1579                                   withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1580             });
1581         }
1582         [pool drain];
1583     };
1584     [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1585     [callbackQueue addOperation:operation];
1586 }
1587
1588 - (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request {
1589     if (request.isCancelled) {
1590         // Request has been cancelled 
1591         // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway
1592         [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1593                    withObject:request];
1594     } else {
1595         NSOperationQueue *callbackQueue;
1596         NSString *operationType = [request.userInfo objectForKey:@"operationType"];
1597         if ([operationType isEqualToString:@"move"])
1598             callbackQueue = moveCallbackQueue;
1599         else if ([operationType isEqualToString:@"copy"])
1600             callbackQueue = copyCallbackQueue;
1601         else if ([operationType isEqualToString:@"delete"])
1602             callbackQueue = deleteCallbackQueue;
1603         else if ([operationType isEqualToString:@"upload"])
1604             callbackQueue = uploadCallbackQueue;
1605         else if ([operationType isEqualToString:@"download"])
1606             callbackQueue = downloadCallbackQueue;
1607         else {
1608             dispatch_async(dispatch_get_main_queue(), ^{
1609                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1610                                   withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1611             });
1612             return;
1613         }
1614         // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1615         NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
1616                                                                                  selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1617                                                                                    object:request] autorelease];
1618         operation.completionBlock = ^{
1619             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1620             if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1621                 dispatch_async(dispatch_get_main_queue(), ^{
1622                     [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1623                                       withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1624                 });
1625             }
1626             [pool drain];
1627         };
1628         [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1629         [callbackQueue addOperation:operation];
1630     }
1631 }
1632
1633 - (void)requestFailed:(ASIPithosRequest *)request {
1634     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1635     NSOperation *operation = [request.userInfo objectForKey:@"operation"];
1636     NSLog(@"Request failed: %@", request.url);
1637     if (operation.isCancelled) {
1638         [pool drain];
1639         return;        
1640     }
1641     if (request.isCancelled) {
1642         dispatch_async(dispatch_get_main_queue(), ^{
1643             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1644                               withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1645         });
1646         [pool drain];
1647         return;
1648     }
1649     NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1650     if (retries > 0) {
1651         ASIPithosRequest *newRequest = (ASIPithosRequest *)[[PithosUtilities copyRequest:request] autorelease];
1652         [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1653         [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithBool:NO] forKey:@"unexpectedResponseStatus"];
1654         [[newRequest.userInfo objectForKey:@"networkQueue"] addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
1655     } else {
1656         dispatch_async(dispatch_get_main_queue(), ^{
1657             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1658                               withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
1659             if ([[request.userInfo objectForKey:@"unexpectedResponseStatus"] boolValue])
1660                 [PithosUtilities unexpectedResponseStatusAlertWithRequest:request];
1661             else
1662                 [PithosUtilities httpRequestErrorAlertWithRequest:request];
1663         });
1664     }
1665     [pool drain];
1666 }
1667
1668 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1669     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1670     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1671     NSLog(@"Download finished: %@", objectRequest.url);
1672     if (operation.isCancelled) {
1673         [self requestFailed:objectRequest];
1674     } else if (objectRequest.responseStatusCode == 200) {
1675         NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1676         PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1677         NSUInteger totalBytes = activity.totalBytes;
1678         
1679         // XXX change contentLength to objectContentLength if it is fixed in the server
1680         if (([objectRequest contentLength] == 0) && ![PithosUtilities isContentTypeDirectory:[objectRequest contentType]]) {
1681             NSLog(@"Downloaded  0 bytes");
1682             NSFileManager *fileManager = [NSFileManager defaultManager];
1683             if (![fileManager fileExistsAtPath:filePath]) {
1684                 if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
1685                     dispatch_async(dispatch_get_main_queue(), ^{
1686                         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1687                         [alert setMessageText:@"Create File Error"];
1688                         [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
1689                         [alert addButtonWithTitle:@"OK"];
1690                         [alert runModal];
1691                     });
1692                 }
1693             }
1694         }
1695         
1696         NSUInteger currentBytes = [objectRequest objectContentLength];
1697         if (currentBytes == 0)
1698             currentBytes = totalBytes;
1699         dispatch_async(dispatch_get_main_queue(), ^{
1700             [activityFacility endActivity:activity 
1701                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
1702                                totalBytes:totalBytes 
1703                              currentBytes:currentBytes];
1704         });
1705     } else {
1706         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1707         [self requestFailed:objectRequest];
1708     }
1709     [pool drain];
1710 }
1711
1712 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1713     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1714     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1715     NSLog(@"Upload directory object finished: %@", objectRequest.url);
1716     if (operation.isCancelled) {
1717         [self requestFailed:objectRequest];
1718     } else if (objectRequest.responseStatusCode == 201) {
1719         NSLog(@"Directory object created: %@", objectRequest.url);
1720         dispatch_async(dispatch_get_main_queue(), ^{
1721             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1722                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1723         });
1724         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1725             [node forceRefresh];
1726         }
1727         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1728             [node refresh];
1729         }
1730         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1731             [self forceRefresh:self];
1732         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1733             [self refresh:self];
1734     } else {
1735         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1736         [self requestFailed:objectRequest];
1737     }
1738     [pool drain];
1739 }
1740
1741 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1742     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1743     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1744     NSLog(@"Upload using hashmap finished: %@", objectRequest.url);
1745     NSString *fileName = [objectRequest.userInfo objectForKey:@"fileName"];
1746     PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1747     NSUInteger totalBytes = activity.totalBytes;
1748     NSUInteger currentBytes = activity.currentBytes;
1749     if (operation.isCancelled) {
1750         [self requestFailed:objectRequest];
1751     } else if (objectRequest.responseStatusCode == 201) {
1752         NSLog(@"Object created: %@", objectRequest.url);
1753         dispatch_async(dispatch_get_main_queue(), ^{
1754             [activityFacility endActivity:activity 
1755                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
1756                                totalBytes:totalBytes 
1757                              currentBytes:totalBytes];
1758         });
1759         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1760             [node forceRefresh];
1761         }
1762         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1763             [node refresh];
1764         }
1765         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1766             [self forceRefresh:self];
1767         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1768             [self refresh:self];        
1769     } else if (objectRequest.responseStatusCode == 409) {
1770         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1771         if (iteration == 0) {
1772             NSLog(@"Upload iteration limit reached: %@", objectRequest.url);
1773             dispatch_async(dispatch_get_main_queue(), ^{
1774                 [activityFacility endActivity:activity 
1775                                   withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; 
1776                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1777                 [alert setMessageText:@"Upload Timeout"];
1778                 [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
1779                                            [objectRequest.userInfo objectForKey:@"objectName"]]];
1780                 [alert addButtonWithTitle:@"OK"];
1781                 [alert runModal];
1782             });
1783             [pool drain];
1784             return;
1785         }
1786         NSLog(@"object is missing hashes: %@", objectRequest.url);
1787         NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1788                                                           withMissingHashes:[objectRequest hashes]];
1789         NSUInteger blockSize = [[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1790         if (totalBytes >= [missingBlocks count]*blockSize)
1791             currentBytes = totalBytes - [missingBlocks count]*blockSize;
1792         dispatch_async(dispatch_get_main_queue(), ^{
1793             [activityFacility updateActivity:activity 
1794                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (totalBytes ? (100*(currentBytes + 0.0)/(totalBytes + 0.0)) : 100)] 
1795                                   totalBytes:totalBytes 
1796                                 currentBytes:currentBytes];
1797         });
1798         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1799         __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
1800                                                                                                          containerName:[objectRequest.userInfo objectForKey:@"containerName"] 
1801                                                                                                              blockSize:blockSize 
1802                                                                                                                forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
1803                                                                                                      missingBlockIndex:missingBlockIndex 
1804                                                                                                         sharingAccount:[objectRequest.userInfo objectForKey:@"sharingAccount"]];
1805         newContainerRequest.delegate = self;
1806         newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1807         newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1808         newContainerRequest.userInfo = objectRequest.userInfo;
1809         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1810         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1811         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1812         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1813         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"];
1814         [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1815             [activityFacility updateActivity:activity 
1816                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
1817                                   totalBytes:activity.totalBytes 
1818                                 currentBytes:(activity.currentBytes + size)];
1819         }];
1820         [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1821     } else {
1822         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1823         [self requestFailed:objectRequest];
1824     }
1825     [pool drain];
1826 }
1827
1828 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1829     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1830     NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1831     NSLog(@"Upload of missing block finished: %@", containerRequest.url);
1832     NSUInteger blockSize = [[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1833     NSString *fileName = [containerRequest.userInfo objectForKey:@"fileName"];
1834     PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1835     if (operation.isCancelled) {
1836         [self requestFailed:containerRequest];
1837     } else if (containerRequest.responseStatusCode == 202) {
1838         NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1839         NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1840         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1841         if (missingBlockIndex == NSNotFound) {
1842             NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1843             ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1844                                                                                            containerName:[containerRequest.userInfo objectForKey:@"containerName"] 
1845                                                                                               objectName:[containerRequest.userInfo objectForKey:@"objectName"] 
1846                                                                                              contentType:[containerRequest.userInfo objectForKey:@"contentType"] 
1847                                                                                                blockSize:blockSize 
1848                                                                                                blockHash:[containerRequest.userInfo objectForKey:@"blockHash"]
1849                                                                                                  forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1850                                                                                            checkIfExists:NO 
1851                                                                                                   hashes:&hashes 
1852                                                                                           sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1853             newObjectRequest.delegate = self;
1854             newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1855             newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1856             newObjectRequest.userInfo = containerRequest.userInfo;
1857             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1858             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1859             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1860             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
1861             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1862         } else {
1863             __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
1864                                                                                                              containerName:[containerRequest.userInfo objectForKey:@"containerName"]
1865                                                                                                                  blockSize:[[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
1866                                                                                                                    forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1867                                                                                                          missingBlockIndex:missingBlockIndex 
1868                                                                                                             sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1869             newContainerRequest.delegate = self;
1870             newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1871             newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1872             newContainerRequest.userInfo = containerRequest.userInfo;
1873             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1874             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1875             [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1876                 [activityFacility updateActivity:activity 
1877                                      withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
1878                                       totalBytes:activity.totalBytes 
1879                                     currentBytes:(activity.currentBytes + size)];
1880             }];
1881             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1882         }
1883     } else {
1884         [(NSMutableDictionary *)(containerRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1885         [self requestFailed:containerRequest];
1886     }
1887     [pool drain];
1888 }
1889
1890 - (void)moveFinished:(ASIPithosObjectRequest *)objectRequest {
1891     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1892     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1893     NSLog(@"Move object finished: %@", objectRequest.url);
1894     if (operation.isCancelled) {
1895         [self requestFailed:objectRequest];
1896     } else if (objectRequest.responseStatusCode == 201) {
1897         dispatch_async(dispatch_get_main_queue(), ^{
1898             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1899                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1900         });
1901         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1902             [node forceRefresh];
1903         }
1904         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1905             [node refresh];
1906         }
1907         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1908             [self forceRefresh:self];
1909         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1910             [self refresh:self];
1911     } else {
1912         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1913         [self requestFailed:objectRequest];
1914     }
1915     [pool drain];
1916 }
1917
1918 - (void)copyFinished:(ASIPithosObjectRequest *)objectRequest {
1919     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1920     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1921     NSLog(@"Copy object finished: %@", objectRequest.url);
1922     if (operation.isCancelled) {
1923         [self requestFailed:objectRequest];
1924     } else if (objectRequest.responseStatusCode == 201) {
1925         dispatch_async(dispatch_get_main_queue(), ^{
1926             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1927                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1928         });
1929         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1930             [node forceRefresh];
1931         }
1932         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1933             [node refresh];
1934         }
1935         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1936             [self forceRefresh:self];
1937         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1938             [self refresh:self];
1939     } else {
1940         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1941         [self requestFailed:objectRequest];
1942     }
1943     [pool drain];
1944 }
1945
1946 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1947     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1948     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1949     NSLog(@"Delete object finished: %@", objectRequest.url);
1950     if (operation.isCancelled) {
1951         [self requestFailed:objectRequest];
1952     } else if (objectRequest.responseStatusCode == 204) {
1953         dispatch_async(dispatch_get_main_queue(), ^{
1954             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1955                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1956         });
1957         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1958             [node forceRefresh];
1959         }
1960         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1961             [node refresh];
1962         }
1963         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1964             [self forceRefresh:self];
1965         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1966             [self refresh:self];
1967     } else {
1968         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1969         [self requestFailed:objectRequest];
1970     }
1971     [pool drain];
1972 }
1973
1974 #pragma mark -
1975 #pragma mark NSSplitViewDelegate
1976
1977 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
1978     if (splitView == verticalSplitView)
1979         return 120;
1980     else
1981         return ([horizontalSplitView bounds].size.height - 108);
1982 }
1983
1984 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
1985     if (splitView == verticalSplitView)
1986         return 220;
1987     else
1988         return ([horizontalSplitView bounds].size.height - 108);
1989 }
1990
1991 - (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex {
1992     if (splitView == verticalSplitView) {
1993         if (proposedPosition < 120)
1994             return 120;
1995         else if (proposedPosition > 220)
1996             return 220;
1997         else
1998             return proposedPosition;
1999     } else {
2000         return ([horizontalSplitView bounds].size.height - 108);
2001     }
2002 }
2003
2004 #pragma mark -
2005 #pragma mark NSOutlineViewDataSource
2006
2007 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
2008     if (item == nil)
2009         return 2;
2010     if (item == containersNode)
2011         return containersNodeChildren.count;
2012     if (item == sharedNode)
2013         return 2;
2014     return 0;
2015 }
2016
2017 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
2018     if (item == nil)
2019         return (!index ? containersNode : sharedNode);
2020     if (item == sharedNode)
2021         return (!index ? mySharedNode : othersSharedNode);
2022     return [containersNodeChildren objectAtIndex:index];
2023 }
2024
2025 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
2026     if ((item == containersNode) || (item == sharedNode))
2027         return YES;
2028     return NO;
2029 }
2030
2031 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
2032     PithosNode *node = (PithosNode *)item;
2033     return node;    
2034 }
2035
2036 #pragma mark Drag and Drop destination
2037
2038 - (NSDragOperation)outlineView:(NSOutlineView *)anOutlineView 
2039                   validateDrop:(id<NSDraggingInfo>)info 
2040                   proposedItem:(id)item 
2041             proposedChildIndex:(NSInteger)index {
2042     NSDragOperation result = NSDragOperationNone;
2043     if ((item == nil) || (index != NSOutlineViewDropOnItemIndex))
2044         return result;
2045     PithosNode *dropNode = (PithosNode *)item;
2046     if ([dropNode class] != [PithosContainerNode class])
2047         return result;
2048     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
2049         result = NSDragOperationCopy;
2050     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
2051         if ([info draggingSourceOperationMask] & NSDragOperationMove) {
2052             // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
2053             if (![dropNode isEqualTo:draggedParentNode])
2054                 result = NSDragOperationMove;
2055         } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
2056             // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
2057             result = NSDragOperationCopy;
2058         }
2059     }
2060    return result;
2061 }
2062
2063 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id<NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
2064     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
2065         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
2066         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
2067         if (item && (index == NSOutlineViewDropOnItemIndex) && (filenames != nil)) {
2068             PithosNode *node = (PithosNode *)item;
2069             NSLog(@"drag in node: %@", node.url);
2070             return [self uploadFiles:filenames toNode:node];
2071         }
2072     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
2073         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
2074         if (item && (index == NSOutlineViewDropOnItemIndex) && (draggedNodes != nil)) {
2075             PithosNode *node = (PithosNode *)item;
2076             NSLog(@"drag local node: %@", node.url);
2077             if ([info draggingSourceOperationMask] & NSDragOperationMove)
2078                 return [self moveNodes:draggedNodes toNode:node];
2079             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
2080                 return [self copyNodes:draggedNodes toNode:node];
2081         }
2082     }
2083     return NO;
2084 }
2085
2086 #pragma mark -
2087 #pragma mark NSOutlineViewDelegate
2088
2089 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
2090     if ((item == containersNode) || (item == sharedNode))
2091         return NO;
2092     return YES;
2093 }
2094
2095 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
2096     if ((item == containersNode) || (item == sharedNode))
2097         return YES;
2098     return NO;
2099 }
2100
2101 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
2102     PithosNode *node = (PithosNode *)[outlineView itemAtRow:[outlineView selectedRow]];
2103     if (node) {
2104         rootNode = node;
2105         [browser loadColumnZero];
2106         [self refresh:nil];
2107     }
2108 }
2109
2110 #pragma mark -
2111 #pragma mark NSMenuDelegate
2112
2113 - (void)menuNeedsUpdate:(NSMenu *)menu {
2114     [menu removeAllItems];
2115     NSMenuItem *menuItem;
2116     NSString *menuItemTitle;
2117     BOOL nodeContextMenu = NO;
2118     PithosNode *menuNode = nil;
2119     NSMutableArray *menuNodes;
2120     if (menu == browserMenu) {
2121         NSInteger column = [browser clickedColumn];
2122         NSInteger row = [browser clickedRow];
2123         if ((column == -1) || (row == -1)) {
2124             // General context menu
2125             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
2126             if ([menuNodesIndexPaths count] == 0) {
2127                 menuNode = [browser parentForItemsInColumn:0];
2128             } else if (([menuNodesIndexPaths count] != 1) || 
2129                        ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) {
2130                 menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)];
2131             } else {
2132                 menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]];
2133             }
2134         } else {
2135             // Node context menu
2136             NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row];
2137             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
2138             menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]];
2139             if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) {
2140                 for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) {
2141                     [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]];
2142                 }
2143             } else {
2144                 [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]];
2145             }
2146             nodeContextMenu = YES;
2147         }
2148     } else if (menu == outlineViewMenu) {
2149         NSInteger row = [outlineView clickedRow];
2150         if (row == -1)
2151             row = [outlineView selectedRow];
2152         if (row == -1)
2153             return;
2154         menuNode = [outlineView itemAtRow:row];
2155     }
2156
2157     if (!nodeContextMenu) {
2158         // General context menu
2159         if (([menuNode class] == [PithosAccountNode class]) || 
2160             ([menuNode class] == [PithosSharingAccountsNode class]) ||
2161             ([menuNode class] == [PithosEmptyNode class]))
2162             return;
2163         BOOL shared = menuNode.shared;
2164         BOOL sharingAccount = (menuNode.sharingAccount != nil);
2165         // New Folder
2166         if (!shared && !sharingAccount) {
2167             menuItem = [[[NSMenuItem alloc] initWithTitle:@"New Folder" action:@selector(menuNewFolder:) keyEquivalent:@""] autorelease];
2168             [menuItem setRepresentedObject:menuNode];
2169             [menu addItem:menuItem];
2170             [menu addItem:[NSMenuItem separatorItem]];
2171         }
2172         // Refresh
2173         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""] autorelease];
2174         [menu addItem:menuItem];
2175         [menu addItem:[NSMenuItem separatorItem]];
2176         // Get Info
2177         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
2178         [menuItem setRepresentedObject:[NSArray arrayWithObject:menuNode]];
2179         [menu addItem:menuItem];
2180         // Paste
2181         if (!shared && !sharingAccount) {
2182             if (clipboardNodes) {
2183                 NSUInteger clipboardNodesCount = [clipboardNodes count];
2184                 if (clipboardNodesCount == 0) {
2185                     self.clipboardNodes = nil;
2186                 } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
2187                     if (clipboardNodesCount == 1)
2188                         menuItemTitle = [NSString stringWithString:@"Paste Item"];
2189                     else
2190                         menuItemTitle = [NSString stringWithString:@"Paste Items"];
2191                     [menu addItem:[NSMenuItem separatorItem]];
2192                     menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
2193                     [menuItem setRepresentedObject:menuNode];
2194                     [menu addItem:menuItem];
2195                 }
2196             }
2197         }
2198     } else {
2199         // Node context menu
2200         NSUInteger menuNodesCount = [menuNodes count];
2201         PithosNode *firstMenuNode = (PithosNode *)[menuNodes objectAtIndex:0];
2202         BOOL shared = firstMenuNode.shared;
2203         BOOL sharingAccount = (firstMenuNode.sharingAccount != nil);
2204         // Move to Trash (pithos container only)
2205         // Delete
2206         if (!shared && !sharingAccount) {
2207             if ([rootNode class] == [PithosContainerNode class]) {
2208                 if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
2209                     menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(menuMoveToTrash:) keyEquivalent:@""] autorelease];
2210                     [menuItem setRepresentedObject:menuNodes];
2211                     [menu addItem:menuItem];
2212                 }
2213                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(menuDelete:) keyEquivalent:@""] autorelease];
2214                 [menuItem setRepresentedObject:menuNodes];
2215                 [menu addItem:menuItem];
2216                 [menu addItem:[NSMenuItem separatorItem]];
2217             }
2218         }
2219         // Refresh
2220         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""] autorelease];
2221         [menu addItem:menuItem];
2222         [menu addItem:[NSMenuItem separatorItem]];        
2223         // Get Info
2224         if (!sharingAccount || ([firstMenuNode class] != [PithosAccountNode class])) {
2225             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
2226             [menuItem setRepresentedObject:menuNodes];
2227             [menu addItem:menuItem];
2228             
2229             if ((!shared && !sharingAccount) || ([firstMenuNode class] != [PithosContainerNode class]))
2230                 [menu addItem:[NSMenuItem separatorItem]];
2231         }
2232         // Cut
2233         if (!shared && !sharingAccount) {
2234             if (menuNodesCount == 1)
2235                 menuItemTitle = [NSString stringWithFormat:@"Cut \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
2236             else 
2237                 menuItemTitle = [NSString stringWithFormat:@"Cut %lu Items", menuNodesCount];
2238             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCut:) keyEquivalent:@""] autorelease];
2239             [menuItem setRepresentedObject:menuNodes];
2240             [menu addItem:menuItem];
2241         }
2242         // Copy
2243         if ((!shared && !sharingAccount) || 
2244             (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class]))) {
2245             if (menuNodesCount == 1)
2246                 menuItemTitle = [NSString stringWithFormat:@"Copy \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
2247             else 
2248                 menuItemTitle = [NSString stringWithFormat:@"Copy %lu Items", menuNodesCount];
2249             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCopy:) keyEquivalent:@""] autorelease];
2250             [menuItem setRepresentedObject:menuNodes];
2251             [menu addItem:menuItem];
2252         }
2253         // Paste
2254         if (!shared && !sharingAccount) {
2255             if (menuNodesCount == 1) {
2256                 PithosNode *menuNode = [menuNodes objectAtIndex:0];
2257                 if (([menuNode class] == [PithosSubdirNode class]) && 
2258                     (menuNode.pithosObject.subdir || ![menuNode.pithosObject.name hasSuffix:@"/"])) {
2259                     if (clipboardNodes) {
2260                         NSUInteger clipboardNodesCount = [clipboardNodes count];
2261                         if (clipboardNodesCount == 0) {
2262                             self.clipboardNodes = nil;
2263                         } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
2264                             if (clipboardNodesCount == 1)
2265                                 menuItemTitle = [NSString stringWithString:@"Paste Item"];
2266                             else
2267                                 menuItemTitle = [NSString stringWithString:@"Paste Items"];
2268                             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
2269                             [menuItem setRepresentedObject:menuNode];
2270                             [menu addItem:menuItem];
2271                         }
2272                     }
2273                 }
2274             }
2275         }
2276     }
2277 }
2278
2279 #pragma mark -
2280 #pragma mark Menu Actions
2281
2282 - (void)menuNewFolder:(NSMenuItem *)sender {
2283     PithosNode *node = (PithosNode *)[sender representedObject];
2284     if ([node class] == [PithosContainerNode class]) {
2285         // Operation: Create (upload) a new root application/directory object
2286         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2287             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2288             if (operation.isCancelled) {
2289                 [pool drain];
2290                 return;
2291             }
2292             NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2293                                                                   containerName:node.pithosContainer.name 
2294                                                                      subdirName:@"untitled folder"];
2295             NSString *fileName = [safeObjectName lastPathComponent];
2296             if (operation.isCancelled) {
2297                 [pool drain];
2298                 return;
2299             }
2300             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
2301                                                                                                containerName:node.pithosContainer.name 
2302                                                                                                   objectName:safeObjectName 
2303                                                                                                         eTag:nil 
2304                                                                                                  contentType:@"application/directory" 
2305                                                                                              contentEncoding:nil 
2306                                                                                           contentDisposition:nil 
2307                                                                                                     manifest:nil 
2308                                                                                                      sharing:nil 
2309                                                                                                     isPublic:ASIPithosObjectRequestPublicIgnore 
2310                                                                                                     metadata:nil 
2311                                                                                                         data:[NSData data]];
2312             objectRequest.delegate = self;
2313             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2314             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2315             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName];
2316             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
2317                                                                        message:messagePrefix];
2318             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2319                                       fileName, @"fileName", 
2320                                       [NSArray arrayWithObject:node], @"refreshNodes", 
2321                                       [NSNumber numberWithBool:YES], @"refresh", 
2322                                       activity, @"activity", 
2323                                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2324                                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2325                                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2326                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2327                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
2328                                       NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
2329                                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2330                                       uploadNetworkQueue, @"networkQueue", 
2331                                       @"upload", @"operationType", 
2332                                       nil];
2333             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2334             [pool drain];
2335         }];
2336         [uploadQueue addOperation:operation];
2337     } else if (([node class] == [PithosSubdirNode class]) && 
2338                (node.pithosObject.subdir || ![node.pithosObject.name hasSuffix:@"/"])) {
2339         // Operation: Create (upload) a new aplication/directory object
2340         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2341             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2342             if (operation.isCancelled) {
2343                 [pool drain];
2344                 return;
2345             }
2346             NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2347                                                                   containerName:node.pithosContainer.name 
2348                                                                      subdirName:[node.pithosObject.name stringByAppendingPathComponent:@"untitled folder"]];
2349             NSString *fileName = [safeObjectName lastPathComponent];
2350             if (operation.isCancelled) {
2351                 [pool drain];
2352                 return;
2353             }
2354             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
2355                                                                                                containerName:node.pithosContainer.name 
2356                                                                                                   objectName:safeObjectName 
2357                                                                                                         eTag:nil 
2358                                                                                                  contentType:@"application/directory" 
2359                                                                                              contentEncoding:nil 
2360                                                                                           contentDisposition:nil 
2361                                                                                                     manifest:nil 
2362                                                                                                      sharing:nil 
2363                                                                                                     isPublic:ASIPithosObjectRequestPublicIgnore 
2364                                                                                                     metadata:nil 
2365                                                                                                         data:[NSData data]];
2366             objectRequest.delegate = self;
2367             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2368             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2369             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName];
2370             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
2371                                                                        message:messagePrefix];
2372             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2373                                       fileName, @"fileName", 
2374                                       [NSArray arrayWithObject:node], @"refreshNodes", 
2375                                       [NSNumber numberWithBool:YES], @"refresh", 
2376                                       activity, @"activity", 
2377                                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2378                                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2379                                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2380                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2381                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
2382                                       NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
2383                                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2384                                       uploadNetworkQueue, @"networkQueue", 
2385                                       @"upload", @"operationType", 
2386                                       nil];
2387             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2388             [pool drain];
2389         }];
2390         [uploadQueue addOperation:operation];
2391     }
2392 }
2393
2394 - (void)menuGetInfo:(NSMenuItem *)sender {
2395     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2396         [node showPithosNodeInfo:sender];
2397     }
2398 }
2399
2400 - (void)menuDelete:(NSMenuItem *)sender {
2401     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2402         if (([node class] == [PithosObjectNode class]) || 
2403             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
2404             // Operation: Delete an object or subdir/ node
2405             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2406                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2407                 if (operation.isCancelled) {
2408                     [pool drain];
2409                     return;
2410                 }
2411                 NSString *fileName = [node.pithosObject.name lastPathComponent];
2412                 if ([node.pithosObject.name hasSuffix:@"/"])
2413                     fileName = [fileName stringByAppendingString:@"/"];
2414                 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos 
2415                                                                                                 containerName:node.pithosContainer.name 
2416                                                                                                    objectName:node.pithosObject.name];
2417                 if (operation.isCancelled) {
2418                     [pool drain];
2419                     return;
2420                 }
2421                 objectRequest.delegate = self;
2422                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2423                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2424                 NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", fileName];
2425                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
2426                                                                            message:messagePrefix];
2427                 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2428                                           fileName, @"fileName", 
2429                                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2430                                           activity, @"activity", 
2431                                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2432                                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2433                                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2434                                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2435                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2436                                           NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
2437                                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2438                                           deleteNetworkQueue, @"networkQueue", 
2439                                           @"delete", @"operationType", 
2440                                           nil];
2441                 [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2442                 [pool drain];
2443             }];
2444             [deleteQueue addOperation:operation];
2445         } else if ([node class] == [PithosSubdirNode class]) {
2446             // Operation: Delete a subdir node and its descendants
2447             // The resulting ASIPithosObjectRequests are chained through dependencies
2448             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2449                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2450                 if (operation.isCancelled) {
2451                     [pool drain];
2452                     return;
2453                 }
2454                 NSArray *objectRequests = [PithosUtilities deleteObjectRequestsForSubdirWithPithos:pithos 
2455                                                                                      containerName:node.pithosContainer.name 
2456                                                                                         objectName:node.pithosObject.name];
2457                 if (!operation.isCancelled && objectRequests) {
2458                     ASIPithosObjectRequest *previousObjectRequest = nil;
2459                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
2460                         if (operation.isCancelled) {
2461                             [pool drain];
2462                             return;
2463                         }
2464                         objectRequest.delegate = self;
2465                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2466                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2467                         NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
2468                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
2469                                                                                    message:messagePrefix];
2470                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2471                          [NSDictionary dictionaryWithObjectsAndKeys:
2472                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2473                           activity, @"activity", 
2474                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2475                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2476                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2477                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2478                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2479                           NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
2480                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2481                           deleteNetworkQueue, @"networkQueue", 
2482                           @"delete", @"operationType", 
2483                           nil]];
2484                         if (previousObjectRequest)
2485                             [objectRequest addDependency:previousObjectRequest];
2486                         previousObjectRequest = objectRequest;
2487                         [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2488                     }
2489                 }
2490                 [pool drain];
2491             }];
2492             [deleteQueue addOperation:operation];
2493         }
2494     }
2495 }
2496
2497 - (void)menuMoveToTrash:(NSMenuItem *)sender {
2498     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2499         if (([node class] == [PithosObjectNode class]) || 
2500             (([node class] == [PithosSubdirNode class]) && 
2501              !node.pithosObject.subdir &&
2502              [node.pithosObject.name hasSuffix:@"/"])) {
2503             // Operation: Move to trash an object or subdir/ node
2504             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2505                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2506                 if (operation.isCancelled) {
2507                     [pool drain];
2508                     return;
2509                 }
2510                 NSString *safeObjectName = [PithosUtilities safeObjectNameForPithos:pithos 
2511                                                                       containerName:@"trash" 
2512                                                                          objectName:node.pithosObject.name];
2513                 if (!operation.isCancelled && safeObjectName) {
2514                     ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
2515                                                                                            containerName:node.pithosContainer.name 
2516                                                                                               objectName:node.pithosObject.name 
2517                                                                                 destinationContainerName:@"trash" 
2518                                                                                    destinationObjectName:safeObjectName 
2519                                                                                            checkIfExists:NO];
2520                     if (!operation.isCancelled && objectRequest) {
2521                         objectRequest.delegate = self;
2522                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2523                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2524                         NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
2525                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2526                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2527                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2528                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
2529                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
2530                                                                                    message:messagePrefix];
2531                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2532                          [NSDictionary dictionaryWithObjectsAndKeys:
2533                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2534                           activity, @"activity", 
2535                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2536                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2537                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2538                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2539                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2540                           NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
2541                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2542                           moveNetworkQueue, @"networkQueue", 
2543                           @"move", @"operationType", 
2544                           nil]];
2545                         [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2546                     }
2547                 }
2548                 [pool drain];
2549             }];
2550             [moveQueue addOperation:operation];
2551         } else if ([node class] == [PithosSubdirNode class]) {
2552             // Operation: Move to trash a subdir node and its descendants
2553             // The resulting ASIPithosObjectRequests are chained through dependencies
2554             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2555                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2556                 if (operation.isCancelled) {
2557                     [pool drain];
2558                     return;
2559                 }
2560                 NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2561                                                                       containerName:@"trash" 
2562                                                                          subdirName:node.pithosObject.name];
2563                 if (!operation.isCancelled && safeObjectName) {
2564                     NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos 
2565                                                                                        containerName:node.pithosContainer.name 
2566                                                                                           objectName:node.pithosObject.name 
2567                                                                             destinationContainerName:@"trash" 
2568                                                                                destinationObjectName:safeObjectName 
2569                                                                                        checkIfExists:NO];
2570                     if (!operation.isCancelled && objectRequests) {
2571                         ASIPithosObjectRequest *previousObjectRequest = nil;
2572                         for (ASIPithosObjectRequest *objectRequest in objectRequests) {
2573                             if (operation.isCancelled) {
2574                                 [pool drain];
2575                                 return;
2576                             }
2577                             objectRequest.delegate = self;
2578                             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2579                             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2580                             NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
2581                                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2582                                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2583                                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2584                                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
2585                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
2586                                                                                        message:messagePrefix];
2587                             [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2588                              [NSDictionary dictionaryWithObjectsAndKeys:
2589                               [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2590                               activity, @"activity", 
2591                               [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2592                               [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2593                               [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2594                               [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2595                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
2596                               NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
2597                               NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2598                               moveNetworkQueue, @"networkQueue", 
2599                               @"move", @"operationType", 
2600                               nil]];
2601                             if (previousObjectRequest)
2602                                 [objectRequest addDependency:previousObjectRequest];
2603                             previousObjectRequest = objectRequest;
2604                             [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2605                         }
2606                     }
2607                 }
2608                 [pool drain];
2609             }];
2610             [moveQueue addOperation:operation];
2611         }
2612     }
2613 }
2614
2615 - (void)menuCut:(NSMenuItem *)sender {
2616     self.clipboardNodes = (NSArray *)[sender representedObject];
2617     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2618     self.clipboardCopy = NO;
2619 }
2620
2621 - (void)menuCopy:(NSMenuItem *)sender {
2622     self.clipboardNodes = (NSArray *)[sender representedObject];
2623     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2624     self.clipboardCopy = YES;
2625 }
2626
2627 - (void)menuPaste:(NSMenuItem *)sender {
2628     if (!clipboardNodes || ![clipboardNodes count])
2629         return;
2630     PithosNode *dropNode = (PithosNode *)[sender representedObject];
2631     NSArray *localClipboardNodes = [NSArray arrayWithArray:clipboardNodes];
2632     if (!clipboardCopy && ![dropNode isEqualTo:clipboardParentNode]) {
2633         self.clipboardNodes = nil;
2634         self.clipboardParentNode = nil;
2635         [self moveNodes:localClipboardNodes toNode:dropNode];
2636     } else {
2637         [self copyNodes:localClipboardNodes toNode:dropNode];
2638     }
2639 }
2640     
2641 #pragma mark -
2642 #pragma mark PithosActivityFacilityDelegate
2643
2644 - (void)activityUpdate:(NSDictionary *)info {
2645     NSString *message = [info objectForKey:@"message"];
2646     NSUInteger runningActivitiesCount = [[info objectForKey:@"runningActivitiesCount"] unsignedIntegerValue];
2647 //    NSUInteger endingActivitiesCount = [[info objectForKey:@"endingActivitiesCount"] unsignedIntegerValue];
2648     NSUInteger totalUploadBytes = [[info objectForKey:@"totalUploadBytes"] unsignedIntegerValue];
2649     NSUInteger currentUploadBytes = [[info objectForKey:@"currentUploadBytes"] unsignedIntegerValue];
2650     NSUInteger totalDownloadBytes = [[info objectForKey:@"totalDownloadBytes"] unsignedIntegerValue];
2651     NSUInteger currentDownloadBytes = [[info objectForKey:@"currentDownloadBytes"] unsignedIntegerValue];
2652     NSUInteger totalBytes = totalUploadBytes + totalDownloadBytes;
2653     if (runningActivitiesCount && totalBytes) {
2654         [activityProgressIndicator setDoubleValue:((currentUploadBytes + currentDownloadBytes + 0.0)/(totalBytes + 0.0))];
2655         [activityProgressIndicator startAnimation:self];
2656     } else {
2657         [activityProgressIndicator setDoubleValue:1.0];
2658         [activityProgressIndicator stopAnimation:self];
2659     }
2660     
2661     if (!message)
2662         message = [[[[UsingSizeTransformer alloc] init] autorelease] transformedValue:accountNode.pithosAccount];
2663     [activityTextField setStringValue:message];
2664 }
2665
2666 @end