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