19782107e3ca5eb3ab7c4a7a31f940303798e8f0
[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         [names addObject:node.displayName];
811         // If the node is a subdir ask if the whole tree should be downloaded
812         if ([node class] == [PithosSubdirNode class]) {
813             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
814             [alert setMessageText:@"Download directory"];
815             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
816             [alert addButtonWithTitle:@"OK"];
817             [alert addButtonWithTitle:@"Cancel"];
818             NSInteger choice = [alert runModal];
819             if (choice == NSAlertFirstButtonReturn)
820                 [self downloadNode:node toDirectory:[dropDestination path] withNewFileName:nil version:nil];
821         } else {
822             [self downloadNode:node toDirectory:[dropDestination path] withNewFileName:nil version:nil];
823         }
824     }
825     return names;
826 }
827
828 #pragma mark Drag and Drop destination
829
830 - (NSDragOperation)browser:aBrowser 
831               validateDrop:(id<NSDraggingInfo>)info 
832                proposedRow:(NSInteger *)row 
833                     column:(NSInteger *)column 
834              dropOperation:(NSBrowserDropOperation *)dropOperation {
835     NSDragOperation result = NSDragOperationNone;
836     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
837         // For a drop above, the drop is redirected to the parent item
838         if (*dropOperation == NSBrowserDropAbove)
839             *row = -1;
840         // Only allow dropping in folders
841         if (*column != -1) {
842             PithosNode *dropNode;
843             if (*row != -1) {
844                 // Check if the node is not a folder and if so redirect to the parent item
845                 dropNode = [browser itemAtRow:*row inColumn:*column];
846                 if ([dropNode class] == [PithosObjectNode class])
847                     *row = -1;
848             }
849             if (*row == -1)
850                 dropNode = [browser parentForItemsInColumn:*column];
851             
852             if (!dropNode.shared && 
853                 (!dropNode.sharingAccount || 
854                  ([dropNode class] == [PithosSubdirNode class]) || 
855                  ([dropNode class] == [PithosContainerNode class]))) {
856                 *dropOperation = NSBrowserDropOn;
857                 result = NSDragOperationCopy;
858             }
859         }
860     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
861         // For a drop above, the drop is redirected to the parent item
862         if (*dropOperation == NSBrowserDropAbove) 
863             *row = -1;
864         // Only allow dropping in folders
865         if (*column != -1) {
866             PithosNode *dropNode;
867             if (*row != -1) {
868                 // Check if the node is not a folder and if so redirect to the parent item
869                 dropNode = [browser itemAtRow:*row inColumn:*column];
870                 if ([dropNode class] == [PithosObjectNode class])
871                     *row = -1;
872             }
873             if (*row == -1)
874                 dropNode = [browser parentForItemsInColumn:*column];
875             
876             if (!dropNode.shared && !dropNode.sharingAccount) {
877                 if ([info draggingSourceOperationMask] & NSDragOperationMove) {
878                     // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
879                     if ((([dropNode class] == [PithosContainerNode class]) || 
880                          dropNode.pithosObject.subdir || 
881                          ![dropNode.pithosObject.name hasSuffix:@"/"]) && 
882                         ![dropNode isEqualTo:draggedParentNode]) { 
883     //                    ![dropNode isEqualTo:draggedParentNode] && 
884     //                    ![draggedNodes containsObject:dropNode]) {                
885                         result = NSDragOperationMove;
886                     }
887                 } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
888                     // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
889                     if (([dropNode class] == [PithosContainerNode class]) || 
890                         dropNode.pithosObject.subdir || 
891                         ![dropNode.pithosObject.name hasSuffix:@"/"]) { 
892                         result = NSDragOperationCopy;
893                     }
894                 }
895             }
896         }
897     }
898     return result;
899 }
900
901 - (BOOL)browser:(NSBrowser *)aBrowser 
902      acceptDrop:(id<NSDraggingInfo>)info 
903           atRow:(NSInteger)row 
904          column:(NSInteger)column 
905   dropOperation:(NSBrowserDropOperation)dropOperation {
906     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
907         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
908         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
909         if ((column != -1) && (filenames != nil)) {
910             PithosNode *node;
911             if (row != -1)
912                 node = [browser itemAtRow:row inColumn:column];
913             else
914                 node = [browser parentForItemsInColumn:column];
915             NSLog(@"drag in node: %@", node.url);
916             return [self uploadFiles:filenames toNode:node];
917         }
918     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
919         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
920         if ((column != -1) && (draggedNodes != nil)) {
921             PithosNode *node;
922             if (row != -1)
923                 node = [browser itemAtRow:row inColumn:column];
924             else
925                 node = [browser parentForItemsInColumn:column];
926             NSLog(@"drag local node: %@", node.url);
927             if ([info draggingSourceOperationMask] & NSDragOperationMove)
928                 return [self moveNodes:draggedNodes toNode:node];
929             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
930                 return [self copyNodes:draggedNodes toNode:node];
931         }
932     }
933     return NO;
934 }
935
936 #pragma mark -
937 #pragma mark Drag and Drop methods
938
939 - (void)downloadNode:(PithosNode *)node 
940          toDirectory:(NSString *)dirPath 
941      withNewFileName:(NSString *)newFileName 
942              version:(NSString *)version {
943     if ([node class] == [PithosSubdirNode class]) {
944         // XXX newFilename and version are ignored in the case of a subdir node for now
945         // Operation: Download a subdir node and its descendants
946         // The resulting ASIPithosObjectRequests are chained through dependencies
947         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
948             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
949             if (operation.isCancelled) {
950                 [pool drain];
951                 return;
952             }
953             NSArray *objectRequests = [PithosUtilities objectDataRequestsForSubdirWithPithos:pithos 
954                                                                                containerName:node.pithosContainer.name 
955                                                                                   objectName:node.pithosObject.name 
956                                                                                  toDirectory:dirPath 
957                                                                                checkIfExists:YES 
958                                                                               sharingAccount:node.sharingAccount];
959             if (!operation.isCancelled && objectRequests) {
960                 ASIPithosObjectRequest *previousObjectRequest = nil;
961                 for (__block ASIPithosObjectRequest *objectRequest in objectRequests) {
962                     if (operation.isCancelled) {
963                         [pool drain];
964                         return;
965                     }
966                     objectRequest.delegate = self;
967                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
968                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
969                     NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
970                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
971                                                                                message:[messagePrefix stringByAppendingString:@" (0%)"]
972                                                                             totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] 
973                                                                           currentBytes:0];
974                     dispatch_async(dispatch_get_main_queue(), ^{
975                         [activityFacility updateActivity:activity withMessage:activity.message];  
976                     });
977                     [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
978                      [NSDictionary dictionaryWithObjectsAndKeys:
979                       activity, @"activity", 
980                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
981                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
982                       [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
983                       [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
984                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
985                       NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", 
986                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
987                       downloadNetworkQueue, @"networkQueue", 
988                       @"download", @"operationType", 
989                       nil]];
990                     [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
991                         [activityFacility updateActivity:activity 
992                                              withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
993                                               totalBytes:activity.totalBytes 
994                                             currentBytes:(activity.currentBytes + size)];
995                     }];
996                     if (previousObjectRequest)
997                         [objectRequest addDependency:previousObjectRequest];
998                     previousObjectRequest = objectRequest;
999                     [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1000                 }
1001             }
1002             [pool drain];
1003         }];
1004         [downloadQueue addOperation:operation];
1005     } else if ([node class] == [PithosObjectNode class]) {
1006         // Operation: Download an object node
1007         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1008             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1009             if (operation.isCancelled) {
1010                 [pool drain];
1011                 return;
1012             }
1013             __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectDataRequestWithPithos:pithos 
1014                                                                                            containerName:node.pithosContainer.name 
1015                                                                                               objectName:node.pithosObject.name 
1016                                                                                                  version:version 
1017                                                                                             toDirectory:dirPath 
1018                                                                                          withNewFileName:newFileName 
1019                                                                                            checkIfExists:(version ? NO : YES) 
1020                                                                                           sharingAccount:node.sharingAccount];
1021             if (!operation.isCancelled && objectRequest) {
1022                 objectRequest.delegate = self;
1023                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1024                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1025                 NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1026                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
1027                                                                            message:[messagePrefix stringByAppendingString:@" (0%)"]
1028                                                                         totalBytes:node.pithosObject.bytes 
1029                                                                       currentBytes:0];
1030                 dispatch_async(dispatch_get_main_queue(), ^{
1031                     [activityFacility updateActivity:activity withMessage:activity.message];  
1032                 });
1033                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1034                  [NSDictionary dictionaryWithObjectsAndKeys:
1035                   activity, @"activity", 
1036                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1037                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1038                   [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
1039                   [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1040                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
1041                   NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", 
1042                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1043                   downloadNetworkQueue, @"networkQueue", 
1044                   @"download", @"operationType", 
1045                   nil]];
1046                 [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1047                     [activityFacility updateActivity:activity 
1048                                          withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
1049                                           totalBytes:activity.totalBytes 
1050                                         currentBytes:(activity.currentBytes + size)];
1051                 }];
1052                 [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1053                 [pool drain];
1054             }
1055         }];
1056         [downloadQueue addOperation:operation];
1057     }
1058 }
1059
1060 - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode {
1061     if (([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class]))
1062         return NO;
1063     NSFileManager *fileManager = [NSFileManager defaultManager];
1064     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1065     NSString *objectNamePrefix;
1066     if ([destinationNode class] == [PithosSubdirNode class])
1067         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1068     else
1069         objectNamePrefix = [NSString string];
1070     if ((destinationNode.pithosContainer.blockHash == nil) || (destinationNode.pithosContainer.blockSize == 0)) {
1071         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest containerMetadataRequestWithPithos:pithos 
1072                                                                                                       containerName:containerName];
1073         ASINetworkQueue *networkQueue = [ASINetworkQueue queue];
1074         [networkQueue go];
1075         [networkQueue addOperations:[NSArray arrayWithObject:[PithosUtilities prepareRequest:containerRequest]] waitUntilFinished:YES];
1076         if ([containerRequest error]) {
1077             [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
1078             return NO;
1079         } else if (containerRequest.responseStatusCode != 204) {
1080             [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest];
1081             return NO;
1082         }
1083         destinationNode.pithosContainer.blockHash = [containerRequest blockHash];
1084         destinationNode.pithosContainer.blockSize = [containerRequest blockSize];
1085     }    
1086     NSUInteger blockSize = destinationNode.pithosContainer.blockSize;
1087     NSString *blockHash = destinationNode.pithosContainer.blockHash;
1088     
1089     for (NSString *filePath in filenames) {
1090         BOOL isDirectory;
1091         if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
1092             if (!isDirectory) {
1093                 // Upload file
1094                 NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
1095                 // Operation: Upload a local file
1096                 __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1097                     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1098                     if (operation.isCancelled) {
1099                         [pool drain];
1100                         return;
1101                     }
1102                     NSError *error = nil;
1103                     NSString *contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
1104                     if (contentType == nil)
1105                         contentType = @"application/octet-stream";
1106                     if (error)
1107                         NSLog(@"contentType detection error: %@", error);
1108                     NSArray *hashes = nil;
1109                     if (operation.isCancelled) {
1110                         [pool drain];
1111                         return;
1112                     }
1113                     ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1114                                                                                                 containerName:containerName 
1115                                                                                                    objectName:objectName 
1116                                                                                                   contentType:contentType 
1117                                                                                                     blockSize:blockSize 
1118                                                                                                     blockHash:blockHash 
1119                                                                                                       forFile:filePath 
1120                                                                                                 checkIfExists:YES 
1121                                                                                                        hashes:&hashes 
1122                                                                                                sharingAccount:destinationNode.sharingAccount];
1123                     if (!operation.isCancelled && objectRequest) {
1124                         objectRequest.delegate = self;
1125                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1126                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1127                         NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1128                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1129                                                                                    message:[messagePrefix stringByAppendingString:@" (0%)"]
1130                                                                                 totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1131                                                                               currentBytes:0];
1132                         [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1133                          [NSDictionary dictionaryWithObjectsAndKeys:
1134                           containerName, @"containerName", 
1135                           objectName, @"objectName", 
1136                           contentType, @"contentType", 
1137                           [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
1138                           blockHash, @"blockHash", 
1139                           filePath, @"filePath", 
1140                           hashes, @"hashes", 
1141                           [NSArray arrayWithObject:destinationNode], @"refreshNodes", 
1142                           [NSNumber numberWithBool:YES], @"refresh", 
1143                           [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1144                           activity, @"activity", 
1145                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1146                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1147                           [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
1148                           [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1149                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1150                           NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1151                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1152                           uploadNetworkQueue, @"networkQueue", 
1153                           @"upload", @"operationType", 
1154                           nil]];
1155                         if (destinationNode.sharingAccount)
1156                             [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
1157                         [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1158                     }
1159                     [pool drain];
1160                 }];
1161                 [uploadQueue addOperation:operation];
1162             } else {
1163                 // Upload directory, confirm first
1164                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1165                 [alert setMessageText:@"Upload directory"];
1166                 [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]];
1167                 [alert addButtonWithTitle:@"OK"];
1168                 [alert addButtonWithTitle:@"Cancel"];
1169                 NSInteger choice = [alert runModal];
1170                 if (choice == NSAlertFirstButtonReturn) {
1171                     NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
1172                     // Operation: Upload a local directory and its descendants
1173                     // The resulting ASIPithosObjectRequests are chained through dependencies
1174                     __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1175                         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1176                         if (operation.isCancelled) {
1177                             [pool drain];
1178                             return;
1179                         }
1180                         NSMutableArray *objectNames = nil;
1181                         NSMutableArray *contentTypes = nil;
1182                         NSMutableArray *filePaths = nil;
1183                         NSMutableArray *hashesArrays = nil;
1184                         NSMutableArray *directoryObjectRequests = nil;
1185                         NSArray *objectRequests = [PithosUtilities writeObjectDataRequestsWithPithos:pithos 
1186                                                                                        containerName:containerName 
1187                                                                                           objectName:objectName 
1188                                                                                            blockSize:blockSize 
1189                                                                                            blockHash:blockHash 
1190                                                                                         forDirectory:filePath 
1191                                                                                        checkIfExists:YES 
1192                                                                                          objectNames:&objectNames
1193                                                                                         contentTypes:&contentTypes
1194                                                                                            filePaths:&filePaths
1195                                                                                         hashesArrays:&hashesArrays 
1196                                                                              directoryObjectRequests:&directoryObjectRequests 
1197                                                                                       sharingAccount:destinationNode.sharingAccount];
1198                         if (operation.isCancelled) {
1199                             [pool drain];
1200                             return;
1201                         }
1202                         ASIPithosObjectRequest *previousObjectRequest = nil;
1203                         for (ASIPithosObjectRequest *objectRequest in directoryObjectRequests) {
1204                             if (operation.isCancelled) {
1205                                 [pool drain];
1206                                 return;
1207                             }
1208                             objectRequest.delegate = self;
1209                             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1210                             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1211                             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1212                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1213                                                                                        message:messagePrefix];
1214                             [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1215                              [NSDictionary dictionaryWithObjectsAndKeys:
1216                               [NSNumber numberWithBool:YES], @"refresh", 
1217                               activity, @"activity", 
1218                               [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1219                               [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1220                               [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1221                               [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1222                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
1223                               NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
1224                               NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1225                               uploadNetworkQueue, @"networkQueue", 
1226                               @"upload", @"operationType", 
1227                               nil]];
1228                             if (previousObjectRequest)
1229                                 [objectRequest addDependency:previousObjectRequest];
1230                             previousObjectRequest = objectRequest;
1231                             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1232                         }
1233                         if (!operation.isCancelled && objectRequests) {
1234                             for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) {
1235                                 if (operation.isCancelled) {
1236                                     [pool drain];
1237                                     return;
1238                                 }
1239                                 ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i];
1240                                 objectRequest.delegate = self;
1241                                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1242                                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1243                                 NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
1244                                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1245                                                                                            message:[messagePrefix stringByAppendingString:@" (0%)"]
1246                                                                                         totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1247                                                                                       currentBytes:0];
1248                                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1249                                  [NSDictionary dictionaryWithObjectsAndKeys:
1250                                   containerName, @"containerName", 
1251                                   [objectNames objectAtIndex:i], @"objectName", 
1252                                   [contentTypes objectAtIndex:i], @"contentType", 
1253                                   [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
1254                                   blockHash, @"blockHash", 
1255                                   [filePaths objectAtIndex:i], @"filePath", 
1256                                   [hashesArrays objectAtIndex:i], @"hashes", 
1257                                   [NSNumber numberWithBool:YES], @"refresh", 
1258                                   [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1259                                   activity, @"activity", 
1260                                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1261                                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1262                                   [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", 
1263                                   [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1264                                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
1265                                   NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1266                                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1267                                   uploadNetworkQueue, @"networkQueue", 
1268                                   @"upload", @"operationType", 
1269                                   nil]];
1270                                 if (destinationNode.sharingAccount)
1271                                     [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
1272                                 if (previousObjectRequest)
1273                                     [objectRequest addDependency:previousObjectRequest];
1274                                 previousObjectRequest = objectRequest;
1275                                 [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]];
1276                             }
1277                         }
1278                         [pool drain];
1279                     }];
1280                     [uploadQueue addOperation:operation];
1281                 }
1282             }
1283         }
1284     }
1285     return YES;
1286 }
1287
1288 - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {
1289     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
1290         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
1291         return NO;
1292     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1293     NSString *objectNamePrefix;
1294     if ([destinationNode class] == [PithosSubdirNode class])
1295         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1296     else
1297         objectNamePrefix = [NSString string];
1298
1299     for (PithosNode *node in nodes) {
1300         if (([node class] == [PithosObjectNode class]) || 
1301             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1302             // Operation: Move an object or subdir/ node
1303             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1304                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1305                 if (operation.isCancelled) {
1306                     [pool drain];
1307                     return;
1308                 }
1309                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1310                 if ([node.pithosObject.name hasSuffix:@"/"])
1311                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1312                 ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
1313                                                                                        containerName:node.pithosContainer.name 
1314                                                                                           objectName:node.pithosObject.name 
1315                                                                             destinationContainerName:containerName 
1316                                                                                destinationObjectName:destinationObjectName 
1317                                                                                        checkIfExists:YES];
1318                 if (!operation.isCancelled && objectRequest) {
1319                     objectRequest.delegate = self;
1320                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1321                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1322                     NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1323                                                [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1324                                                [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1325                                                [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1326                                                [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1327                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1328                                                                                message:messagePrefix];
1329                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1330                      [NSDictionary dictionaryWithObjectsAndKeys:
1331                       [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
1332                       activity, @"activity", 
1333                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1334                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1335                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1336                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1337                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1338                       NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
1339                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1340                       moveNetworkQueue, @"networkQueue", 
1341                       @"move", @"operationType", 
1342                       nil]];
1343                     [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1344                 }
1345                 [pool drain];
1346             }];
1347             [moveQueue addOperation:operation];
1348         } else if ([node class] == [PithosSubdirNode class]) {
1349             // Operation: Move a subdir node and its descendants
1350             // The resulting ASIPithosObjectRequests are chained through dependencies
1351             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1352                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1353                 if (operation.isCancelled) {
1354                     [pool drain];
1355                     return;
1356                 }
1357                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1358                 if (node.pithosObject.subdir)
1359                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1360                 NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos 
1361                                                                                    containerName:node.pithosContainer.name 
1362                                                                                       objectName:node.pithosObject.name 
1363                                                                         destinationContainerName:containerName 
1364                                                                            destinationObjectName:destinationObjectName 
1365                                                                                    checkIfExists:YES];
1366                 if (!operation.isCancelled && objectRequests) {
1367                     ASIPithosObjectRequest *previousObjectRequest = nil;
1368                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1369                         if (operation.isCancelled) {
1370                             [pool drain];
1371                             return;
1372                         }
1373                         objectRequest.delegate = self;
1374                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1375                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1376                         NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1377                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1378                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1379                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1380                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1381                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1382                                                                                    message:messagePrefix];
1383                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1384                          [NSDictionary dictionaryWithObjectsAndKeys:
1385                           [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
1386                           [NSNumber numberWithBool:YES], @"refresh", 
1387                           activity, @"activity", 
1388                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1389                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1390                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1391                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1392                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1393                           NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
1394                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1395                           moveNetworkQueue, @"networkQueue", 
1396                           @"move", @"operationType", 
1397                           nil]];
1398                         if (previousObjectRequest)
1399                             [objectRequest addDependency:previousObjectRequest];
1400                         previousObjectRequest = objectRequest;
1401                         [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1402                     }
1403                 }
1404                 [pool drain];
1405             }];
1406             [moveQueue addOperation:operation];
1407         }
1408     }
1409     return YES;
1410 }
1411
1412 - (BOOL)copyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {    
1413     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
1414         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
1415         return NO;
1416     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
1417     NSString *objectNamePrefix;
1418     if ([destinationNode class] == [PithosSubdirNode class])
1419         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
1420     else
1421         objectNamePrefix = [NSString string];
1422     
1423     for (PithosNode *node in nodes) {
1424         if (([node class] == [PithosObjectNode class]) || 
1425             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1426             // Operation: Copy an object or subdir/ node
1427             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1428                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1429                 if (operation.isCancelled) {
1430                     [pool drain];
1431                     return;
1432                 }
1433                 NSString *destinationObjectName;
1434                 if (![destinationNode isEqualTo:node.parent]) {
1435                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1436                     if ([node.pithosObject.name hasSuffix:@"/"])
1437                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1438                 } else {
1439                     destinationObjectName = [PithosUtilities safeObjectNameForPithos:pithos 
1440                                                                        containerName:containerName 
1441                                                                           objectName:node.pithosObject.name];
1442                 }
1443                 if (operation.isCancelled) {
1444                     [pool drain];
1445                     return;
1446                 }
1447                 ASIPithosObjectRequest *objectRequest = [PithosUtilities copyObjectRequestWithPithos:pithos 
1448                                                                                        containerName:node.pithosContainer.name 
1449                                                                                           objectName:node.pithosObject.name 
1450                                                                             destinationContainerName:containerName 
1451                                                                                destinationObjectName:destinationObjectName 
1452                                                                                        checkIfExists:YES 
1453                                                                                       sharingAccount:node.sharingAccount];
1454                 if (!operation.isCancelled && objectRequest) {
1455                     objectRequest.delegate = self;
1456                     objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1457                     objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1458                     NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1459                                                [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1460                                                [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1461                                                [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1462                                                [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1463                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1464                                                                                message:messagePrefix];
1465                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1466                      [NSDictionary dictionaryWithObjectsAndKeys:
1467                       [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1468                       activity, @"activity", 
1469                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1470                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1471                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1472                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1473                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1474                       NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", 
1475                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1476                       copyNetworkQueue, @"networkQueue", 
1477                       @"copy", @"operationType", 
1478                       nil]];
1479                     [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1480                 }
1481                 [pool drain];
1482             }];
1483             [copyQueue addOperation:operation];
1484         } else if ([node class] == [PithosSubdirNode class]) {
1485             // Operation: Copy a subdir node and its descendants
1486             // The resulting ASIPithosObjectRequests are chained through dependencies
1487             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
1488                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1489                 if (operation.isCancelled) {
1490                     [pool drain];
1491                     return;
1492                 }
1493                 NSString *destinationObjectName;
1494                 if (![destinationNode isEqualTo:node.parent]) {
1495                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1496                     if (node.pithosObject.subdir)
1497                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1498                 } else {
1499                     destinationObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
1500                                                                        containerName:containerName 
1501                                                                           subdirName:node.pithosObject.name];
1502                 }
1503                 if (operation.isCancelled) {
1504                     [pool drain];
1505                     return;
1506                 }
1507                 NSArray *objectRequests = [PithosUtilities copyObjectRequestsForSubdirWithPithos:pithos 
1508                                                                                    containerName:node.pithosContainer.name 
1509                                                                                       objectName:node.pithosObject.name 
1510                                                                         destinationContainerName:containerName 
1511                                                                            destinationObjectName:destinationObjectName 
1512                                                                                    checkIfExists:YES 
1513                                                                                   sharingAccount:node.sharingAccount];
1514                 if (!operation.isCancelled && objectRequests) {
1515                     ASIPithosObjectRequest *previousObjectRequest = nil;
1516                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1517                         if (operation.isCancelled) {
1518                             [pool drain];
1519                             return;
1520                         }
1521                         objectRequest.delegate = self;
1522                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1523                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1524                         NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1525                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1526                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1527                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1528                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
1529                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1530                                                                                    message:messagePrefix];
1531                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1532                          [NSDictionary dictionaryWithObjectsAndKeys:
1533                           [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1534                           activity, @"activity", 
1535                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1536                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1537                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1538                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1539                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1540                           NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", 
1541                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1542                           copyNetworkQueue, @"networkQueue", 
1543                           @"copy", @"operationType", 
1544                           nil]];
1545                         if (previousObjectRequest)
1546                             [objectRequest addDependency:previousObjectRequest];
1547                         previousObjectRequest = objectRequest;
1548                         [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1549                     }
1550                 }
1551                 [pool drain];
1552             }];
1553             [copyQueue addOperation:operation];
1554         }
1555     }
1556     return YES;
1557 }
1558
1559 #pragma mark -
1560 #pragma mark ASIHTTPRequestDelegate
1561
1562 - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
1563     NSOperationQueue *callbackQueue;
1564     NSString *operationType = [request.userInfo objectForKey:@"operationType"];
1565     if ([operationType isEqualToString:@"move"])
1566         callbackQueue = moveCallbackQueue;
1567     else if ([operationType isEqualToString:@"copy"])
1568         callbackQueue = copyCallbackQueue;
1569     else if ([operationType isEqualToString:@"delete"])
1570         callbackQueue = deleteCallbackQueue;
1571     else if ([operationType isEqualToString:@"upload"])
1572         callbackQueue = uploadCallbackQueue;
1573     else if ([operationType isEqualToString:@"download"])
1574         callbackQueue = downloadCallbackQueue;
1575     else {
1576         dispatch_async(dispatch_get_main_queue(), ^{
1577             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1578                               withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1579         });
1580         return;
1581     }
1582     // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1583     NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
1584                                                                              selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) 
1585                                                                                object:request] autorelease];
1586     operation.completionBlock = ^{
1587         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1588         if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1589             dispatch_async(dispatch_get_main_queue(), ^{
1590                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1591                                   withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1592             });
1593         }
1594         [pool drain];
1595     };
1596     [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1597     [callbackQueue addOperation:operation];
1598 }
1599
1600 - (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request {
1601     if (request.isCancelled) {
1602         // Request has been cancelled 
1603         // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway
1604         [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1605                    withObject:request];
1606     } else {
1607         NSOperationQueue *callbackQueue;
1608         NSString *operationType = [request.userInfo objectForKey:@"operationType"];
1609         if ([operationType isEqualToString:@"move"])
1610             callbackQueue = moveCallbackQueue;
1611         else if ([operationType isEqualToString:@"copy"])
1612             callbackQueue = copyCallbackQueue;
1613         else if ([operationType isEqualToString:@"delete"])
1614             callbackQueue = deleteCallbackQueue;
1615         else if ([operationType isEqualToString:@"upload"])
1616             callbackQueue = uploadCallbackQueue;
1617         else if ([operationType isEqualToString:@"download"])
1618             callbackQueue = downloadCallbackQueue;
1619         else {
1620             dispatch_async(dispatch_get_main_queue(), ^{
1621                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1622                                   withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1623             });
1624             return;
1625         }
1626         // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1627         NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
1628                                                                                  selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1629                                                                                    object:request] autorelease];
1630         operation.completionBlock = ^{
1631             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1632             if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1633                 dispatch_async(dispatch_get_main_queue(), ^{
1634                     [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1635                                       withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1636                 });
1637             }
1638             [pool drain];
1639         };
1640         [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1641         [callbackQueue addOperation:operation];
1642     }
1643 }
1644
1645 - (void)requestFailed:(ASIPithosRequest *)request {
1646     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1647     NSOperation *operation = [request.userInfo objectForKey:@"operation"];
1648     NSLog(@"Request failed: %@", request.url);
1649     if (operation.isCancelled) {
1650         [pool drain];
1651         return;        
1652     }
1653     if (request.isCancelled) {
1654         dispatch_async(dispatch_get_main_queue(), ^{
1655             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1656                               withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1657         });
1658         [pool drain];
1659         return;
1660     }
1661     NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1662     if (retries > 0) {
1663         ASIPithosRequest *newRequest = (ASIPithosRequest *)[[PithosUtilities copyRequest:request] autorelease];
1664         [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1665         [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithBool:NO] forKey:@"unexpectedResponseStatus"];
1666         [[newRequest.userInfo objectForKey:@"networkQueue"] addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
1667     } else {
1668         dispatch_async(dispatch_get_main_queue(), ^{
1669             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1670                               withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
1671             if ([[request.userInfo objectForKey:@"unexpectedResponseStatus"] boolValue])
1672                 [PithosUtilities unexpectedResponseStatusAlertWithRequest:request];
1673             else
1674                 [PithosUtilities httpRequestErrorAlertWithRequest:request];
1675         });
1676     }
1677     [pool drain];
1678 }
1679
1680 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1681     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1682     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1683     NSLog(@"Download finished: %@", objectRequest.url);
1684     if (operation.isCancelled) {
1685         [self requestFailed:objectRequest];
1686     } else if (objectRequest.responseStatusCode == 200) {
1687         NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1688         PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1689         NSUInteger totalBytes = activity.totalBytes;
1690         
1691         // XXX change contentLength to objectContentLength if it is fixed in the server
1692         if ([objectRequest contentLength] == 0) {
1693             // The check above was:
1694             // if (([objectRequest contentLength] == 0) && ![PithosUtilities isContentTypeDirectory:[objectRequest contentType]]) {
1695             // I checked for directory content types in order not to create a file in place of a directory,
1696             // but this callback method is not called in the case of a directory download.
1697             // It maybe the case though, when downloading an old version of an object, is of a directory content type.
1698             // In this case, a file should be created. This is actually a feature that allows you to hide data in a directory object.
1699             NSLog(@"Downloaded  0 bytes");
1700             NSFileManager *fileManager = [NSFileManager defaultManager];
1701             if (![fileManager fileExistsAtPath:filePath]) {
1702                 if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
1703                     dispatch_async(dispatch_get_main_queue(), ^{
1704                         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1705                         [alert setMessageText:@"Create File Error"];
1706                         [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
1707                         [alert addButtonWithTitle:@"OK"];
1708                         [alert runModal];
1709                     });
1710                 }
1711             }
1712         }
1713         
1714         NSUInteger currentBytes = [objectRequest objectContentLength];
1715         if (currentBytes == 0)
1716             currentBytes = totalBytes;
1717         dispatch_async(dispatch_get_main_queue(), ^{
1718             [activityFacility endActivity:activity 
1719                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
1720                                totalBytes:totalBytes 
1721                              currentBytes:currentBytes];
1722         });
1723     } else {
1724         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1725         [self requestFailed:objectRequest];
1726     }
1727     [pool drain];
1728 }
1729
1730 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1731     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1732     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1733     NSLog(@"Upload directory object finished: %@", objectRequest.url);
1734     if (operation.isCancelled) {
1735         [self requestFailed:objectRequest];
1736     } else if (objectRequest.responseStatusCode == 201) {
1737         NSLog(@"Directory object created: %@", objectRequest.url);
1738         dispatch_async(dispatch_get_main_queue(), ^{
1739             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1740                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1741         });
1742         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1743             [node forceRefresh];
1744         }
1745         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1746             [node refresh];
1747         }
1748         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1749             [self forceRefresh:self];
1750         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1751             [self refresh:self];
1752     } else {
1753         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1754         [self requestFailed:objectRequest];
1755     }
1756     [pool drain];
1757 }
1758
1759 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1760     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1761     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1762     NSLog(@"Upload using hashmap finished: %@", objectRequest.url);
1763     NSString *fileName = [objectRequest.userInfo objectForKey:@"fileName"];
1764     PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1765     NSUInteger totalBytes = activity.totalBytes;
1766     NSUInteger currentBytes = activity.currentBytes;
1767     if (operation.isCancelled) {
1768         [self requestFailed:objectRequest];
1769     } else if (objectRequest.responseStatusCode == 201) {
1770         NSLog(@"Object created: %@", objectRequest.url);
1771         dispatch_async(dispatch_get_main_queue(), ^{
1772             [activityFacility endActivity:activity 
1773                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
1774                                totalBytes:totalBytes 
1775                              currentBytes:totalBytes];
1776         });
1777         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1778             [node forceRefresh];
1779         }
1780         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1781             [node refresh];
1782         }
1783         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1784             [self forceRefresh:self];
1785         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1786             [self refresh:self];        
1787     } else if (objectRequest.responseStatusCode == 409) {
1788         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1789         if (iteration == 0) {
1790             NSLog(@"Upload iteration limit reached: %@", objectRequest.url);
1791             dispatch_async(dispatch_get_main_queue(), ^{
1792                 [activityFacility endActivity:activity 
1793                                   withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; 
1794                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1795                 [alert setMessageText:@"Upload Timeout"];
1796                 [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
1797                                            [objectRequest.userInfo objectForKey:@"objectName"]]];
1798                 [alert addButtonWithTitle:@"OK"];
1799                 [alert runModal];
1800             });
1801             [pool drain];
1802             return;
1803         }
1804         NSLog(@"object is missing hashes: %@", objectRequest.url);
1805         NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1806                                                           withMissingHashes:[objectRequest hashes]];
1807         NSUInteger blockSize = [[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1808         if (totalBytes >= [missingBlocks count]*blockSize)
1809             currentBytes = totalBytes - [missingBlocks count]*blockSize;
1810         dispatch_async(dispatch_get_main_queue(), ^{
1811             [activityFacility updateActivity:activity 
1812                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (totalBytes ? (100*(currentBytes + 0.0)/(totalBytes + 0.0)) : 100)] 
1813                                   totalBytes:totalBytes 
1814                                 currentBytes:currentBytes];
1815         });
1816         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1817         __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
1818                                                                                                          containerName:[objectRequest.userInfo objectForKey:@"containerName"] 
1819                                                                                                              blockSize:blockSize 
1820                                                                                                                forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
1821                                                                                                      missingBlockIndex:missingBlockIndex 
1822                                                                                                         sharingAccount:[objectRequest.userInfo objectForKey:@"sharingAccount"]];
1823         newContainerRequest.delegate = self;
1824         newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1825         newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1826         newContainerRequest.userInfo = objectRequest.userInfo;
1827         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1828         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1829         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1830         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1831         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"];
1832         [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1833             [activityFacility updateActivity:activity 
1834                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
1835                                   totalBytes:activity.totalBytes 
1836                                 currentBytes:(activity.currentBytes + size)];
1837         }];
1838         [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1839     } else {
1840         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1841         [self requestFailed:objectRequest];
1842     }
1843     [pool drain];
1844 }
1845
1846 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1847     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1848     NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1849     NSLog(@"Upload of missing block finished: %@", containerRequest.url);
1850     NSUInteger blockSize = [[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1851     NSString *fileName = [containerRequest.userInfo objectForKey:@"fileName"];
1852     PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1853     if (operation.isCancelled) {
1854         [self requestFailed:containerRequest];
1855     } else if (containerRequest.responseStatusCode == 202) {
1856         NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1857         NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1858         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1859         if (missingBlockIndex == NSNotFound) {
1860             NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1861             ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1862                                                                                            containerName:[containerRequest.userInfo objectForKey:@"containerName"] 
1863                                                                                               objectName:[containerRequest.userInfo objectForKey:@"objectName"] 
1864                                                                                              contentType:[containerRequest.userInfo objectForKey:@"contentType"] 
1865                                                                                                blockSize:blockSize 
1866                                                                                                blockHash:[containerRequest.userInfo objectForKey:@"blockHash"]
1867                                                                                                  forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1868                                                                                            checkIfExists:NO 
1869                                                                                                   hashes:&hashes 
1870                                                                                           sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1871             newObjectRequest.delegate = self;
1872             newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1873             newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1874             newObjectRequest.userInfo = containerRequest.userInfo;
1875             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1876             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1877             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1878             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
1879             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1880         } else {
1881             __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
1882                                                                                                              containerName:[containerRequest.userInfo objectForKey:@"containerName"]
1883                                                                                                                  blockSize:[[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
1884                                                                                                                    forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1885                                                                                                          missingBlockIndex:missingBlockIndex 
1886                                                                                                             sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1887             newContainerRequest.delegate = self;
1888             newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1889             newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1890             newContainerRequest.userInfo = containerRequest.userInfo;
1891             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1892             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1893             [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1894                 [activityFacility updateActivity:activity 
1895                                      withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] 
1896                                       totalBytes:activity.totalBytes 
1897                                     currentBytes:(activity.currentBytes + size)];
1898             }];
1899             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1900         }
1901     } else {
1902         [(NSMutableDictionary *)(containerRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1903         [self requestFailed:containerRequest];
1904     }
1905     [pool drain];
1906 }
1907
1908 - (void)moveFinished:(ASIPithosObjectRequest *)objectRequest {
1909     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1910     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1911     NSLog(@"Move object finished: %@", objectRequest.url);
1912     if (operation.isCancelled) {
1913         [self requestFailed:objectRequest];
1914     } else if (objectRequest.responseStatusCode == 201) {
1915         dispatch_async(dispatch_get_main_queue(), ^{
1916             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1917                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1918         });
1919         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1920             [node forceRefresh];
1921         }
1922         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1923             [node refresh];
1924         }
1925         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1926             [self forceRefresh:self];
1927         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1928             [self refresh:self];
1929     } else {
1930         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1931         [self requestFailed:objectRequest];
1932     }
1933     [pool drain];
1934 }
1935
1936 - (void)copyFinished:(ASIPithosObjectRequest *)objectRequest {
1937     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1938     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1939     NSLog(@"Copy object finished: %@", objectRequest.url);
1940     if (operation.isCancelled) {
1941         [self requestFailed:objectRequest];
1942     } else if (objectRequest.responseStatusCode == 201) {
1943         dispatch_async(dispatch_get_main_queue(), ^{
1944             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1945                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1946         });
1947         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1948             [node forceRefresh];
1949         }
1950         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1951             [node refresh];
1952         }
1953         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1954             [self forceRefresh:self];
1955         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1956             [self refresh:self];
1957     } else {
1958         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1959         [self requestFailed:objectRequest];
1960     }
1961     [pool drain];
1962 }
1963
1964 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1965     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1966     NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1967     NSLog(@"Delete object finished: %@", objectRequest.url);
1968     if (operation.isCancelled) {
1969         [self requestFailed:objectRequest];
1970     } else if (objectRequest.responseStatusCode == 204) {
1971         dispatch_async(dispatch_get_main_queue(), ^{
1972             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1973                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1974         });
1975         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1976             [node forceRefresh];
1977         }
1978         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1979             [node refresh];
1980         }
1981         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1982             [self forceRefresh:self];
1983         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1984             [self refresh:self];
1985     } else {
1986         [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"];
1987         [self requestFailed:objectRequest];
1988     }
1989     [pool drain];
1990 }
1991
1992 #pragma mark -
1993 #pragma mark NSSplitViewDelegate
1994
1995 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
1996     if (splitView == verticalSplitView)
1997         return 120;
1998     else
1999         return ([horizontalSplitView bounds].size.height - 108);
2000 }
2001
2002 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
2003     if (splitView == verticalSplitView)
2004         return 220;
2005     else
2006         return ([horizontalSplitView bounds].size.height - 108);
2007 }
2008
2009 - (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex {
2010     if (splitView == verticalSplitView) {
2011         if (proposedPosition < 120)
2012             return 120;
2013         else if (proposedPosition > 220)
2014             return 220;
2015         else
2016             return proposedPosition;
2017     } else {
2018         return ([horizontalSplitView bounds].size.height - 108);
2019     }
2020 }
2021
2022 #pragma mark -
2023 #pragma mark NSOutlineViewDataSource
2024
2025 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
2026     if (item == nil)
2027         return 2;
2028     if (item == containersNode)
2029         return containersNodeChildren.count;
2030     if (item == sharedNode)
2031         return 2;
2032     return 0;
2033 }
2034
2035 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
2036     if (item == nil)
2037         return (!index ? containersNode : sharedNode);
2038     if (item == sharedNode)
2039         return (!index ? mySharedNode : othersSharedNode);
2040     return [containersNodeChildren objectAtIndex:index];
2041 }
2042
2043 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
2044     if ((item == containersNode) || (item == sharedNode))
2045         return YES;
2046     return NO;
2047 }
2048
2049 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
2050     PithosNode *node = (PithosNode *)item;
2051     return node;    
2052 }
2053
2054 #pragma mark Drag and Drop destination
2055
2056 - (NSDragOperation)outlineView:(NSOutlineView *)anOutlineView 
2057                   validateDrop:(id<NSDraggingInfo>)info 
2058                   proposedItem:(id)item 
2059             proposedChildIndex:(NSInteger)index {
2060     NSDragOperation result = NSDragOperationNone;
2061     if ((item == nil) || (index != NSOutlineViewDropOnItemIndex))
2062         return result;
2063     PithosNode *dropNode = (PithosNode *)item;
2064     if ([dropNode class] != [PithosContainerNode class])
2065         return result;
2066     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
2067         result = NSDragOperationCopy;
2068     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
2069         if ([info draggingSourceOperationMask] & NSDragOperationMove) {
2070             // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
2071             if (![dropNode isEqualTo:draggedParentNode])
2072                 result = NSDragOperationMove;
2073         } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
2074             // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
2075             result = NSDragOperationCopy;
2076         }
2077     }
2078    return result;
2079 }
2080
2081 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id<NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
2082     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
2083         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
2084         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
2085         if (item && (index == NSOutlineViewDropOnItemIndex) && (filenames != nil)) {
2086             PithosNode *node = (PithosNode *)item;
2087             NSLog(@"drag in node: %@", node.url);
2088             return [self uploadFiles:filenames toNode:node];
2089         }
2090     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
2091         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
2092         if (item && (index == NSOutlineViewDropOnItemIndex) && (draggedNodes != nil)) {
2093             PithosNode *node = (PithosNode *)item;
2094             NSLog(@"drag local node: %@", node.url);
2095             if ([info draggingSourceOperationMask] & NSDragOperationMove)
2096                 return [self moveNodes:draggedNodes toNode:node];
2097             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
2098                 return [self copyNodes:draggedNodes toNode:node];
2099         }
2100     }
2101     return NO;
2102 }
2103
2104 #pragma mark -
2105 #pragma mark NSOutlineViewDelegate
2106
2107 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
2108     if ((item == containersNode) || (item == sharedNode))
2109         return NO;
2110     return YES;
2111 }
2112
2113 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
2114     if ((item == containersNode) || (item == sharedNode))
2115         return YES;
2116     return NO;
2117 }
2118
2119 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
2120     PithosNode *node = (PithosNode *)[outlineView itemAtRow:[outlineView selectedRow]];
2121     if (node) {
2122         rootNode = node;
2123         [browser loadColumnZero];
2124         [self refresh:nil];
2125     }
2126 }
2127
2128 #pragma mark -
2129 #pragma mark NSMenuDelegate
2130
2131 - (void)menuNeedsUpdate:(NSMenu *)menu {
2132     [menu removeAllItems];
2133     NSMenuItem *menuItem;
2134     NSString *menuItemTitle;
2135     BOOL nodeContextMenu = NO;
2136     PithosNode *menuNode = nil;
2137     NSMutableArray *menuNodes;
2138     if (menu == browserMenu) {
2139         NSInteger column = [browser clickedColumn];
2140         NSInteger row = [browser clickedRow];
2141         if ((column == -1) || (row == -1)) {
2142             // General context menu
2143             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
2144             if ([menuNodesIndexPaths count] == 0) {
2145                 menuNode = [browser parentForItemsInColumn:0];
2146             } else if (([menuNodesIndexPaths count] != 1) || 
2147                        ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) {
2148                 menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)];
2149             } else {
2150                 menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]];
2151             }
2152         } else {
2153             // Node context menu
2154             NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row];
2155             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
2156             menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]];
2157             if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) {
2158                 for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) {
2159                     [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]];
2160                 }
2161             } else {
2162                 [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]];
2163             }
2164             nodeContextMenu = YES;
2165         }
2166     } else if (menu == outlineViewMenu) {
2167         NSInteger row = [outlineView clickedRow];
2168         if (row == -1)
2169             row = [outlineView selectedRow];
2170         if (row == -1)
2171             return;
2172         menuNode = [outlineView itemAtRow:row];
2173     }
2174
2175     if (!nodeContextMenu) {
2176         // General context menu
2177         if (([menuNode class] == [PithosAccountNode class]) || 
2178             ([menuNode class] == [PithosSharingAccountsNode class]) ||
2179             ([menuNode class] == [PithosEmptyNode class]))
2180             return;
2181         BOOL shared = menuNode.shared;
2182         BOOL sharingAccount = (menuNode.sharingAccount != nil);
2183         // New Folder
2184         if (!shared && !sharingAccount) {
2185             menuItem = [[[NSMenuItem alloc] initWithTitle:@"New Folder" action:@selector(menuNewFolder:) keyEquivalent:@""] autorelease];
2186             [menuItem setRepresentedObject:menuNode];
2187             [menu addItem:menuItem];
2188             [menu addItem:[NSMenuItem separatorItem]];
2189         }
2190         // Refresh
2191         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""] autorelease];
2192         [menu addItem:menuItem];
2193         [menu addItem:[NSMenuItem separatorItem]];
2194         // Get Info
2195         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
2196         [menuItem setRepresentedObject:[NSArray arrayWithObject:menuNode]];
2197         [menu addItem:menuItem];
2198         // Paste
2199         if (!shared && !sharingAccount) {
2200             if (clipboardNodes) {
2201                 NSUInteger clipboardNodesCount = [clipboardNodes count];
2202                 if (clipboardNodesCount == 0) {
2203                     self.clipboardNodes = nil;
2204                 } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
2205                     if (clipboardNodesCount == 1)
2206                         menuItemTitle = [NSString stringWithString:@"Paste Item"];
2207                     else
2208                         menuItemTitle = [NSString stringWithString:@"Paste Items"];
2209                     [menu addItem:[NSMenuItem separatorItem]];
2210                     menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
2211                     [menuItem setRepresentedObject:menuNode];
2212                     [menu addItem:menuItem];
2213                 }
2214             }
2215         }
2216     } else {
2217         // Node context menu
2218         NSUInteger menuNodesCount = [menuNodes count];
2219         PithosNode *firstMenuNode = (PithosNode *)[menuNodes objectAtIndex:0];
2220         BOOL shared = firstMenuNode.shared;
2221         BOOL sharingAccount = (firstMenuNode.sharingAccount != nil);
2222         // Move to Trash (pithos container only)
2223         // Delete
2224         if (!shared && !sharingAccount) {
2225             if ([rootNode class] == [PithosContainerNode class]) {
2226                 if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
2227                     menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(menuMoveToTrash:) keyEquivalent:@""] autorelease];
2228                     [menuItem setRepresentedObject:menuNodes];
2229                     [menu addItem:menuItem];
2230                 }
2231                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(menuDelete:) keyEquivalent:@""] autorelease];
2232                 [menuItem setRepresentedObject:menuNodes];
2233                 [menu addItem:menuItem];
2234                 [menu addItem:[NSMenuItem separatorItem]];
2235             }
2236         }
2237         // Refresh
2238         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""] autorelease];
2239         [menu addItem:menuItem];
2240         // Get Info
2241         if (!sharingAccount || ([firstMenuNode class] != [PithosAccountNode class])) {
2242             [menu addItem:[NSMenuItem separatorItem]];
2243             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
2244             [menuItem setRepresentedObject:menuNodes];
2245             [menu addItem:menuItem];
2246             
2247             if ((!shared && !sharingAccount) || ([firstMenuNode class] != [PithosContainerNode class]))
2248                 [menu addItem:[NSMenuItem separatorItem]];
2249         }
2250         // Cut
2251         if (!shared && !sharingAccount) {
2252             if (menuNodesCount == 1)
2253                 menuItemTitle = [NSString stringWithFormat:@"Cut \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
2254             else 
2255                 menuItemTitle = [NSString stringWithFormat:@"Cut %lu Items", menuNodesCount];
2256             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCut:) keyEquivalent:@""] autorelease];
2257             [menuItem setRepresentedObject:menuNodes];
2258             [menu addItem:menuItem];
2259         }
2260         // Copy
2261         if ((!shared && !sharingAccount) || 
2262             (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class]))) {
2263             if (menuNodesCount == 1)
2264                 menuItemTitle = [NSString stringWithFormat:@"Copy \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
2265             else 
2266                 menuItemTitle = [NSString stringWithFormat:@"Copy %lu Items", menuNodesCount];
2267             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCopy:) keyEquivalent:@""] autorelease];
2268             [menuItem setRepresentedObject:menuNodes];
2269             [menu addItem:menuItem];
2270         }
2271         // Paste
2272         if (!shared && !sharingAccount) {
2273             if (menuNodesCount == 1) {
2274                 PithosNode *menuNode = [menuNodes objectAtIndex:0];
2275                 if (([menuNode class] == [PithosSubdirNode class]) && 
2276                     (menuNode.pithosObject.subdir || ![menuNode.pithosObject.name hasSuffix:@"/"])) {
2277                     if (clipboardNodes) {
2278                         NSUInteger clipboardNodesCount = [clipboardNodes count];
2279                         if (clipboardNodesCount == 0) {
2280                             self.clipboardNodes = nil;
2281                         } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
2282                             if (clipboardNodesCount == 1)
2283                                 menuItemTitle = [NSString stringWithString:@"Paste Item"];
2284                             else
2285                                 menuItemTitle = [NSString stringWithString:@"Paste Items"];
2286                             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
2287                             [menuItem setRepresentedObject:menuNode];
2288                             [menu addItem:menuItem];
2289                         }
2290                     }
2291                 }
2292             }
2293         }
2294     }
2295 }
2296
2297 #pragma mark -
2298 #pragma mark Menu Actions
2299
2300 - (void)menuNewFolder:(NSMenuItem *)sender {
2301     PithosNode *node = (PithosNode *)[sender representedObject];
2302     if ([node class] == [PithosContainerNode class]) {
2303         // Operation: Create (upload) a new root application/directory object
2304         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2305             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2306             if (operation.isCancelled) {
2307                 [pool drain];
2308                 return;
2309             }
2310             NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2311                                                                   containerName:node.pithosContainer.name 
2312                                                                      subdirName:@"untitled folder"];
2313             NSString *fileName = [safeObjectName lastPathComponent];
2314             if (operation.isCancelled) {
2315                 [pool drain];
2316                 return;
2317             }
2318             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
2319                                                                                                containerName:node.pithosContainer.name 
2320                                                                                                   objectName:safeObjectName 
2321                                                                                                         eTag:nil 
2322                                                                                                  contentType:@"application/directory" 
2323                                                                                              contentEncoding:nil 
2324                                                                                           contentDisposition:nil 
2325                                                                                                     manifest:nil 
2326                                                                                                      sharing:nil 
2327                                                                                                     isPublic:ASIPithosObjectRequestPublicIgnore 
2328                                                                                                     metadata:nil 
2329                                                                                                         data:[NSData data]];
2330             objectRequest.delegate = self;
2331             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2332             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2333             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName];
2334             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
2335                                                                        message:messagePrefix];
2336             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2337                                       fileName, @"fileName", 
2338                                       [NSArray arrayWithObject:node], @"refreshNodes", 
2339                                       [NSNumber numberWithBool:YES], @"refresh", 
2340                                       activity, @"activity", 
2341                                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2342                                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2343                                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2344                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2345                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
2346                                       NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
2347                                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2348                                       uploadNetworkQueue, @"networkQueue", 
2349                                       @"upload", @"operationType", 
2350                                       nil];
2351             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2352             [pool drain];
2353         }];
2354         [uploadQueue addOperation:operation];
2355     } else if (([node class] == [PithosSubdirNode class]) && 
2356                (node.pithosObject.subdir || ![node.pithosObject.name hasSuffix:@"/"])) {
2357         // Operation: Create (upload) a new aplication/directory object
2358         __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2359             NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2360             if (operation.isCancelled) {
2361                 [pool drain];
2362                 return;
2363             }
2364             NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2365                                                                   containerName:node.pithosContainer.name 
2366                                                                      subdirName:[node.pithosObject.name stringByAppendingPathComponent:@"untitled folder"]];
2367             NSString *fileName = [safeObjectName lastPathComponent];
2368             if (operation.isCancelled) {
2369                 [pool drain];
2370                 return;
2371             }
2372             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
2373                                                                                                containerName:node.pithosContainer.name 
2374                                                                                                   objectName:safeObjectName 
2375                                                                                                         eTag:nil 
2376                                                                                                  contentType:@"application/directory" 
2377                                                                                              contentEncoding:nil 
2378                                                                                           contentDisposition:nil 
2379                                                                                                     manifest:nil 
2380                                                                                                      sharing:nil 
2381                                                                                                     isPublic:ASIPithosObjectRequestPublicIgnore 
2382                                                                                                     metadata:nil 
2383                                                                                                         data:[NSData data]];
2384             objectRequest.delegate = self;
2385             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2386             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2387             NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName];
2388             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
2389                                                                        message:messagePrefix];
2390             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2391                                       fileName, @"fileName", 
2392                                       [NSArray arrayWithObject:node], @"refreshNodes", 
2393                                       [NSNumber numberWithBool:YES], @"refresh", 
2394                                       activity, @"activity", 
2395                                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2396                                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2397                                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2398                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2399                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
2400                                       NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
2401                                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2402                                       uploadNetworkQueue, @"networkQueue", 
2403                                       @"upload", @"operationType", 
2404                                       nil];
2405             [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2406             [pool drain];
2407         }];
2408         [uploadQueue addOperation:operation];
2409     }
2410 }
2411
2412 - (void)menuGetInfo:(NSMenuItem *)sender {
2413     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2414         [node showPithosNodeInfo:sender];
2415     }
2416 }
2417
2418 - (void)menuDelete:(NSMenuItem *)sender {
2419     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2420         if (([node class] == [PithosObjectNode class]) || 
2421             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
2422             // Operation: Delete an object or subdir/ node
2423             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2424                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2425                 if (operation.isCancelled) {
2426                     [pool drain];
2427                     return;
2428                 }
2429                 NSString *fileName = [node.pithosObject.name lastPathComponent];
2430                 if ([node.pithosObject.name hasSuffix:@"/"])
2431                     fileName = [fileName stringByAppendingString:@"/"];
2432                 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos 
2433                                                                                                 containerName:node.pithosContainer.name 
2434                                                                                                    objectName:node.pithosObject.name];
2435                 if (operation.isCancelled) {
2436                     [pool drain];
2437                     return;
2438                 }
2439                 objectRequest.delegate = self;
2440                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2441                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2442                 NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", fileName];
2443                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
2444                                                                            message:messagePrefix];
2445                 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
2446                                           fileName, @"fileName", 
2447                                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2448                                           activity, @"activity", 
2449                                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2450                                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2451                                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2452                                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2453                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2454                                           NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
2455                                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2456                                           deleteNetworkQueue, @"networkQueue", 
2457                                           @"delete", @"operationType", 
2458                                           nil];
2459                 [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2460                 [pool drain];
2461             }];
2462             [deleteQueue addOperation:operation];
2463         } else if ([node class] == [PithosSubdirNode class]) {
2464             // Operation: Delete a subdir node and its descendants
2465             // The resulting ASIPithosObjectRequests are chained through dependencies
2466             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2467                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2468                 if (operation.isCancelled) {
2469                     [pool drain];
2470                     return;
2471                 }
2472                 NSArray *objectRequests = [PithosUtilities deleteObjectRequestsForSubdirWithPithos:pithos 
2473                                                                                      containerName:node.pithosContainer.name 
2474                                                                                         objectName:node.pithosObject.name];
2475                 if (!operation.isCancelled && objectRequests) {
2476                     ASIPithosObjectRequest *previousObjectRequest = nil;
2477                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
2478                         if (operation.isCancelled) {
2479                             [pool drain];
2480                             return;
2481                         }
2482                         objectRequest.delegate = self;
2483                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2484                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2485                         NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", [objectRequest.userInfo objectForKey:@"fileName"]];
2486                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
2487                                                                                    message:messagePrefix];
2488                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2489                          [NSDictionary dictionaryWithObjectsAndKeys:
2490                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2491                           activity, @"activity", 
2492                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2493                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2494                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2495                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2496                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2497                           NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
2498                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2499                           deleteNetworkQueue, @"networkQueue", 
2500                           @"delete", @"operationType", 
2501                           nil]];
2502                         if (previousObjectRequest)
2503                             [objectRequest addDependency:previousObjectRequest];
2504                         previousObjectRequest = objectRequest;
2505                         [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2506                     }
2507                 }
2508                 [pool drain];
2509             }];
2510             [deleteQueue addOperation:operation];
2511         }
2512     }
2513 }
2514
2515 - (void)menuMoveToTrash:(NSMenuItem *)sender {
2516     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
2517         if (([node class] == [PithosObjectNode class]) || 
2518             (([node class] == [PithosSubdirNode class]) && 
2519              !node.pithosObject.subdir &&
2520              [node.pithosObject.name hasSuffix:@"/"])) {
2521             // Operation: Move to trash an object or subdir/ node
2522             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2523                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2524                 if (operation.isCancelled) {
2525                     [pool drain];
2526                     return;
2527                 }
2528                 NSString *safeObjectName = [PithosUtilities safeObjectNameForPithos:pithos 
2529                                                                       containerName:@"trash" 
2530                                                                          objectName:node.pithosObject.name];
2531                 if (!operation.isCancelled && safeObjectName) {
2532                     ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
2533                                                                                            containerName:node.pithosContainer.name 
2534                                                                                               objectName:node.pithosObject.name 
2535                                                                                 destinationContainerName:@"trash" 
2536                                                                                    destinationObjectName:safeObjectName 
2537                                                                                            checkIfExists:NO];
2538                     if (!operation.isCancelled && objectRequest) {
2539                         objectRequest.delegate = self;
2540                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2541                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2542                         NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
2543                                                    [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2544                                                    [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2545                                                    [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2546                                                    [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
2547                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
2548                                                                                    message:messagePrefix];
2549                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2550                          [NSDictionary dictionaryWithObjectsAndKeys:
2551                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2552                           activity, @"activity", 
2553                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2554                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2555                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2556                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2557                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
2558                           NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
2559                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2560                           moveNetworkQueue, @"networkQueue", 
2561                           @"move", @"operationType", 
2562                           nil]];
2563                         [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2564                     }
2565                 }
2566                 [pool drain];
2567             }];
2568             [moveQueue addOperation:operation];
2569         } else if ([node class] == [PithosSubdirNode class]) {
2570             // Operation: Move to trash a subdir node and its descendants
2571             // The resulting ASIPithosObjectRequests are chained through dependencies
2572             __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
2573                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2574                 if (operation.isCancelled) {
2575                     [pool drain];
2576                     return;
2577                 }
2578                 NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos 
2579                                                                       containerName:@"trash" 
2580                                                                          subdirName:node.pithosObject.name];
2581                 if (!operation.isCancelled && safeObjectName) {
2582                     NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos 
2583                                                                                        containerName:node.pithosContainer.name 
2584                                                                                           objectName:node.pithosObject.name 
2585                                                                             destinationContainerName:@"trash" 
2586                                                                                destinationObjectName:safeObjectName 
2587                                                                                        checkIfExists:NO];
2588                     if (!operation.isCancelled && objectRequests) {
2589                         ASIPithosObjectRequest *previousObjectRequest = nil;
2590                         for (ASIPithosObjectRequest *objectRequest in objectRequests) {
2591                             if (operation.isCancelled) {
2592                                 [pool drain];
2593                                 return;
2594                             }
2595                             objectRequest.delegate = self;
2596                             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2597                             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2598                             NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
2599                                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2600                                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2601                                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2602                                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]];
2603                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
2604                                                                                        message:messagePrefix];
2605                             [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2606                              [NSDictionary dictionaryWithObjectsAndKeys:
2607                               [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2608                               activity, @"activity", 
2609                               [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
2610                               [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
2611                               [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
2612                               [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
2613                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
2614                               NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", 
2615                               NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
2616                               moveNetworkQueue, @"networkQueue", 
2617                               @"move", @"operationType", 
2618                               nil]];
2619                             if (previousObjectRequest)
2620                                 [objectRequest addDependency:previousObjectRequest];
2621                             previousObjectRequest = objectRequest;
2622                             [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
2623                         }
2624                     }
2625                 }
2626                 [pool drain];
2627             }];
2628             [moveQueue addOperation:operation];
2629         }
2630     }
2631 }
2632
2633 - (void)menuCut:(NSMenuItem *)sender {
2634     self.clipboardNodes = (NSArray *)[sender representedObject];
2635     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2636     self.clipboardCopy = NO;
2637 }
2638
2639 - (void)menuCopy:(NSMenuItem *)sender {
2640     self.clipboardNodes = (NSArray *)[sender representedObject];
2641     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2642     self.clipboardCopy = YES;
2643 }
2644
2645 - (void)menuPaste:(NSMenuItem *)sender {
2646     if (!clipboardNodes || ![clipboardNodes count])
2647         return;
2648     PithosNode *dropNode = (PithosNode *)[sender representedObject];
2649     NSArray *localClipboardNodes = [NSArray arrayWithArray:clipboardNodes];
2650     if (!clipboardCopy && ![dropNode isEqualTo:clipboardParentNode]) {
2651         self.clipboardNodes = nil;
2652         self.clipboardParentNode = nil;
2653         [self moveNodes:localClipboardNodes toNode:dropNode];
2654     } else {
2655         [self copyNodes:localClipboardNodes toNode:dropNode];
2656     }
2657 }
2658     
2659 #pragma mark -
2660 #pragma mark PithosActivityFacilityDelegate
2661
2662 - (void)activityUpdate:(NSDictionary *)info {
2663     NSString *message = [info objectForKey:@"message"];
2664     NSUInteger runningActivitiesCount = [[info objectForKey:@"runningActivitiesCount"] unsignedIntegerValue];
2665 //    NSUInteger endingActivitiesCount = [[info objectForKey:@"endingActivitiesCount"] unsignedIntegerValue];
2666     NSUInteger totalUploadBytes = [[info objectForKey:@"totalUploadBytes"] unsignedIntegerValue];
2667     NSUInteger currentUploadBytes = [[info objectForKey:@"currentUploadBytes"] unsignedIntegerValue];
2668     NSUInteger totalDownloadBytes = [[info objectForKey:@"totalDownloadBytes"] unsignedIntegerValue];
2669     NSUInteger currentDownloadBytes = [[info objectForKey:@"currentDownloadBytes"] unsignedIntegerValue];
2670     NSUInteger totalBytes = totalUploadBytes + totalDownloadBytes;
2671     if (runningActivitiesCount && totalBytes) {
2672         [activityProgressIndicator setDoubleValue:((currentUploadBytes + currentDownloadBytes + 0.0)/(totalBytes + 0.0))];
2673         [activityProgressIndicator startAnimation:self];
2674     } else {
2675         [activityProgressIndicator setDoubleValue:1.0];
2676         [activityProgressIndicator stopAnimation:self];
2677     }
2678     
2679     if (!message)
2680         message = [[[[UsingSizeTransformer alloc] init] autorelease] transformedValue:accountNode.pithosAccount];
2681     [activityTextField setStringValue:message];
2682 }
2683
2684 @end