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