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