0b42490c39e7927df12c1ee2088999735becc9da
[pithos-macos] / pithos-macos / PithosBrowserController.m
1 //
2 //  PithosBrowserController.m
3 //  pithos-macos
4 //
5 // Copyright 2011 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 "ASIPithosRequest.h"
49 #import "ASIPithosContainerRequest.h"
50 #import "ASIPithosObjectRequest.h"
51 #import "ASIPithosAccount.h"
52 #import "ASIPithosContainer.h"
53 #import "ASIPithosObject.h"
54 #import "PithosUtilities.h"
55 #import "BytesSizeTransformer.h"
56
57 @interface PithosBrowserCell : FileSystemBrowserCell {}
58 @end
59
60 @implementation PithosBrowserCell
61
62 - (id)init {
63     if ((self = [super init])) {
64         [self setLineBreakMode:NSLineBreakByTruncatingMiddle];
65         [self setEditable:YES];
66     }
67     return self;
68 }
69
70 - (void)setObjectValue:(id)object {
71     if ([object isKindOfClass:[PithosNode class]]) {
72         PithosNode *node = (PithosNode *)object;
73         [self setStringValue:node.displayName];
74         [self setImage:node.icon];
75     } else {
76         [super setObjectValue:object];
77     }
78 }
79
80 @end
81
82 @interface PithosOutlineViewCell : ImageAndTextCell {}
83 @end
84
85 @implementation PithosOutlineViewCell
86
87 - (void)setObjectValue:(id)object {
88     if ([object isKindOfClass:[PithosNode class]]) {
89         PithosNode *node = (PithosNode *)object;
90         [self setStringValue:node.displayName];
91         [self setImage:node.icon];
92         [self setEditable:NO];
93     } else {
94         [super setObjectValue:object];
95     }
96 }
97
98 @end
99
100 @interface PithosBrowserController (Private) {}
101 - (void)resetContainers:(NSNotification *)notification;
102 - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode;
103 - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode;
104 - (BOOL)copyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode;
105 @end
106
107 @implementation PithosBrowserController
108 @synthesize accountNode;
109 @synthesize verticalSplitView, horizontalSplitView, leftTopView, leftBottomView, outlineView, browser, outlineViewMenu, browserMenu;
110 @synthesize draggedNodes, draggedParentNode;
111 @synthesize clipboardNodes, clipboardParentNode, clipboardCopy;
112 @synthesize activityTextField, activityProgressIndicator;
113
114 #pragma mark -
115 #pragma Object Lifecycle
116
117 - (id)init {
118     return [super initWithWindowNibName:@"PithosBrowserController"];
119 }
120
121 - (void)dealloc {
122     [[NSNotificationCenter defaultCenter] removeObserver:self];
123     [clipboardParentNode release];
124     [clipboardNodes release];
125     [draggedParentNode release];
126     [draggedNodes release];
127     [sharedPreviewController release];
128     [othersSharedNode release];
129     [mySharedNode release];
130     [sharedNode release];
131     [containersNodeChildren release];
132     [containersNode release];
133     [accountNode release];
134     [rootNode release];
135     [super dealloc];
136 }
137
138 - (void)awakeFromNib {
139     [super awakeFromNib];
140     
141     [browser registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]];
142     [browser setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES];
143     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
144     
145     [outlineView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]];
146     [outlineView setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES];
147     [outlineView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
148     
149     [browser setCellClass:[PithosBrowserCell class]];
150 }
151
152 - (void)resetContainers:(NSNotification *)notification {
153     rootNode = nil;
154     [browser loadColumnZero];
155     [containersNodeChildren removeAllObjects];
156     [outlineView reloadData];
157         // Expand the folder outline view
158     [outlineView expandItem:nil expandChildren:YES];
159         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
160     
161     // Refresh account
162     [accountNode refresh];
163     [mySharedNode refresh];
164     [othersSharedNode refresh];
165     
166     [activityFacility reset];
167 }
168
169 - (void)windowDidLoad {
170     [super windowDidLoad];
171     
172     [activityProgressIndicator setUsesThreadedAnimation:YES];
173     [activityProgressIndicator setMinValue:0.0];
174     [activityProgressIndicator setMaxValue:1.0];
175     activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
176     activityFacility.delegate = self;
177     
178     accountNode = [[PithosAccountNode alloc] init];
179     containersNode = [[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil];
180     containersNodeChildren = [[NSMutableArray alloc] init];
181     sharedNode = [[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil];
182     mySharedNode = [[PithosAccountNode alloc] init];
183     mySharedNode.displayName = @"my shared";
184     mySharedNode.shared = YES;
185     mySharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)];
186     othersSharedNode = [[PithosSharingAccountsNode alloc] init];
187     othersSharedNode.displayName = @"others shared";
188     othersSharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)];
189     
190     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
191     
192     // Register for updates
193     // PithosContainerNode updates browser nodes
194     [[NSNotificationCenter defaultCenter] addObserver:self 
195                                              selector:@selector(pithosNodeChildrenUpdated:) 
196                                                  name:@"PithosContainerNodeChildrenUpdated" 
197                                                object:nil];
198     // PithosSubdirNode updates browser nodes
199     [[NSNotificationCenter defaultCenter] addObserver:self 
200                                              selector:@selector(pithosNodeChildrenUpdated:) 
201                                                  name:@"PithosSubdirNodeChildrenUpdated" 
202                                                object:nil];
203     // PithosAccountNode accountNode updates outlineView container nodes 
204     [[NSNotificationCenter defaultCenter] addObserver:self 
205                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
206                                                  name:@"PithosAccountNodeChildrenUpdated" 
207                                                object:accountNode];
208     // PithosAccountNode other than accountNode updates nodes 
209     [[NSNotificationCenter defaultCenter] addObserver:self 
210                                              selector:@selector(pithosNodeChildrenUpdated:) 
211                                                  name:@"PithosAccountNodeChildrenUpdated" 
212                                                object:nil];
213     // PithosSharingAccountsNode othersSharedNode updates browser nodes 
214     [[NSNotificationCenter defaultCenter] addObserver:self 
215                                              selector:@selector(pithosNodeChildrenUpdated:) 
216                                                  name:@"PithosSharingAccountsNodeChildrenUpdated" 
217                                                object:othersSharedNode];
218     // Updated authentication credentials reset containers in the outline view 
219     [[NSNotificationCenter defaultCenter] addObserver:self 
220                                              selector:@selector(resetContainers:) 
221                                                  name:@"PithosAuthenticationCredentialsUpdated" 
222                                                object:nil];
223     // Request for browser refresh 
224     [[NSNotificationCenter defaultCenter] addObserver:self 
225                                              selector:@selector(pithosBrowserRefreshNeeded:) 
226                                                  name:@"PithosBrowserRefreshNeeeded" 
227                                                object:nil];    
228 }
229
230 #pragma mark -
231 #pragma mark Observers
232
233 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
234     PithosNode *node = (PithosNode *)[notification object];
235     if (node == accountNode)
236         return;
237     NSLog(@"pithosNodeChildrenUpdated:%@", node.url);
238     NSInteger lastColumn = [browser lastColumn];
239     for (NSInteger column = lastColumn; column >= 0; column--) {
240         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
241             [browser reloadColumn:column];
242             return;
243         }
244     }
245 }
246
247 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
248     BOOL containerPithosFound = NO;
249     BOOL containerTrashFound = NO;
250     NSMutableIndexSet *removedContainersNodeChildren = [NSMutableIndexSet indexSet];
251     for (NSUInteger i = 0 ; i < [containersNodeChildren count] ; i++) {
252         if (![accountNode.children containsObject:[containersNodeChildren objectAtIndex:i]])
253             [removedContainersNodeChildren addIndex:i];
254     }
255     [containersNodeChildren removeObjectsAtIndexes:removedContainersNodeChildren];
256     for (PithosContainerNode *containerNode in accountNode.children) {
257         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
258             if (![containersNodeChildren containsObject:containerNode])
259                 [containersNodeChildren insertObject:containerNode atIndex:0];
260             containerPithosFound = YES;
261         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
262             NSUInteger insertIndex = 1;
263             if (!containerPithosFound)
264                 insertIndex = 0;
265             if (![containersNodeChildren containsObject:containerNode])
266                 [containersNodeChildren insertObject:containerNode atIndex:insertIndex];
267             containerTrashFound = YES;
268         } else if (![containersNodeChildren containsObject:containerNode]) {
269             [containersNodeChildren addObject:containerNode];
270         }
271     }
272     BOOL refreshAccountNode = NO;
273     if (!containerPithosFound) {
274         // Create pithos node
275         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
276         [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
277         while (![containerRequest isFinished]) {
278             sleep(1);
279         }
280         if ([containerRequest error]) {
281             [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
282         } else {
283             refreshAccountNode = YES;
284         }
285     }
286     if (!containerTrashFound) {
287         // Create trash node
288         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
289         [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
290         while (![containerRequest isFinished]) {
291             sleep(1);
292         }
293         if ([containerRequest error]) {
294             [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
295         } else {
296             refreshAccountNode = YES;
297         }
298     }
299     
300     if (refreshAccountNode)
301         [accountNode refresh];
302     
303     [outlineView reloadData];
304     
305     // Expand the folder outline view
306     [outlineView expandItem:nil expandChildren:YES];
307     
308     if ((rootNode == nil) || (rootNode == containersNode) || (rootNode == sharedNode)) {
309         rootNode = [containersNodeChildren objectAtIndex:0];
310         [browser loadColumnZero];
311     }
312     
313     if (notification)
314         [self refresh:nil];
315 }
316
317 - (void)pithosBrowserRefreshNeeded:(NSNotification *)notification {
318     [self refresh:nil];
319 }
320
321 #pragma mark -
322 #pragma mark Actions
323
324 - (IBAction)forceRefresh:(id)sender {
325     if (sender)
326         [accountNode forceRefresh];
327     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
328         PithosNode *node = (PithosNode *)[browser parentForItemsInColumn:column];
329         node.forcedRefresh = YES;
330         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];                      
331     }
332     [browser validateVisibleColumns];
333 }
334
335 - (IBAction)refresh:(id)sender {
336     if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) {
337         [self forceRefresh:sender];
338     } else {
339         if (sender)
340             [accountNode refresh];
341         for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
342             [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
343         }
344         [browser validateVisibleColumns];
345     }
346 }
347
348 #pragma mark -
349 #pragma mark NSBrowserDelegate
350
351 - (id)rootItemForBrowser:(NSBrowser *)browser {
352     return rootNode;    
353 }
354
355 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
356     PithosNode *node = (PithosNode *)item;
357     return node.children.count;
358 }
359
360 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
361     PithosNode *node = (PithosNode *)item;
362     return [node.children objectAtIndex:index];
363 }
364
365 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
366     PithosNode *node = (PithosNode *)item;
367     return node.isLeafItem;
368 }
369
370 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
371     PithosNode *node = (PithosNode *)item;
372     return node;
373 }
374
375 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
376     if (sharedPreviewController == nil)
377         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
378     return sharedPreviewController;
379 }
380
381 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
382 //    if (!forUserResize) {
383 //        id item = [browser parentForItemsInColumn:columnIndex]; 
384 //        if ([self browser:browser isLeafItem:item]) {
385 //            suggestedWidth = 200; 
386 //        }
387 //    }
388 //    return suggestedWidth;
389 //}
390
391 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
392     return NO;
393 }
394
395 #pragma mark Editing
396
397 - (BOOL)browser:(NSBrowser *)browser shouldEditItem:(id)item {
398     PithosNode *node = (PithosNode *)item;
399     if (node.shared || node.sharingAccount || 
400         ([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class]))
401         return NO;
402     return YES;
403 }
404
405 - (void)browser:(NSBrowser *)browser setObjectValue:(id)object forItem:(id)item {
406     PithosNode *node = (PithosNode *)item;
407     NSString *newName = (NSString *)object;
408     NSUInteger newNameLength = [newName length];
409     NSRange firstSlashRange = [newName rangeOfString:@"/"];
410     if ((newNameLength == 0) || 
411         ((firstSlashRange.length == 1) && (firstSlashRange.location != (newNameLength - 1))) || 
412         ([newName isEqualToString:node.displayName])) {
413         return;
414     }
415     if (([node class] == [PithosObjectNode class]) || 
416         (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
417         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
418         dispatch_async(queue, ^{
419             NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName];
420             if ([newName hasSuffix:@"/"])
421                 destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
422             NSError *error = nil;
423             BOOL isDirectory;
424             if ([PithosUtilities objectExistsAtContainerName:node.pithosContainer.name 
425                                                   objectName:destinationObjectName 
426                                                        error:&error 
427                                                  isDirectory:&isDirectory 
428                                               sharingAccount:nil]) {
429                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
430                 [alert setMessageText:@"Name Taken"];
431                 [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]];
432                 [alert addButtonWithTitle:@"OK"];
433                 [alert runModal];
434                 return;
435             } else if (error) {
436                 return;
437             }
438             ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:node.pithosContainer.name 
439                                                                                              objectName:node.pithosObject.name 
440                                                                                destinationContainerName:node.pithosContainer.name 
441                                                                                   destinationObjectName:destinationObjectName 
442                                                                                           checkIfExists:NO];
443             if (objectRequest) {
444                 objectRequest.delegate = self;
445                 objectRequest.didFinishSelector = @selector(moveFinished:);
446                 objectRequest.didFailSelector = @selector(moveFailed:);
447                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
448                                                                            message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
449                                                                                     [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
450                                                                                     [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
451                                                                                     [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
452                                                                                     [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
453                 [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
454                  [NSDictionary dictionaryWithObjectsAndKeys:
455                   [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
456                   [NSNumber numberWithBool:YES], @"refresh", 
457                   activity, @"activity", 
458                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
459                   nil]];
460                 [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
461             }
462         });
463     } else if ([node class] == [PithosSubdirNode class]) {
464         if (firstSlashRange.length == 1)
465             return;
466         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
467         dispatch_async(queue, ^{
468             NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName];
469             NSError *error = nil;
470             BOOL isDirectory;
471             if ([PithosUtilities objectExistsAtContainerName:node.pithosContainer.name 
472                                                   objectName:destinationObjectName 
473                                                        error:&error 
474                                                  isDirectory:&isDirectory 
475                                               sharingAccount:nil]) {
476                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
477                 [alert setMessageText:@"Name Taken"];
478                 [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]];
479                 [alert addButtonWithTitle:@"OK"];
480                 [alert runModal];
481                 return;
482             } else if (error) {
483                 return;
484             }
485             if (node.pithosObject.subdir)
486                 destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
487             NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
488                                                                                          objectName:node.pithosObject.name 
489                                                                            destinationContainerName:node.pithosContainer.name 
490                                                                               destinationObjectName:destinationObjectName 
491                                                                                       checkIfExists:NO];
492             if (objectRequests) {
493                 for (ASIPithosObjectRequest *objectRequest in objectRequests) {
494                     objectRequest.delegate = self;
495                     objectRequest.didFinishSelector = @selector(moveFinished:);
496                     objectRequest.didFailSelector = @selector(moveFailed:);
497                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
498                                                                                message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
499                                                                                         [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
500                                                                                         [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
501                                                                                         [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
502                                                                                         [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
503                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
504                      [NSDictionary dictionaryWithObjectsAndKeys:
505                       [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
506                       [NSNumber numberWithBool:YES], @"refresh", 
507                       activity, @"activity", 
508                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
509                       nil]];
510                     [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
511                 }
512             }
513         });
514     }
515 }
516
517 #pragma mark Drag and Drop source
518
519 - (BOOL)browser:(NSBrowser *)aBrowser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
520       withEvent:(NSEvent *)event {
521     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column];
522     __block BOOL result = YES;
523     [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
524         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]];
525         if (([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class])) {
526             result = NO;
527             *stop = YES;
528         }
529     }];
530     return result;
531 }
532
533 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
534    toPasteboard:(NSPasteboard *)pasteboard {
535     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
536     NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
537     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column];
538     [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){
539         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]];
540         [propertyList addObject:[node.pithosObject.name pathExtension]];
541         [nodes addObject:node];
542     }];
543
544     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
545     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
546     self.draggedNodes = nodes;
547     self.draggedParentNode = [browser parentForItemsInColumn:column];
548     return YES;
549 }
550
551 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
552 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
553     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[draggedNodes count]];
554     for (PithosNode *node in draggedNodes) {        
555         // If the node is a subdir ask if the whole tree should be downloaded
556         if ([node class] == [PithosSubdirNode class]) {
557             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
558             [alert setMessageText:@"Download directory"];
559             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
560             [alert addButtonWithTitle:@"OK"];
561             [alert addButtonWithTitle:@"Cancel"];
562             NSInteger choice = [alert runModal];
563             if (choice == NSAlertFirstButtonReturn) {
564                 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
565                 dispatch_async(queue, ^{
566                     NSArray *objectRequests = [PithosUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
567                                                                                                      objectName:node.pithosObject.name 
568                                                                                                     toDirectory:[dropDestination path] 
569                                                                                                   checkIfExists:YES 
570                                                                                                  sharingAccount:node.sharingAccount];
571                     if (objectRequests) {
572                         for (__block ASIPithosObjectRequest *objectRequest in objectRequests) {
573                             [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
574                             objectRequest.delegate = self;
575                             objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
576                             objectRequest.didFailSelector = @selector(downloadObjectFailed:);
577                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
578                                                                                        message:[NSString stringWithFormat:@"Downloading '%@' (0%%)", [objectRequest.userInfo objectForKey:@"fileName"]] 
579                                                                                     totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] 
580                                                                                   currentBytes:0];
581                             [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
582                              [NSDictionary dictionaryWithObjectsAndKeys:
583                               activity, @"activity", 
584                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
585                               nil]];
586                             [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
587                                 [activityFacility updateActivity:activity 
588                                                      withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", [objectRequest.userInfo valueForKey:@"fileName"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
589                                                       totalBytes:activity.totalBytes 
590                                                     currentBytes:(activity.currentBytes + size)];
591                             }];
592                             [[PithosUtilities prepareRequest:objectRequest] startAsynchronous];
593                         }
594                     }
595                 });
596             }
597         } else {
598             __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
599                                                                                                      objectName:node.pithosObject.name 
600                                                                                                     toDirectory:[dropDestination path] 
601                                                                                                   checkIfExists:YES 
602                                                                                                  sharingAccount:node.sharingAccount];
603             if (objectRequest) {
604                 [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
605                 objectRequest.delegate = self;
606                 objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
607                 objectRequest.didFailSelector = @selector(downloadObjectFailed:);
608                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
609                                                                            message:[NSString stringWithFormat:@"Downloading '%@' (0%%)", [objectRequest.userInfo valueForKey:@"fileName"]]
610                                                                         totalBytes:node.pithosObject.bytes 
611                                                                       currentBytes:0];
612                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
613                  [NSDictionary dictionaryWithObjectsAndKeys:
614                   activity, @"activity", 
615                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
616                   nil]];
617                 [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
618                     [activityFacility updateActivity:activity 
619                                          withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", [objectRequest.userInfo valueForKey:@"fileName"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
620                                           totalBytes:activity.totalBytes 
621                                         currentBytes:(activity.currentBytes + size)];
622                 }];
623                 [[PithosUtilities prepareRequest:objectRequest] startAsynchronous];
624             }
625         }
626     }
627     return names;
628 }
629
630 #pragma mark Drag and Drop destination
631
632 - (NSDragOperation)browser:aBrowser 
633               validateDrop:(id<NSDraggingInfo>)info 
634                proposedRow:(NSInteger *)row 
635                     column:(NSInteger *)column 
636              dropOperation:(NSBrowserDropOperation *)dropOperation {
637     NSDragOperation result = NSDragOperationNone;
638     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
639         // For a drop above, the drop is redirected to the parent item
640         if (*dropOperation == NSBrowserDropAbove)
641             *row = -1;
642         // Only allow dropping in folders
643         if (*column != -1) {
644             PithosNode *dropNode;
645             if (*row != -1) {
646                 // Check if the node is not a folder and if so redirect to the parent item
647                 dropNode = [browser itemAtRow:*row inColumn:*column];
648                 if ([dropNode class] == [PithosObjectNode class])
649                     *row = -1;
650             }
651             if (*row == -1)
652                 dropNode = [browser parentForItemsInColumn:*column];
653             
654             if (!dropNode.shared && 
655                 (!dropNode.sharingAccount || 
656                  ([dropNode class] == [PithosSubdirNode class]) || 
657                  ([dropNode class] == [PithosContainerNode class]))) {
658                 *dropOperation = NSBrowserDropOn;
659                 result = NSDragOperationCopy;
660             }
661         }
662     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
663         // For a drop above, the drop is redirected to the parent item
664         if (*dropOperation == NSBrowserDropAbove) 
665             *row = -1;
666         // Only allow dropping in folders
667         if (*column != -1) {
668             PithosNode *dropNode;
669             if (*row != -1) {
670                 // Check if the node is not a folder and if so redirect to the parent item
671                 dropNode = [browser itemAtRow:*row inColumn:*column];
672                 if ([dropNode class] == [PithosObjectNode class])
673                     *row = -1;
674             }
675             if (*row == -1)
676                 dropNode = [browser parentForItemsInColumn:*column];
677             
678             if (!dropNode.shared && !dropNode.sharingAccount) {
679                 if ([info draggingSourceOperationMask] & NSDragOperationMove) {
680                     // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
681                     if ((([dropNode class] == [PithosContainerNode class]) || 
682                          dropNode.pithosObject.subdir || 
683                          ![dropNode.pithosObject.name hasSuffix:@"/"]) && 
684                         ![dropNode isEqualTo:draggedParentNode]) { 
685     //                    ![dropNode isEqualTo:draggedParentNode] && 
686     //                    ![draggedNodes containsObject:dropNode]) {                
687                         result = NSDragOperationMove;
688                     }
689                 } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
690                     // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
691                     if (([dropNode class] == [PithosContainerNode class]) || 
692                         dropNode.pithosObject.subdir || 
693                         ![dropNode.pithosObject.name hasSuffix:@"/"]) { 
694                         result = NSDragOperationCopy;
695                     }
696                 }
697             }
698         }
699     }
700     return result;
701 }
702
703 - (BOOL)browser:(NSBrowser *)aBrowser 
704      acceptDrop:(id<NSDraggingInfo>)info 
705           atRow:(NSInteger)row 
706          column:(NSInteger)column 
707   dropOperation:(NSBrowserDropOperation)dropOperation {
708     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
709         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
710         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
711         if ((column != -1) && (filenames != nil)) {
712             PithosNode *node;
713             if (row != -1)
714                 node = [browser itemAtRow:row inColumn:column];
715             else
716                 node = [browser parentForItemsInColumn:column];
717             NSLog(@"drag in node: %@", node.url);
718             return [self uploadFiles:filenames toNode:node];
719         }
720     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
721         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
722         if ((column != -1) && (draggedNodes != nil)) {
723             PithosNode *node;
724             if (row != -1)
725                 node = [browser itemAtRow:row inColumn:column];
726             else
727                 node = [browser parentForItemsInColumn:column];
728             NSLog(@"drag local node: %@", node.url);
729             if ([info draggingSourceOperationMask] & NSDragOperationMove)
730                 return [self moveNodes:draggedNodes toNode:node];
731             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
732                 return [self copyNodes:draggedNodes toNode:node];
733         }
734     }
735     return NO;
736 }
737
738 #pragma mark -
739 #pragma mark Drag and Drop methods
740
741 - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode {
742     if (([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class]))
743         return NO;
744     NSFileManager *defaultManager = [NSFileManager defaultManager];
745     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
746     NSString *objectNamePrefix;
747     if ([destinationNode class] == [PithosSubdirNode class])
748         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
749     else
750         objectNamePrefix = [NSString string];
751     if ((destinationNode.pithosContainer.blockHash == nil) || (destinationNode.pithosContainer.blockSize == 0)) {
752         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest containerMetadataRequestWithContainerName:containerName];
753         [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
754         while (![containerRequest isFinished]) {
755             sleep(1);
756         }
757         if ([containerRequest error]) {
758             [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
759             return NO;
760         } else if (containerRequest.responseStatusCode != 200) {
761             [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest];
762             return NO;
763         }
764         destinationNode.pithosContainer.blockHash = [containerRequest blockHash];
765         destinationNode.pithosContainer.blockSize = [containerRequest blockSize];
766     }    
767     NSUInteger blockSize = destinationNode.pithosContainer.blockSize;
768     NSString *blockHash = destinationNode.pithosContainer.blockHash;
769     
770     for (NSString *filePath in filenames) {
771         BOOL isDirectory;
772         if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
773             if (!isDirectory) {
774                 // Upload file
775                 NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
776                 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
777                 dispatch_async(queue, ^{
778                     NSError *error = nil;
779                     NSString *contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
780                     if (contentType == nil)
781                         contentType = @"application/octet-stream";
782                     if (error)
783                         NSLog(@"contentType detection error: %@", error);
784                     NSArray *hashes = nil;
785                     ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName 
786                                                                                                           objectName:objectName 
787                                                                                                          contentType:contentType 
788                                                                                                            blockSize:blockSize 
789                                                                                                            blockHash:blockHash 
790                                                                                                              forFile:filePath 
791                                                                                                        checkIfExists:YES 
792                                                                                                               hashes:&hashes 
793                                                                                                       sharingAccount:destinationNode.sharingAccount];
794                     if (objectRequest) {
795                         objectRequest.delegate = self;
796                         objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
797                         objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
798                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
799                                                                                    message:[NSString stringWithFormat:@"Uploading '%@' (0%%)", [objectRequest.userInfo valueForKey:@"fileName"]]
800                                                                                 totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue]
801                                                                               currentBytes:0];
802                         [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
803                          [NSDictionary dictionaryWithObjectsAndKeys:
804                           containerName, @"containerName", 
805                           objectName, @"objectName", 
806                           contentType, @"contentType", 
807                           [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
808                           blockHash, @"blockHash", 
809                           filePath, @"filePath", 
810                           hashes, @"hashes", 
811                           [NSArray arrayWithObject:destinationNode], @"refreshNodes", 
812                           [NSNumber numberWithBool:YES], @"refresh", 
813                           [NSNumber numberWithUnsignedInteger:10], @"iteration", 
814                           activity, @"activity", 
815                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
816                           nil]];
817                         if (destinationNode.sharingAccount)
818                             [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
819                         [[PithosUtilities prepareRequest:objectRequest] startAsynchronous];
820                     }
821                 });
822             } else {
823                 // Upload directory, confirm first
824                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
825                 [alert setMessageText:@"Upload directory"];
826                 [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]];
827                 [alert addButtonWithTitle:@"OK"];
828                 [alert addButtonWithTitle:@"Cancel"];
829                 NSInteger choice = [alert runModal];
830                 if (choice == NSAlertFirstButtonReturn) {
831                     NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
832                     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
833                     dispatch_async(queue, ^{
834                         NSMutableArray *objectNames = nil;
835                         NSMutableArray *contentTypes = nil;
836                         NSMutableArray *filePaths = nil;
837                         NSMutableArray *hashesArrays = nil;
838                         NSMutableArray *directoryObjectRequests = nil;
839                         NSArray *objectRequests = [PithosUtilities writeObjectDataRequestsWithContainerName:containerName 
840                                                                                                      objectName:objectName 
841                                                                                                       blockSize:blockSize 
842                                                                                                       blockHash:blockHash 
843                                                                                                    forDirectory:filePath 
844                                                                                                   checkIfExists:YES 
845                                                                                                     objectNames:&objectNames
846                                                                                                    contentTypes:&contentTypes
847                                                                                                       filePaths:&filePaths
848                                                                                                    hashesArrays:&hashesArrays 
849                                                                                         directoryObjectRequests:&directoryObjectRequests 
850                                                                                                  sharingAccount:destinationNode.sharingAccount];
851                         for (ASIPithosObjectRequest *objectRequest in directoryObjectRequests) {
852                             objectRequest.delegate = self;
853                             objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
854                             objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
855                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
856                                                                                        message:[NSString stringWithFormat:@"Creating directory '%@'", [objectRequest.userInfo valueForKey:@"fileName"]]];
857                             [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
858                              [NSDictionary dictionaryWithObjectsAndKeys:
859                               [NSNumber numberWithBool:YES], @"refresh", 
860                               activity, @"activity", 
861                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
862                               nil]];
863                             [[PithosUtilities prepareRequest:objectRequest] startAsynchronous];
864                         }
865                         if (objectRequests) {
866                             for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) {
867                                 ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i];
868                                 objectRequest.delegate = self;
869                                 objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
870                                 objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
871                                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
872                                                                                            message:[NSString stringWithFormat:@"Uploading '%@' (0%%)", [objectRequest.userInfo valueForKey:@"fileName"]]
873                                                                                         totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue]
874                                                                                       currentBytes:0];
875                                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
876                                  [NSDictionary dictionaryWithObjectsAndKeys:
877                                   containerName, @"containerName", 
878                                   [objectNames objectAtIndex:i], @"objectName", 
879                                   [contentTypes objectAtIndex:i], @"contentType", 
880                                   [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
881                                   blockHash, @"blockHash", 
882                                   [filePaths objectAtIndex:i], @"filePath", 
883                                   [hashesArrays objectAtIndex:i], @"hashes", 
884                                   [NSNumber numberWithBool:YES], @"refresh", 
885                                   [NSNumber numberWithUnsignedInteger:10], @"iteration", 
886                                   activity, @"activity", 
887                                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
888                                   nil]];
889                                 if (destinationNode.sharingAccount)
890                                     [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"];
891                                 [[PithosUtilities prepareRequest:objectRequest] startAsynchronous];
892                             }
893                         }
894                     });
895                 }
896             }
897         }
898     }
899     return YES;
900 }
901
902 - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {
903     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
904         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
905         return NO;
906     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
907     NSString *objectNamePrefix;
908     if ([destinationNode class] == [PithosSubdirNode class])
909         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
910     else
911         objectNamePrefix = [NSString string];
912
913     for (PithosNode *node in nodes) {
914         if (([node class] == [PithosObjectNode class]) || 
915             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
916             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
917             dispatch_async(queue, ^{
918                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
919                 if ([node.pithosObject.name hasSuffix:@"/"])
920                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
921                 ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:node.pithosContainer.name 
922                                                                                                      objectName:node.pithosObject.name 
923                                                                                        destinationContainerName:containerName 
924                                                                                           destinationObjectName:destinationObjectName 
925                                                                                                   checkIfExists:YES];
926                 if (objectRequest) {
927                     objectRequest.delegate = self;
928                     objectRequest.didFinishSelector = @selector(moveFinished:);
929                     objectRequest.didFailSelector = @selector(moveFailed:);
930                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
931                                                                                message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
932                                                                                         [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
933                                                                                         [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
934                                                                                         [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
935                                                                                         [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
936                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
937                      [NSDictionary dictionaryWithObjectsAndKeys:
938                       [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
939                       activity, @"activity", 
940                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
941                       nil]];
942                     [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
943                 }
944             });
945         } else if ([node class] == [PithosSubdirNode class]) {
946             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
947             dispatch_async(queue, ^{
948                 NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
949                 if (node.pithosObject.subdir)
950                     destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
951                 NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
952                                                                                                  objectName:node.pithosObject.name 
953                                                                                    destinationContainerName:containerName 
954                                                                                       destinationObjectName:destinationObjectName 
955                                                                                               checkIfExists:YES];
956                 if (objectRequests) {
957                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
958                         objectRequest.delegate = self;
959                         objectRequest.didFinishSelector = @selector(moveFinished:);
960                         objectRequest.didFailSelector = @selector(moveFailed:);
961                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
962                                                                                    message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
963                                                                                             [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
964                                                                                             [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
965                                                                                             [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
966                                                                                             [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
967                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
968                          [NSDictionary dictionaryWithObjectsAndKeys:
969                           [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", 
970                           [NSNumber numberWithBool:YES], @"refresh", 
971                           activity, @"activity", 
972                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
973                           nil]];
974                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
975                     }
976                 }
977             });
978         }
979     }
980     return YES;
981 }
982
983 - (BOOL)copyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode {    
984     if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) ||
985         (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) 
986         return NO;
987     NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name];
988     NSString *objectNamePrefix;
989     if ([destinationNode class] == [PithosSubdirNode class])
990         objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name];
991     else
992         objectNamePrefix = [NSString string];
993     
994     for (PithosNode *node in nodes) {
995         if (([node class] == [PithosObjectNode class]) || 
996             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
997             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
998             dispatch_async(queue, ^{
999                 NSString *destinationObjectName;
1000                 if (![destinationNode isEqualTo:node.parent]) {
1001                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1002                     if ([node.pithosObject.name hasSuffix:@"/"])
1003                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1004                 } else {
1005                     destinationObjectName = [PithosUtilities safeObjectNameForContainerName:containerName 
1006                                                                                  objectName:node.pithosObject.name];
1007                 }
1008                 ASIPithosObjectRequest *objectRequest = [PithosUtilities copyObjectRequestWithContainerName:node.pithosContainer.name 
1009                                                                                                      objectName:node.pithosObject.name 
1010                                                                                        destinationContainerName:containerName 
1011                                                                                           destinationObjectName:destinationObjectName 
1012                                                                                                   checkIfExists:YES 
1013                                                                                                  sharingAccount:node.sharingAccount];
1014                 if (objectRequest) {
1015                     objectRequest.delegate = self;
1016                     objectRequest.didFinishSelector = @selector(copyFinished:);
1017                     objectRequest.didFailSelector = @selector(copyFailed:);
1018                     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1019                                                                                message:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1020                                                                                         [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1021                                                                                         [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1022                                                                                         [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1023                                                                                         [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1024                     [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1025                      [NSDictionary dictionaryWithObjectsAndKeys:
1026                       [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1027                       activity, @"activity", 
1028                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1029                       nil]];
1030                     [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1031                 }
1032             });
1033         } else if ([node class] == [PithosSubdirNode class]) {
1034             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1035             dispatch_async(queue, ^{
1036                 NSString *destinationObjectName;
1037                 if (![destinationNode isEqualTo:node.parent]) {
1038                     destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName];
1039                     if (node.pithosObject.subdir)
1040                         destinationObjectName = [destinationObjectName stringByAppendingString:@"/"];
1041                 } else {
1042                     destinationObjectName = [PithosUtilities safeSubdirNameForContainerName:containerName 
1043                                                                                      subdirName:node.pithosObject.name];
1044                 }
1045                 NSArray *objectRequests = [PithosUtilities copyObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
1046                                                                                                  objectName:node.pithosObject.name 
1047                                                                                    destinationContainerName:containerName 
1048                                                                                       destinationObjectName:destinationObjectName 
1049                                                                                               checkIfExists:YES 
1050                                                                                              sharingAccount:node.sharingAccount];
1051                 if (objectRequests) {
1052                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1053                         objectRequest.delegate = self;
1054                         objectRequest.didFinishSelector = @selector(copyFinished:);
1055                         objectRequest.didFailSelector = @selector(copyFailed:);
1056                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy 
1057                                                                                    message:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", 
1058                                                                                             [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1059                                                                                             [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1060                                                                                             [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1061                                                                                             [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1062                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1063                          [NSDictionary dictionaryWithObjectsAndKeys:
1064                           [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", 
1065                           activity, @"activity", 
1066                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1067                           nil]];
1068                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1069                     }
1070                 }
1071             });
1072         }
1073     }
1074     return YES;
1075 }
1076
1077 #pragma mark -
1078 #pragma mark ASIHTTPRequestDelegate
1079
1080 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1081     NSLog(@"Download finished: %@", objectRequest.url);
1082     if (objectRequest.responseStatusCode == 200) {
1083         NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1084         PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1085         NSUInteger totalBytes = activity.totalBytes;
1086         NSUInteger currentBytes = activity.currentBytes;
1087         
1088         // XXX change contentLength to objectContentLength if it is fixed in the server
1089         if (([objectRequest contentLength] == 0) && (![[objectRequest contentType] isEqualToString:@"application/directory"])) {
1090             NSLog(@"Downloaded  0 bytes");
1091             NSFileManager *defaultManager = [NSFileManager defaultManager];
1092             if (![defaultManager fileExistsAtPath:filePath]) {
1093                 if (![defaultManager createFileAtPath:filePath contents:nil attributes:nil]) {
1094                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1095                     [alert setMessageText:@"Create File Error"];
1096                     [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
1097                     [alert addButtonWithTitle:@"OK"];
1098                     [alert runModal];
1099                 }
1100             }
1101         }
1102         
1103         currentBytes = [objectRequest objectContentLength];
1104         if (currentBytes == 0)
1105             currentBytes = totalBytes;
1106         [activityFacility endActivity:activity 
1107                           withMessage:[NSString stringWithFormat:@"Downloading '%@' (100%%)", 
1108                                        [objectRequest.userInfo objectForKey:@"fileName"]] 
1109                            totalBytes:totalBytes 
1110                          currentBytes:currentBytes];
1111     } else {
1112         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1113         if (retries > 0) {
1114             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1115             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1116             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1117         } else {
1118             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1119                               withMessage:[NSString stringWithFormat:@"Downloading '%@' (failed)", 
1120                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1121             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1122         }
1123     }
1124 }
1125
1126 - (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1127     NSLog(@"Download failed: %@", objectRequest.url);
1128     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1129     if (retries > 0) {
1130         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1131         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1132         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1133     } else {
1134         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1135                           withMessage:[NSString stringWithFormat:@"Downloading '%@' (failed)", 
1136                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1137         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1138     }
1139 }
1140
1141 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1142     NSLog(@"Upload directory object finished: %@", objectRequest.url);
1143     if (objectRequest.responseStatusCode == 201) {
1144         NSLog(@"Directory object created: %@", objectRequest.url);
1145         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1146                           withMessage:[NSString stringWithFormat:@"Creating directory '%@' (finished)", 
1147                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1148         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1149             [node forceRefresh];
1150         }
1151         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1152             [node refresh];
1153         }
1154         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1155             [self forceRefresh:self];
1156         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1157             [self refresh:self];
1158     } else {
1159         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1160         if (retries > 0) {
1161             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1162             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1163             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1164         } else {
1165             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1166                               withMessage:[NSString stringWithFormat:@"Creating directory '%@' (failed)", 
1167                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1168             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1169         }
1170     }
1171 }
1172
1173 - (void)uploadDirectoryObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1174     NSLog(@"Upload directory object failed: %@", objectRequest.url);
1175     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1176     if (retries > 0) {
1177         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1178         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1179         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1180     } else {
1181         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1182                           withMessage:[NSString stringWithFormat:@"Creating directory '%@' (failed)", 
1183                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1184         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1185     }
1186 }
1187
1188 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1189     NSLog(@"Upload using hashmap finished: %@", objectRequest.url);
1190     NSString *fileName = [objectRequest.userInfo objectForKey:@"fileName"];
1191     PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1192     NSUInteger totalBytes = activity.totalBytes;
1193     NSUInteger currentBytes = activity.currentBytes;
1194     if (objectRequest.responseStatusCode == 201) {
1195         NSLog(@"Object created: %@", objectRequest.url);
1196         [activityFacility endActivity:activity 
1197                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (100%%)", fileName] 
1198                            totalBytes:totalBytes 
1199                          currentBytes:totalBytes];
1200         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1201             [node forceRefresh];
1202         }
1203         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1204             [node refresh];
1205         }
1206         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1207             [self forceRefresh:self];
1208         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1209             [self refresh:self];        
1210     } else if (objectRequest.responseStatusCode == 409) {
1211         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1212         if (iteration == 0) {
1213             NSLog(@"Upload iteration limit reached: %@", objectRequest.url);
1214             [activityFacility endActivity:activity 
1215                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]]; 
1216             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1217             [alert setMessageText:@"Upload Timeout"];
1218             [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
1219                                        [objectRequest.userInfo objectForKey:@"objectName"]]];
1220             [alert addButtonWithTitle:@"OK"];
1221             [alert runModal];
1222             return;
1223         }
1224         NSLog(@"object is missing hashes: %@", objectRequest.url);
1225         NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1226                                                   withMissingHashesResponse:[objectRequest responseString]];
1227         NSUInteger blockSize = [[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1228         if (totalBytes >= [missingBlocks count]*blockSize)
1229             currentBytes = totalBytes - [missingBlocks count]*blockSize;
1230         [activityFacility updateActivity:activity 
1231                              withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
1232                               totalBytes:totalBytes 
1233                             currentBytes:currentBytes];
1234         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1235         __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
1236                                                                                                                     blockSize:blockSize 
1237                                                                                                                       forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
1238                                                                                                             missingBlockIndex:missingBlockIndex 
1239                                                                                                                sharingAccount:[objectRequest.userInfo objectForKey:@"sharingAccount"]];
1240         newContainerRequest.delegate = self;
1241         newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1242         newContainerRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
1243         newContainerRequest.userInfo = objectRequest.userInfo;
1244         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1245         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1246         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1247         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1248         [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1249             [activityFacility updateActivity:activity 
1250                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1251                                   totalBytes:activity.totalBytes 
1252                                 currentBytes:(activity.currentBytes + size)];
1253         }];
1254         [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1255     } else {
1256         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1257         if (retries > 0) {
1258             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1259             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1260             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1261         } else {
1262             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1263                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]];
1264             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1265         }
1266     }
1267 }
1268
1269 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
1270     NSLog(@"Upload using hashmap failed: %@", objectRequest.url);
1271     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1272     if (retries > 0) {
1273         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1274         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1275         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1276     } else {
1277         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1278                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", 
1279                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1280         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1281     }
1282 }
1283
1284 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1285     NSLog(@"Upload of missing block finished: %@", containerRequest.url);
1286     NSUInteger blockSize = [[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1287     NSString *fileName = [containerRequest.userInfo objectForKey:@"fileName"];
1288     PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1289     NSUInteger totalBytes = activity.totalBytes;
1290     NSUInteger currentBytes = activity.currentBytes + blockSize;
1291     if (currentBytes > totalBytes)
1292         currentBytes = totalBytes;
1293     if (containerRequest.responseStatusCode == 202) {
1294         [activityFacility updateActivity:activity 
1295                              withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
1296                               totalBytes:totalBytes 
1297                             currentBytes:currentBytes];
1298         NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1299         NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1300         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1301         if (missingBlockIndex == NSNotFound) {
1302             NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1303             ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:[containerRequest.userInfo objectForKey:@"containerName"] 
1304                                                                                                      objectName:[containerRequest.userInfo objectForKey:@"objectName"] 
1305                                                                                                     contentType:[containerRequest.userInfo objectForKey:@"contentType"] 
1306                                                                                                       blockSize:blockSize 
1307                                                                                                       blockHash:[containerRequest.userInfo objectForKey:@"blockHash"]
1308                                                                                                         forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1309                                                                                                   checkIfExists:NO 
1310                                                                                                          hashes:&hashes 
1311                                                                                                  sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1312             newObjectRequest.delegate = self;
1313             newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
1314             newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
1315             newObjectRequest.userInfo = containerRequest.userInfo;
1316             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1317             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1318             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1319             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1320         } else {
1321             __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:[containerRequest.userInfo objectForKey:@"containerName"]
1322                                                                                                                         blockSize:[[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
1323                                                                                                                           forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1324                                                                                                                 missingBlockIndex:missingBlockIndex 
1325                                                                                                                    sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1326             newContainerRequest.delegate = self;
1327             newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1328             newContainerRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
1329             newContainerRequest.userInfo = containerRequest.userInfo;
1330             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1331             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1332             [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1333                 [activityFacility updateActivity:activity 
1334                                      withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1335                                       totalBytes:activity.totalBytes 
1336                                     currentBytes:(activity.currentBytes + size)];
1337             }];
1338             [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1339         }
1340     } else {
1341         NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1342         if (retries > 0) {
1343             ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
1344             [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1345             [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1346         } else {
1347             [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1348                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]];
1349             [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest];
1350         }
1351     }
1352 }
1353
1354 - (void)uploadMissingBlockFailed:(ASIPithosContainerRequest *)containerRequest {
1355     NSLog(@"Upload of missing block failed: %@", containerRequest.url);
1356     NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1357     if (retries > 0) {
1358         ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
1359         [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1360         [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1361     } else {
1362         [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1363                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", 
1364                                        [containerRequest.userInfo objectForKey:@"fileName"]]];
1365         [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
1366     }
1367 }
1368
1369 - (void)moveFinished:(ASIPithosObjectRequest *)objectRequest {
1370     NSLog(@"Move object finished: %@", objectRequest.url);
1371     if (objectRequest.responseStatusCode == 201) {
1372         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1373                           withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (finished)", 
1374                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1375                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1376                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1377                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1378         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1379             [node forceRefresh];
1380         }
1381         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1382             [node refresh];
1383         }
1384         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1385             [self forceRefresh:self];
1386         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1387             [self refresh:self];
1388     } else {
1389         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1390         if (retries > 0) {
1391             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1392             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1393             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1394         } else {
1395             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1396                               withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (failed)", 
1397                                            [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1398                                            [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1399                                            [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1400                                            [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1401             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1402         }
1403     }
1404 }
1405
1406 - (void)moveFailed:(ASIPithosObjectRequest *)objectRequest {
1407     NSLog(@"Move object failed: %@", objectRequest.url);
1408     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1409     if (retries > 0) {
1410         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1411         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1412         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1413     } else {
1414         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1415                           withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (failed)", 
1416                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1417                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1418                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1419                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1420         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1421     }
1422 }
1423
1424 - (void)copyFinished:(ASIPithosObjectRequest *)objectRequest {
1425     NSLog(@"Copy object finished: %@", objectRequest.url);
1426     if (objectRequest.responseStatusCode == 201) {
1427         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1428                           withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (finished)", 
1429                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1430                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1431                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1432                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1433         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1434             [node forceRefresh];
1435         }
1436         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1437             [node refresh];
1438         }
1439         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1440             [self forceRefresh:self];
1441         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1442             [self refresh:self];
1443     } else {
1444         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1445         if (retries > 0) {
1446             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1447             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1448             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1449         } else {
1450             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1451                               withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (failed)", 
1452                                            [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1453                                            [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1454                                            [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1455                                            [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1456             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1457         }
1458     }
1459 }
1460
1461 - (void)copyFailed:(ASIPithosObjectRequest *)objectRequest {
1462     NSLog(@"Copy object failed: %@", objectRequest.url);
1463     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1464     if (retries > 0) {
1465         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1466         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1467         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1468     } else {
1469         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1470                           withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (failed)", 
1471                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1472                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1473                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1474                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1475         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1476     }
1477 }
1478
1479 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1480     NSLog(@"Delete object finished: %@", objectRequest.url);
1481     if (objectRequest.responseStatusCode == 204) {
1482         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1483                           withMessage:[NSString stringWithFormat:@"Deleting '%@' (finished)", 
1484                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1485         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1486             [node forceRefresh];
1487         }
1488         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1489             [node refresh];
1490         }
1491         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1492             [self forceRefresh:self];
1493         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1494             [self refresh:self];
1495     } else {
1496         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1497         if (retries > 0) {
1498             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1499             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1500             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1501         } else {
1502             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1503                               withMessage:[NSString stringWithFormat:@"Deleting '%@' (failed)", 
1504                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1505             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1506         }
1507     }
1508 }
1509
1510 - (void)deleteObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1511     NSLog(@"Delete object failed: %@", objectRequest.url);
1512     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1513     if (retries > 0) {
1514         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1515         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1516         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1517     } else {
1518         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1519                           withMessage:[NSString stringWithFormat:@"Deleting '%@' (failed)", 
1520                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1521         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1522     }
1523 }
1524
1525 #pragma mark -
1526 #pragma mark NSSplitViewDelegate
1527
1528 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
1529     if (splitView == verticalSplitView)
1530         return 120;
1531     else
1532         return ([horizontalSplitView bounds].size.height - 108);
1533 }
1534
1535 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
1536     if (splitView == verticalSplitView)
1537         return 220;
1538     else
1539         return ([horizontalSplitView bounds].size.height - 108);
1540 }
1541
1542 - (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex {
1543     if (splitView == verticalSplitView) {
1544         if (proposedPosition < 120)
1545             return 120;
1546         else if (proposedPosition > 220)
1547             return 220;
1548         else
1549             return proposedPosition;
1550     } else {
1551         return ([horizontalSplitView bounds].size.height - 108);
1552     }
1553 }
1554
1555 #pragma mark -
1556 #pragma mark NSOutlineViewDataSource
1557
1558 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
1559     if (item == nil)
1560         return 2;
1561     if (item == containersNode)
1562         return containersNodeChildren.count;
1563     if (item == sharedNode)
1564         return 2;
1565     return 0;
1566 }
1567
1568 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
1569     if (item == nil)
1570         return (!index ? containersNode : sharedNode);
1571     if (item == sharedNode)
1572         return (!index ? mySharedNode : othersSharedNode);
1573     return [containersNodeChildren objectAtIndex:index];
1574 }
1575
1576 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
1577     if ((item == containersNode) || (item == sharedNode))
1578         return YES;
1579     return NO;
1580 }
1581
1582 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
1583     PithosNode *node = (PithosNode *)item;
1584     return node;    
1585 }
1586
1587 #pragma mark Drag and Drop destination
1588
1589 - (NSDragOperation)outlineView:(NSOutlineView *)anOutlineView 
1590                   validateDrop:(id<NSDraggingInfo>)info 
1591                   proposedItem:(id)item 
1592             proposedChildIndex:(NSInteger)index {
1593     NSDragOperation result = NSDragOperationNone;
1594     if ((item == nil) || (index != NSOutlineViewDropOnItemIndex))
1595         return result;
1596     PithosNode *dropNode = (PithosNode *)item;
1597     if ([dropNode class] != [PithosContainerNode class])
1598         return result;
1599     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
1600         result = NSDragOperationCopy;
1601     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
1602         if ([info draggingSourceOperationMask] & NSDragOperationMove) {
1603             // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
1604             if (![dropNode isEqualTo:draggedParentNode])
1605                 result = NSDragOperationMove;
1606         } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
1607             // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
1608             result = NSDragOperationCopy;
1609         }
1610     }
1611    return result;
1612 }
1613
1614 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id<NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
1615     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
1616         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
1617         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
1618         if (item && (index == NSOutlineViewDropOnItemIndex) && (filenames != nil)) {
1619             PithosNode *node = (PithosNode *)item;
1620             NSLog(@"drag in node: %@", node.url);
1621             return [self uploadFiles:filenames toNode:node];
1622         }
1623     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
1624         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
1625         if (item && (index == NSOutlineViewDropOnItemIndex) && (draggedNodes != nil)) {
1626             PithosNode *node = (PithosNode *)item;
1627             NSLog(@"drag local node: %@", node.url);
1628             if ([info draggingSourceOperationMask] & NSDragOperationMove)
1629                 return [self moveNodes:draggedNodes toNode:node];
1630             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
1631                 return [self copyNodes:draggedNodes toNode:node];
1632         }
1633     }
1634     return NO;
1635 }
1636
1637 #pragma mark -
1638 #pragma mark NSOutlineViewDelegate
1639
1640 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
1641     if ((item == containersNode) || (item == sharedNode))
1642         return NO;
1643     return YES;
1644 }
1645
1646 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
1647     if ((item == containersNode) || (item == sharedNode))
1648         return YES;
1649     return NO;
1650 }
1651
1652 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
1653     PithosNode *node = (PithosNode *)[outlineView itemAtRow:[outlineView selectedRow]];
1654     if (node) {
1655         rootNode = node;
1656         [browser loadColumnZero];
1657         [self refresh:nil];
1658     }
1659 }
1660
1661 #pragma mark -
1662 #pragma mark NSMenuDelegate
1663
1664 - (void)menuNeedsUpdate:(NSMenu *)menu {
1665     [menu removeAllItems];
1666     NSMenuItem *menuItem;
1667     NSString *menuItemTitle;
1668     BOOL nodeContextMenu = NO;
1669     PithosNode *menuNode;
1670     NSMutableArray *menuNodes;
1671     if (menu == browserMenu) {
1672         NSInteger column = [browser clickedColumn];
1673         NSInteger row = [browser clickedRow];
1674         if ((column == -1) || (row == -1)) {
1675             // General context menu
1676             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
1677             if ([menuNodesIndexPaths count] == 0) {
1678                 menuNode = [browser parentForItemsInColumn:0];
1679             } else if (([menuNodesIndexPaths count] != 1) || 
1680                        ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) {
1681                 menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)];
1682             } else {
1683                 menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]];
1684             }
1685         } else {
1686             // Node context menu
1687             NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row];
1688             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
1689             menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]];
1690             if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) {
1691                 for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) {
1692                     [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]];
1693                 }
1694             } else {
1695                 [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]];
1696             }
1697             nodeContextMenu = YES;
1698         }
1699     } else if (menu == outlineViewMenu) {
1700         NSInteger row = [outlineView clickedRow];
1701         if (row == -1)
1702             row = [outlineView selectedRow];
1703         if (row == -1)
1704             return;
1705         menuNode = [outlineView itemAtRow:row];
1706     }
1707
1708     if (!nodeContextMenu) {
1709         // General context menu
1710         if (([menuNode class] == [PithosAccountNode class]) || 
1711             ([menuNode class] == [PithosSharingAccountsNode class]) ||
1712             ([menuNode class] == [PithosEmptyNode class]))
1713             return;
1714         BOOL shared = menuNode.shared;
1715         BOOL sharingAccount = (menuNode.sharingAccount != nil);
1716         // New Folder
1717         if (!shared && !sharingAccount) {
1718             menuItem = [[[NSMenuItem alloc] initWithTitle:@"New Folder" action:@selector(menuNewFolder:) keyEquivalent:@""] autorelease];
1719             [menuItem setRepresentedObject:menuNode];
1720             [menu addItem:menuItem];
1721             [menu addItem:[NSMenuItem separatorItem]];
1722         }
1723         // Get Info
1724         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
1725         [menuItem setRepresentedObject:[NSArray arrayWithObject:menuNode]];
1726         [menu addItem:menuItem];
1727         // Paste
1728         if (!shared && !sharingAccount) {
1729             if (clipboardNodes) {
1730                 NSUInteger clipboardNodesCount = [clipboardNodes count];
1731                 if (clipboardNodesCount == 0) {
1732                     self.clipboardNodes = nil;
1733                 } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
1734                     if (clipboardNodesCount == 1)
1735                         menuItemTitle = [NSString stringWithString:@"Paste Item"];
1736                     else
1737                         menuItemTitle = [NSString stringWithString:@"Paste Items"];
1738                     [menu addItem:[NSMenuItem separatorItem]];
1739                     menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
1740                     [menuItem setRepresentedObject:menuNode];
1741                     [menu addItem:menuItem];
1742                 }
1743             }
1744         }
1745     } else {
1746         // Node context menu
1747         NSUInteger menuNodesCount = [menuNodes count];
1748         PithosNode *firstMenuNode = (PithosNode *)[menuNodes objectAtIndex:0];
1749         BOOL shared = firstMenuNode.shared;
1750         BOOL sharingAccount = (firstMenuNode.sharingAccount != nil);
1751         // Move to Trash (pithos container only)
1752         // Delete
1753         if (!shared && !sharingAccount) {
1754             if ([rootNode class] == [PithosContainerNode class]) {
1755                 if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
1756                     menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(menuMoveToTrash:) keyEquivalent:@""] autorelease];
1757                     [menuItem setRepresentedObject:menuNodes];
1758                     [menu addItem:menuItem];
1759                 }
1760                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(menuDelete:) keyEquivalent:@""] autorelease];
1761                 [menuItem setRepresentedObject:menuNodes];
1762                 [menu addItem:menuItem];
1763                 [menu addItem:[NSMenuItem separatorItem]];
1764             }
1765         }
1766         // Get Info
1767         if (!sharingAccount || ([firstMenuNode class] != [PithosAccountNode class])) {
1768             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
1769             [menuItem setRepresentedObject:menuNodes];
1770             [menu addItem:menuItem];
1771             
1772             if ((!shared && !sharingAccount) || ([firstMenuNode class] != [PithosContainerNode class]))
1773                 [menu addItem:[NSMenuItem separatorItem]];
1774         }
1775         // Cut
1776         if (!shared && !sharingAccount) {
1777             if (menuNodesCount == 1)
1778                 menuItemTitle = [NSString stringWithFormat:@"Cut \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
1779             else 
1780                 menuItemTitle = [NSString stringWithFormat:@"Cut %lu Items", menuNodesCount];
1781             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCut:) keyEquivalent:@""] autorelease];
1782             [menuItem setRepresentedObject:menuNodes];
1783             [menu addItem:menuItem];
1784         }
1785         // Copy
1786         if ((!shared && !sharingAccount) || 
1787             (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class]))) {
1788             if (menuNodesCount == 1)
1789                 menuItemTitle = [NSString stringWithFormat:@"Copy \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
1790             else 
1791                 menuItemTitle = [NSString stringWithFormat:@"Copy %lu Items", menuNodesCount];
1792             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCopy:) keyEquivalent:@""] autorelease];
1793             [menuItem setRepresentedObject:menuNodes];
1794             [menu addItem:menuItem];
1795         }
1796         // Paste
1797         if (!shared && !sharingAccount) {
1798             if (menuNodesCount == 1) {
1799                 PithosNode *menuNode = [menuNodes objectAtIndex:0];
1800                 if (([menuNode class] == [PithosSubdirNode class]) && 
1801                     (menuNode.pithosObject.subdir || ![menuNode.pithosObject.name hasSuffix:@"/"])) {
1802                     if (clipboardNodes) {
1803                         NSUInteger clipboardNodesCount = [clipboardNodes count];
1804                         if (clipboardNodesCount == 0) {
1805                             self.clipboardNodes = nil;
1806                         } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
1807                             if (clipboardNodesCount == 1)
1808                                 menuItemTitle = [NSString stringWithString:@"Paste Item"];
1809                             else
1810                                 menuItemTitle = [NSString stringWithString:@"Paste Items"];
1811                             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
1812                             [menuItem setRepresentedObject:menuNode];
1813                             [menu addItem:menuItem];
1814                         }
1815                     }
1816                 }
1817             }
1818         }
1819     }
1820 }
1821
1822 #pragma mark -
1823 #pragma mark Menu Actions
1824
1825 - (void)menuNewFolder:(NSMenuItem *)sender {
1826     PithosNode *node = (PithosNode *)[sender representedObject];
1827     if ([node class] == [PithosContainerNode class]) {
1828         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1829         dispatch_async(queue, ^{
1830             NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:node.pithosContainer.name 
1831                                                                                 subdirName:@"untitled folder"];
1832             NSString *fileName = [safeObjectName lastPathComponent];
1833             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:node.pithosContainer.name 
1834                                                                                                          objectName:safeObjectName 
1835                                                                                                            eTag:nil 
1836                                                                                                     contentType:@"application/directory" 
1837                                                                                                 contentEncoding:nil 
1838                                                                                              contentDisposition:nil 
1839                                                                                                        manifest:nil 
1840                                                                                                         sharing:nil 
1841                                                                                                        isPublic:ASIPithosObjectRequestPublicIgnore 
1842                                                                                                        metadata:nil 
1843                                                                                                            data:[NSData data]];
1844             objectRequest.delegate = self;
1845             objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
1846             objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
1847             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1848                                                                        message:[NSString stringWithFormat:@"Creating directory '%@'", fileName]];
1849             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1850                                       fileName, @"fileName", 
1851                                       [NSArray arrayWithObject:node], @"refreshNodes", 
1852                                       [NSNumber numberWithBool:YES], @"refresh", 
1853                                       activity, @"activity", 
1854                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1855                                       nil];
1856             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1857         });
1858     } else if (([node class] == [PithosSubdirNode class]) && 
1859                (node.pithosObject.subdir || ![node.pithosObject.name hasSuffix:@"/"])) {
1860         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1861         dispatch_async(queue, ^{
1862             NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:node.pithosContainer.name 
1863                                                                                subdirName:[node.pithosObject.name stringByAppendingPathComponent:@"untitled folder"]];
1864             NSString *fileName = [safeObjectName lastPathComponent];
1865             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:node.pithosContainer.name 
1866                                                                                                         objectName:safeObjectName 
1867                                                                                                               eTag:nil 
1868                                                                                                        contentType:@"application/directory" 
1869                                                                                                    contentEncoding:nil 
1870                                                                                                 contentDisposition:nil 
1871                                                                                                           manifest:nil 
1872                                                                                                            sharing:nil 
1873                                                                                                           isPublic:ASIPithosObjectRequestPublicIgnore 
1874                                                                                                           metadata:nil 
1875                                                                                                               data:[NSData data]];
1876             objectRequest.delegate = self;
1877             objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
1878             objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
1879             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1880                                                                        message:[NSString stringWithFormat:@"Creating directory '%@'", fileName]];
1881             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1882                                       fileName, @"fileName", 
1883                                       [NSArray arrayWithObject:node], @"refreshNodes", 
1884                                       [NSNumber numberWithBool:YES], @"refresh", 
1885                                       activity, @"activity", 
1886                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1887                                       nil];
1888             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1889         });
1890     }
1891 }
1892
1893 - (void)menuGetInfo:(NSMenuItem *)sender {
1894     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1895         [node showPithosNodeInfo:sender];
1896     }
1897 }
1898
1899 - (void)menuDelete:(NSMenuItem *)sender {
1900     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1901         if (([node class] == [PithosObjectNode class]) || 
1902             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1903             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name 
1904                                                                                                       objectName:node.pithosObject.name];
1905             objectRequest.delegate = self;
1906             objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
1907             objectRequest.didFailSelector = @selector(deleteObjectFailed:);
1908             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1909                                                                        message:[NSString stringWithFormat:@"Deleting '%@'", 
1910                                                                                 [objectRequest.userInfo objectForKey:@"fileName"]]];
1911             [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1912              [NSDictionary dictionaryWithObjectsAndKeys:
1913               [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1914               activity, @"activity", 
1915               [NSNumber numberWithUnsignedInteger:10], @"retries", 
1916               nil]];
1917             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1918         } else if ([node class] == [PithosSubdirNode class]) {
1919             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1920             dispatch_async(queue, ^{
1921                 NSArray *objectRequests = [PithosUtilities deleteObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
1922                                                                                                objectName:node.pithosObject.name];
1923                 if (objectRequests) {
1924                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1925                         objectRequest.delegate = self;
1926                         objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
1927                         objectRequest.didFailSelector = @selector(deleteObjectFailed:);
1928                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1929                                                                                    message:[NSString stringWithFormat:@"Deleting '%@'", 
1930                                                                                             [objectRequest.userInfo objectForKey:@"fileName"]]];
1931                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1932                          [NSDictionary dictionaryWithObjectsAndKeys:
1933                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1934                           activity, @"activity", 
1935                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1936                           nil]];
1937                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1938                     }
1939                 }
1940             });
1941         }
1942     }
1943 }
1944
1945 - (void)menuMoveToTrash:(NSMenuItem *)sender {
1946     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1947         if (([node class] == [PithosObjectNode class]) || 
1948             (([node class] == [PithosSubdirNode class]) && 
1949              !node.pithosObject.subdir &&
1950              [node.pithosObject.name hasSuffix:@"/"])) {
1951             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1952             dispatch_async(queue, ^{
1953                 NSString *safeObjectName = [PithosUtilities safeObjectNameForContainerName:@"trash" 
1954                                                                                     objectName:node.pithosObject.name];
1955                 if (safeObjectName) {
1956                     ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:node.pithosContainer.name 
1957                                                                                                          objectName:node.pithosObject.name 
1958                                                                                            destinationContainerName:@"trash" 
1959                                                                                               destinationObjectName:safeObjectName 
1960                                                                                                       checkIfExists:NO];
1961                     if (objectRequest) {
1962                         objectRequest.delegate = self;
1963                         objectRequest.didFinishSelector = @selector(moveFinished:);
1964                         objectRequest.didFailSelector = @selector(moveFailed:);
1965                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1966                                                                                    message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1967                                                                                             [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1968                                                                                             [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1969                                                                                             [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1970                                                                                             [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1971                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1972                          [NSDictionary dictionaryWithObjectsAndKeys:
1973                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1974                           activity, @"activity", 
1975                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1976                           nil]];
1977                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1978                     }
1979                 }
1980             });
1981         } else if ([node class] == [PithosSubdirNode class]) {
1982             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1983             dispatch_async(queue, ^{
1984                 NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:@"trash" 
1985                                                                                     subdirName:node.pithosObject.name];
1986                 if (safeObjectName) {
1987                     NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
1988                                                                                                      objectName:node.pithosObject.name 
1989                                                                                        destinationContainerName:@"trash" 
1990                                                                                           destinationObjectName:safeObjectName 
1991                                                                                                   checkIfExists:NO];
1992                     if (objectRequests) {
1993                         for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1994                             objectRequest.delegate = self;
1995                             objectRequest.didFinishSelector = @selector(moveFinished:);
1996                             objectRequest.didFailSelector = @selector(moveFailed:);
1997                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1998                                                                                        message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1999                                                                                                 [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2000                                                                                                 [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2001                                                                                                 [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2002                                                                                                 [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
2003                             [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2004                              [NSDictionary dictionaryWithObjectsAndKeys:
2005                               [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2006                               activity, @"activity", 
2007                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
2008                               nil]];
2009                             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
2010                         }
2011                     }
2012                 }
2013             });
2014         }
2015     }
2016 }
2017
2018 - (void)menuCut:(NSMenuItem *)sender {
2019     self.clipboardNodes = (NSArray *)[sender representedObject];
2020     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2021     self.clipboardCopy = NO;
2022 }
2023
2024 - (void)menuCopy:(NSMenuItem *)sender {
2025     self.clipboardNodes = (NSArray *)[sender representedObject];
2026     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2027     self.clipboardCopy = YES;
2028 }
2029
2030 - (void)menuPaste:(NSMenuItem *)sender {
2031     if (!clipboardNodes || ![clipboardNodes count])
2032         return;
2033     PithosNode *dropNode = (PithosNode *)[sender representedObject];
2034     NSArray *localClipboardNodes = [NSArray arrayWithArray:clipboardNodes];
2035     if (!clipboardCopy && ![dropNode isEqualTo:clipboardParentNode]) {
2036         self.clipboardNodes = nil;
2037         self.clipboardParentNode = nil;
2038         [self moveNodes:localClipboardNodes toNode:dropNode];
2039     } else {
2040         [self copyNodes:localClipboardNodes toNode:dropNode];
2041     }
2042 }
2043     
2044 #pragma mark -
2045 #pragma mark PithosActivityFacilityDelegate
2046
2047 - (void)activityUpdate:(NSDictionary *)info {
2048     NSString *message = [info objectForKey:@"message"];
2049     NSUInteger runningActivitiesCount = [[info objectForKey:@"runningActivitiesCount"] unsignedIntegerValue];
2050 //    NSUInteger endingActivitiesCount = [[info objectForKey:@"endingActivitiesCount"] unsignedIntegerValue];
2051     NSUInteger totalUploadBytes = [[info objectForKey:@"totalUploadBytes"] unsignedIntegerValue];
2052     NSUInteger currentUploadBytes = [[info objectForKey:@"currentUploadBytes"] unsignedIntegerValue];
2053     NSUInteger totalDownloadBytes = [[info objectForKey:@"totalDownloadBytes"] unsignedIntegerValue];
2054     NSUInteger currentDownloadBytes = [[info objectForKey:@"currentDownloadBytes"] unsignedIntegerValue];
2055     NSUInteger totalBytes = totalUploadBytes + totalDownloadBytes;
2056     if (runningActivitiesCount && totalBytes) {
2057         [activityProgressIndicator setDoubleValue:((currentUploadBytes + currentDownloadBytes + 0.0)/(totalBytes + 0.0))];
2058         [activityProgressIndicator startAnimation:self];
2059     } else {
2060         [activityProgressIndicator setDoubleValue:1.0];
2061         [activityProgressIndicator stopAnimation:self];
2062     }
2063     
2064     if (!message)
2065         message = [NSString stringWithFormat:@"%@ used", 
2066                    [[[[BytesSizeTransformer alloc] init] autorelease] transformedValue: 
2067                     [NSNumber numberWithUnsignedInteger:accountNode.pithosAccount.bytesUsed]]];
2068     [activityTextField setStringValue:message];
2069 }
2070
2071 @end