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