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