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