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