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