Upload progress bug fixed.
[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             sleep(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             sleep(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:NSOperationQueuePriorityVeryHigh] 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:NSOperationQueuePriorityVeryHigh] 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             sleep(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:NSOperationQueuePriorityVeryHigh] 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:NSOperationQueuePriorityVeryHigh] 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:NSOperationQueuePriorityVeryHigh] 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:NSOperationQueuePriorityVeryHigh] 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         NSUInteger currentBytes = activity.currentBytes;
1094         
1095         // XXX change contentLength to objectContentLength if it is fixed in the server
1096         if (([objectRequest contentLength] == 0) && (![[objectRequest contentType] isEqualToString:@"application/directory"])) {
1097             NSLog(@"Downloaded  0 bytes");
1098             NSFileManager *fileManager = [NSFileManager defaultManager];
1099             if (![fileManager fileExistsAtPath:filePath]) {
1100                 if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
1101                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1102                     [alert setMessageText:@"Create File Error"];
1103                     [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
1104                     [alert addButtonWithTitle:@"OK"];
1105                     [alert runModal];
1106                 }
1107             }
1108         }
1109         
1110         currentBytes = [objectRequest objectContentLength];
1111         if (currentBytes == 0)
1112             currentBytes = totalBytes;
1113         [activityFacility endActivity:activity 
1114                           withMessage:[NSString stringWithFormat:@"Downloading '%@' (100%%)", 
1115                                        [objectRequest.userInfo objectForKey:@"fileName"]] 
1116                            totalBytes:totalBytes 
1117                          currentBytes:currentBytes];
1118     } else {
1119         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1120         if (retries > 0) {
1121             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1122             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1123             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1124         } else {
1125             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1126                               withMessage:[NSString stringWithFormat:@"Downloading '%@' (failed)", 
1127                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1128             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1129         }
1130     }
1131 }
1132
1133 - (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1134     NSLog(@"Download failed: %@", objectRequest.url);
1135     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1136     if (retries > 0) {
1137         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1138         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1139         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1140     } else {
1141         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1142                           withMessage:[NSString stringWithFormat:@"Downloading '%@' (failed)", 
1143                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1144         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1145     }
1146 }
1147
1148 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1149     NSLog(@"Upload directory object finished: %@", objectRequest.url);
1150     if (objectRequest.responseStatusCode == 201) {
1151         NSLog(@"Directory object created: %@", objectRequest.url);
1152         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1153                           withMessage:[NSString stringWithFormat:@"Creating directory '%@' (finished)", 
1154                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1155         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1156             [node forceRefresh];
1157         }
1158         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1159             [node refresh];
1160         }
1161         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1162             [self forceRefresh:self];
1163         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1164             [self refresh:self];
1165     } else {
1166         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1167         if (retries > 0) {
1168             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1169             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1170             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1171         } else {
1172             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1173                               withMessage:[NSString stringWithFormat:@"Creating directory '%@' (failed)", 
1174                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1175             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1176         }
1177     }
1178 }
1179
1180 - (void)uploadDirectoryObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1181     NSLog(@"Upload directory object failed: %@", objectRequest.url);
1182     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1183     if (retries > 0) {
1184         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1185         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1186         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1187     } else {
1188         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1189                           withMessage:[NSString stringWithFormat:@"Creating directory '%@' (failed)", 
1190                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1191         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1192     }
1193 }
1194
1195 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1196     NSLog(@"Upload using hashmap finished: %@", objectRequest.url);
1197     NSString *fileName = [objectRequest.userInfo objectForKey:@"fileName"];
1198     PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1199     NSUInteger totalBytes = activity.totalBytes;
1200     NSUInteger currentBytes = activity.currentBytes;
1201     if (objectRequest.responseStatusCode == 201) {
1202         NSLog(@"Object created: %@", objectRequest.url);
1203         [activityFacility endActivity:activity 
1204                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (100%%)", fileName] 
1205                            totalBytes:totalBytes 
1206                          currentBytes:totalBytes];
1207         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1208             [node forceRefresh];
1209         }
1210         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1211             [node refresh];
1212         }
1213         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1214             [self forceRefresh:self];
1215         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1216             [self refresh:self];        
1217     } else if (objectRequest.responseStatusCode == 409) {
1218         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1219         if (iteration == 0) {
1220             NSLog(@"Upload iteration limit reached: %@", objectRequest.url);
1221             [activityFacility endActivity:activity 
1222                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]]; 
1223             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
1224             [alert setMessageText:@"Upload Timeout"];
1225             [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
1226                                        [objectRequest.userInfo objectForKey:@"objectName"]]];
1227             [alert addButtonWithTitle:@"OK"];
1228             [alert runModal];
1229             return;
1230         }
1231         NSLog(@"object is missing hashes: %@", objectRequest.url);
1232         NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1233                                                   withMissingHashesResponse:[objectRequest responseString]];
1234         NSUInteger blockSize = [[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1235         if (totalBytes >= [missingBlocks count]*blockSize)
1236             currentBytes = totalBytes - [missingBlocks count]*blockSize;
1237         [activityFacility updateActivity:activity 
1238                              withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
1239                               totalBytes:totalBytes 
1240                             currentBytes:currentBytes];
1241         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1242         __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
1243                                                                                                                     blockSize:blockSize 
1244                                                                                                                       forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
1245                                                                                                             missingBlockIndex:missingBlockIndex 
1246                                                                                                                sharingAccount:[objectRequest.userInfo objectForKey:@"sharingAccount"]];
1247         newContainerRequest.delegate = self;
1248         newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1249         newContainerRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
1250         newContainerRequest.userInfo = objectRequest.userInfo;
1251         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1252         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1253         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1254         [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1255         [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1256             [activityFacility updateActivity:activity 
1257                                  withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1258                                   totalBytes:activity.totalBytes 
1259                                 currentBytes:(activity.currentBytes + size)];
1260         }];
1261         [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1262     } else {
1263         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1264         if (retries > 0) {
1265             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1266             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1267             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1268         } else {
1269             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1270                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]];
1271             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1272         }
1273     }
1274 }
1275
1276 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
1277     NSLog(@"Upload using hashmap failed: %@", objectRequest.url);
1278     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1279     if (retries > 0) {
1280         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1281         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1282         [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1283     } else {
1284         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1285                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", 
1286                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1287         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1288     }
1289 }
1290
1291 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1292     NSLog(@"Upload of missing block finished: %@", containerRequest.url);
1293     NSUInteger blockSize = [[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue];
1294     NSString *fileName = [containerRequest.userInfo objectForKey:@"fileName"];
1295     PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1296     if (containerRequest.responseStatusCode == 202) {
1297         NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1298         NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1299         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1300         if (missingBlockIndex == NSNotFound) {
1301             NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1302             ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:[containerRequest.userInfo objectForKey:@"containerName"] 
1303                                                                                                      objectName:[containerRequest.userInfo objectForKey:@"objectName"] 
1304                                                                                                     contentType:[containerRequest.userInfo objectForKey:@"contentType"] 
1305                                                                                                       blockSize:blockSize 
1306                                                                                                       blockHash:[containerRequest.userInfo objectForKey:@"blockHash"]
1307                                                                                                         forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1308                                                                                                   checkIfExists:NO 
1309                                                                                                          hashes:&hashes 
1310                                                                                                  sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1311             newObjectRequest.delegate = self;
1312             newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
1313             newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
1314             newObjectRequest.userInfo = containerRequest.userInfo;
1315             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1316             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1317             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1318             [[PithosUtilities prepareRequest:newObjectRequest] startAsynchronous];
1319         } else {
1320             __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:[containerRequest.userInfo objectForKey:@"containerName"]
1321                                                                                                                         blockSize:[[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
1322                                                                                                                           forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1323                                                                                                                 missingBlockIndex:missingBlockIndex 
1324                                                                                                                    sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]];
1325             newContainerRequest.delegate = self;
1326             newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1327             newContainerRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
1328             newContainerRequest.userInfo = containerRequest.userInfo;
1329             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1330             [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1331             [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1332                 [activityFacility updateActivity:activity 
1333                                      withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1334                                       totalBytes:activity.totalBytes 
1335                                     currentBytes:(activity.currentBytes + size)];
1336             }];
1337             [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1338         }
1339     } else {
1340         NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1341         if (retries > 0) {
1342             ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
1343             [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1344             [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1345         } else {
1346             [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1347                               withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", fileName]];
1348             [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest];
1349         }
1350     }
1351 }
1352
1353 - (void)uploadMissingBlockFailed:(ASIPithosContainerRequest *)containerRequest {
1354     NSLog(@"Upload of missing block failed: %@", containerRequest.url);
1355     NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1356     if (retries > 0) {
1357         ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
1358         [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1359         [[PithosUtilities prepareRequest:newContainerRequest] startAsynchronous];
1360     } else {
1361         [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1362                           withMessage:[NSString stringWithFormat:@"Uploading '%@' (failed)", 
1363                                        [containerRequest.userInfo objectForKey:@"fileName"]]];
1364         [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest];
1365     }
1366 }
1367
1368 - (void)moveFinished:(ASIPithosObjectRequest *)objectRequest {
1369     NSLog(@"Move object finished: %@", objectRequest.url);
1370     if (objectRequest.responseStatusCode == 201) {
1371         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1372                           withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (finished)", 
1373                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1374                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1375                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1376                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1377         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1378             [node forceRefresh];
1379         }
1380         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1381             [node refresh];
1382         }
1383         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1384             [self forceRefresh:self];
1385         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1386             [self refresh:self];
1387     } else {
1388         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1389         if (retries > 0) {
1390             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1391             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1392             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1393         } else {
1394             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1395                               withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (failed)", 
1396                                            [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1397                                            [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1398                                            [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1399                                            [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1400             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1401         }
1402     }
1403 }
1404
1405 - (void)moveFailed:(ASIPithosObjectRequest *)objectRequest {
1406     NSLog(@"Move object failed: %@", objectRequest.url);
1407     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1408     if (retries > 0) {
1409         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1410         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1411         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1412     } else {
1413         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1414                           withMessage:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@' (failed)", 
1415                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1416                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1417                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1418                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1419         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1420     }
1421 }
1422
1423 - (void)copyFinished:(ASIPithosObjectRequest *)objectRequest {
1424     NSLog(@"Copy object finished: %@", objectRequest.url);
1425     if (objectRequest.responseStatusCode == 201) {
1426         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1427                           withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (finished)", 
1428                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1429                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1430                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1431                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1432         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1433             [node forceRefresh];
1434         }
1435         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1436             [node refresh];
1437         }
1438         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1439             [self forceRefresh:self];
1440         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1441             [self refresh:self];
1442     } else {
1443         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1444         if (retries > 0) {
1445             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1446             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1447             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1448         } else {
1449             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1450                               withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (failed)", 
1451                                            [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1452                                            [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1453                                            [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1454                                            [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1455             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1456         }
1457     }
1458 }
1459
1460 - (void)copyFailed:(ASIPithosObjectRequest *)objectRequest {
1461     NSLog(@"Copy object failed: %@", objectRequest.url);
1462     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1463     if (retries > 0) {
1464         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1465         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1466         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1467     } else {
1468         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1469                           withMessage:[NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@' (failed)", 
1470                                        [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1471                                        [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1472                                        [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1473                                        [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1474         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1475     }
1476 }
1477
1478 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1479     NSLog(@"Delete object finished: %@", objectRequest.url);
1480     if (objectRequest.responseStatusCode == 204) {
1481         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1482                           withMessage:[NSString stringWithFormat:@"Deleting '%@' (finished)", 
1483                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1484         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) {
1485             [node forceRefresh];
1486         }
1487         for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) {
1488             [node refresh];
1489         }
1490         if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue])
1491             [self forceRefresh:self];
1492         else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue])
1493             [self refresh:self];
1494     } else {
1495         NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1496         if (retries > 0) {
1497             ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1498             [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1499             [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1500         } else {
1501             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1502                               withMessage:[NSString stringWithFormat:@"Deleting '%@' (failed)", 
1503                                            [objectRequest.userInfo objectForKey:@"fileName"]]];
1504             [PithosUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
1505         }
1506     }
1507 }
1508
1509 - (void)deleteObjectFailed:(ASIPithosObjectRequest *)objectRequest {
1510     NSLog(@"Delete object failed: %@", objectRequest.url);
1511     NSUInteger retries = [[objectRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1512     if (retries > 0) {
1513         ASIPithosObjectRequest *newObjectRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:objectRequest];
1514         [(NSMutableDictionary *)(newObjectRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1515         [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1516     } else {
1517         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1518                           withMessage:[NSString stringWithFormat:@"Deleting '%@' (failed)", 
1519                                        [objectRequest.userInfo objectForKey:@"fileName"]]];
1520         [PithosUtilities httpRequestErrorAlertWithRequest:objectRequest];
1521     }
1522 }
1523
1524 #pragma mark -
1525 #pragma mark NSSplitViewDelegate
1526
1527 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
1528     if (splitView == verticalSplitView)
1529         return 120;
1530     else
1531         return ([horizontalSplitView bounds].size.height - 108);
1532 }
1533
1534 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
1535     if (splitView == verticalSplitView)
1536         return 220;
1537     else
1538         return ([horizontalSplitView bounds].size.height - 108);
1539 }
1540
1541 - (CGFloat)splitView:(NSSplitView *)splitView constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)dividerIndex {
1542     if (splitView == verticalSplitView) {
1543         if (proposedPosition < 120)
1544             return 120;
1545         else if (proposedPosition > 220)
1546             return 220;
1547         else
1548             return proposedPosition;
1549     } else {
1550         return ([horizontalSplitView bounds].size.height - 108);
1551     }
1552 }
1553
1554 #pragma mark -
1555 #pragma mark NSOutlineViewDataSource
1556
1557 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
1558     if (item == nil)
1559         return 2;
1560     if (item == containersNode)
1561         return containersNodeChildren.count;
1562     if (item == sharedNode)
1563         return 2;
1564     return 0;
1565 }
1566
1567 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
1568     if (item == nil)
1569         return (!index ? containersNode : sharedNode);
1570     if (item == sharedNode)
1571         return (!index ? mySharedNode : othersSharedNode);
1572     return [containersNodeChildren objectAtIndex:index];
1573 }
1574
1575 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
1576     if ((item == containersNode) || (item == sharedNode))
1577         return YES;
1578     return NO;
1579 }
1580
1581 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
1582     PithosNode *node = (PithosNode *)item;
1583     return node;    
1584 }
1585
1586 #pragma mark Drag and Drop destination
1587
1588 - (NSDragOperation)outlineView:(NSOutlineView *)anOutlineView 
1589                   validateDrop:(id<NSDraggingInfo>)info 
1590                   proposedItem:(id)item 
1591             proposedChildIndex:(NSInteger)index {
1592     NSDragOperation result = NSDragOperationNone;
1593     if ((item == nil) || (index != NSOutlineViewDropOnItemIndex))
1594         return result;
1595     PithosNode *dropNode = (PithosNode *)item;
1596     if ([dropNode class] != [PithosContainerNode class])
1597         return result;
1598     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
1599         result = NSDragOperationCopy;
1600     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
1601         if ([info draggingSourceOperationMask] & NSDragOperationMove) {
1602             // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove
1603             if (![dropNode isEqualTo:draggedParentNode])
1604                 result = NSDragOperationMove;
1605         } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) {
1606             // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy
1607             result = NSDragOperationCopy;
1608         }
1609     }
1610    return result;
1611 }
1612
1613 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id<NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
1614     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
1615         NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
1616         NSLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames);
1617         if (item && (index == NSOutlineViewDropOnItemIndex) && (filenames != nil)) {
1618             PithosNode *node = (PithosNode *)item;
1619             NSLog(@"drag in node: %@", node.url);
1620             return [self uploadFiles:filenames toNode:node];
1621         }
1622     } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) {
1623         NSLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes);
1624         if (item && (index == NSOutlineViewDropOnItemIndex) && (draggedNodes != nil)) {
1625             PithosNode *node = (PithosNode *)item;
1626             NSLog(@"drag local node: %@", node.url);
1627             if ([info draggingSourceOperationMask] & NSDragOperationMove)
1628                 return [self moveNodes:draggedNodes toNode:node];
1629             else if ([info draggingSourceOperationMask] & NSDragOperationCopy)
1630                 return [self copyNodes:draggedNodes toNode:node];
1631         }
1632     }
1633     return NO;
1634 }
1635
1636 #pragma mark -
1637 #pragma mark NSOutlineViewDelegate
1638
1639 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
1640     if ((item == containersNode) || (item == sharedNode))
1641         return NO;
1642     return YES;
1643 }
1644
1645 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
1646     if ((item == containersNode) || (item == sharedNode))
1647         return YES;
1648     return NO;
1649 }
1650
1651 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
1652     PithosNode *node = (PithosNode *)[outlineView itemAtRow:[outlineView selectedRow]];
1653     if (node) {
1654         rootNode = node;
1655         [browser loadColumnZero];
1656         [self refresh:nil];
1657     }
1658 }
1659
1660 #pragma mark -
1661 #pragma mark NSMenuDelegate
1662
1663 - (void)menuNeedsUpdate:(NSMenu *)menu {
1664     [menu removeAllItems];
1665     NSMenuItem *menuItem;
1666     NSString *menuItemTitle;
1667     BOOL nodeContextMenu = NO;
1668     PithosNode *menuNode;
1669     NSMutableArray *menuNodes;
1670     if (menu == browserMenu) {
1671         NSInteger column = [browser clickedColumn];
1672         NSInteger row = [browser clickedRow];
1673         if ((column == -1) || (row == -1)) {
1674             // General context menu
1675             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
1676             if ([menuNodesIndexPaths count] == 0) {
1677                 menuNode = [browser parentForItemsInColumn:0];
1678             } else if (([menuNodesIndexPaths count] != 1) || 
1679                        ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) {
1680                 menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)];
1681             } else {
1682                 menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]];
1683             }
1684         } else {
1685             // Node context menu
1686             NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row];
1687             NSArray *menuNodesIndexPaths = [browser selectionIndexPaths];
1688             menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]];
1689             if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) {
1690                 for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) {
1691                     [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]];
1692                 }
1693             } else {
1694                 [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]];
1695             }
1696             nodeContextMenu = YES;
1697         }
1698     } else if (menu == outlineViewMenu) {
1699         NSInteger row = [outlineView clickedRow];
1700         if (row == -1)
1701             row = [outlineView selectedRow];
1702         if (row == -1)
1703             return;
1704         menuNode = [outlineView itemAtRow:row];
1705     }
1706
1707     if (!nodeContextMenu) {
1708         // General context menu
1709         if (([menuNode class] == [PithosAccountNode class]) || 
1710             ([menuNode class] == [PithosSharingAccountsNode class]) ||
1711             ([menuNode class] == [PithosEmptyNode class]))
1712             return;
1713         BOOL shared = menuNode.shared;
1714         BOOL sharingAccount = (menuNode.sharingAccount != nil);
1715         // New Folder
1716         if (!shared && !sharingAccount) {
1717             menuItem = [[[NSMenuItem alloc] initWithTitle:@"New Folder" action:@selector(menuNewFolder:) keyEquivalent:@""] autorelease];
1718             [menuItem setRepresentedObject:menuNode];
1719             [menu addItem:menuItem];
1720             [menu addItem:[NSMenuItem separatorItem]];
1721         }
1722         // Get Info
1723         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
1724         [menuItem setRepresentedObject:[NSArray arrayWithObject:menuNode]];
1725         [menu addItem:menuItem];
1726         // Paste
1727         if (!shared && !sharingAccount) {
1728             if (clipboardNodes) {
1729                 NSUInteger clipboardNodesCount = [clipboardNodes count];
1730                 if (clipboardNodesCount == 0) {
1731                     self.clipboardNodes = nil;
1732                 } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
1733                     if (clipboardNodesCount == 1)
1734                         menuItemTitle = [NSString stringWithString:@"Paste Item"];
1735                     else
1736                         menuItemTitle = [NSString stringWithString:@"Paste Items"];
1737                     [menu addItem:[NSMenuItem separatorItem]];
1738                     menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
1739                     [menuItem setRepresentedObject:menuNode];
1740                     [menu addItem:menuItem];
1741                 }
1742             }
1743         }
1744     } else {
1745         // Node context menu
1746         NSUInteger menuNodesCount = [menuNodes count];
1747         PithosNode *firstMenuNode = (PithosNode *)[menuNodes objectAtIndex:0];
1748         BOOL shared = firstMenuNode.shared;
1749         BOOL sharingAccount = (firstMenuNode.sharingAccount != nil);
1750         // Move to Trash (pithos container only)
1751         // Delete
1752         if (!shared && !sharingAccount) {
1753             if ([rootNode class] == [PithosContainerNode class]) {
1754                 if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
1755                     menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(menuMoveToTrash:) keyEquivalent:@""] autorelease];
1756                     [menuItem setRepresentedObject:menuNodes];
1757                     [menu addItem:menuItem];
1758                 }
1759                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(menuDelete:) keyEquivalent:@""] autorelease];
1760                 [menuItem setRepresentedObject:menuNodes];
1761                 [menu addItem:menuItem];
1762                 [menu addItem:[NSMenuItem separatorItem]];
1763             }
1764         }
1765         // Get Info
1766         if (!sharingAccount || ([firstMenuNode class] != [PithosAccountNode class])) {
1767             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(menuGetInfo:) keyEquivalent:@""] autorelease];
1768             [menuItem setRepresentedObject:menuNodes];
1769             [menu addItem:menuItem];
1770             
1771             if ((!shared && !sharingAccount) || ([firstMenuNode class] != [PithosContainerNode class]))
1772                 [menu addItem:[NSMenuItem separatorItem]];
1773         }
1774         // Cut
1775         if (!shared && !sharingAccount) {
1776             if (menuNodesCount == 1)
1777                 menuItemTitle = [NSString stringWithFormat:@"Cut \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
1778             else 
1779                 menuItemTitle = [NSString stringWithFormat:@"Cut %lu Items", menuNodesCount];
1780             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCut:) keyEquivalent:@""] autorelease];
1781             [menuItem setRepresentedObject:menuNodes];
1782             [menu addItem:menuItem];
1783         }
1784         // Copy
1785         if ((!shared && !sharingAccount) || 
1786             (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class]))) {
1787             if (menuNodesCount == 1)
1788                 menuItemTitle = [NSString stringWithFormat:@"Copy \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName];
1789             else 
1790                 menuItemTitle = [NSString stringWithFormat:@"Copy %lu Items", menuNodesCount];
1791             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCopy:) keyEquivalent:@""] autorelease];
1792             [menuItem setRepresentedObject:menuNodes];
1793             [menu addItem:menuItem];
1794         }
1795         // Paste
1796         if (!shared && !sharingAccount) {
1797             if (menuNodesCount == 1) {
1798                 PithosNode *menuNode = [menuNodes objectAtIndex:0];
1799                 if (([menuNode class] == [PithosSubdirNode class]) && 
1800                     (menuNode.pithosObject.subdir || ![menuNode.pithosObject.name hasSuffix:@"/"])) {
1801                     if (clipboardNodes) {
1802                         NSUInteger clipboardNodesCount = [clipboardNodes count];
1803                         if (clipboardNodesCount == 0) {
1804                             self.clipboardNodes = nil;
1805                         } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) {
1806                             if (clipboardNodesCount == 1)
1807                                 menuItemTitle = [NSString stringWithString:@"Paste Item"];
1808                             else
1809                                 menuItemTitle = [NSString stringWithString:@"Paste Items"];
1810                             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""] autorelease];
1811                             [menuItem setRepresentedObject:menuNode];
1812                             [menu addItem:menuItem];
1813                         }
1814                     }
1815                 }
1816             }
1817         }
1818     }
1819 }
1820
1821 #pragma mark -
1822 #pragma mark Menu Actions
1823
1824 - (void)menuNewFolder:(NSMenuItem *)sender {
1825     PithosNode *node = (PithosNode *)[sender representedObject];
1826     if ([node class] == [PithosContainerNode class]) {
1827         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1828         dispatch_async(queue, ^{
1829             NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:node.pithosContainer.name 
1830                                                                                 subdirName:@"untitled folder"];
1831             NSString *fileName = [safeObjectName lastPathComponent];
1832             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:node.pithosContainer.name 
1833                                                                                                          objectName:safeObjectName 
1834                                                                                                            eTag:nil 
1835                                                                                                     contentType:@"application/directory" 
1836                                                                                                 contentEncoding:nil 
1837                                                                                              contentDisposition:nil 
1838                                                                                                        manifest:nil 
1839                                                                                                         sharing:nil 
1840                                                                                                        isPublic:ASIPithosObjectRequestPublicIgnore 
1841                                                                                                        metadata:nil 
1842                                                                                                            data:[NSData data]];
1843             objectRequest.delegate = self;
1844             objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
1845             objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
1846             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1847                                                                        message:[NSString stringWithFormat:@"Creating directory '%@'", fileName]];
1848             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1849                                       fileName, @"fileName", 
1850                                       [NSArray arrayWithObject:node], @"refreshNodes", 
1851                                       [NSNumber numberWithBool:YES], @"refresh", 
1852                                       activity, @"activity", 
1853                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1854                                       nil];
1855             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1856         });
1857     } else if (([node class] == [PithosSubdirNode class]) && 
1858                (node.pithosObject.subdir || ![node.pithosObject.name hasSuffix:@"/"])) {
1859         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1860         dispatch_async(queue, ^{
1861             NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:node.pithosContainer.name 
1862                                                                                subdirName:[node.pithosObject.name stringByAppendingPathComponent:@"untitled folder"]];
1863             NSString *fileName = [safeObjectName lastPathComponent];
1864             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:node.pithosContainer.name 
1865                                                                                                         objectName:safeObjectName 
1866                                                                                                               eTag:nil 
1867                                                                                                        contentType:@"application/directory" 
1868                                                                                                    contentEncoding:nil 
1869                                                                                                 contentDisposition:nil 
1870                                                                                                           manifest:nil 
1871                                                                                                            sharing:nil 
1872                                                                                                           isPublic:ASIPithosObjectRequestPublicIgnore 
1873                                                                                                           metadata:nil 
1874                                                                                                               data:[NSData data]];
1875             objectRequest.delegate = self;
1876             objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
1877             objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
1878             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1879                                                                        message:[NSString stringWithFormat:@"Creating directory '%@'", fileName]];
1880             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1881                                       fileName, @"fileName", 
1882                                       [NSArray arrayWithObject:node], @"refreshNodes", 
1883                                       [NSNumber numberWithBool:YES], @"refresh", 
1884                                       activity, @"activity", 
1885                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1886                                       nil];
1887             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1888         });
1889     }
1890 }
1891
1892 - (void)menuGetInfo:(NSMenuItem *)sender {
1893     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1894         [node showPithosNodeInfo:sender];
1895     }
1896 }
1897
1898 - (void)menuDelete:(NSMenuItem *)sender {
1899     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1900         if (([node class] == [PithosObjectNode class]) || 
1901             (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) {
1902             NSString *fileName = [node.pithosObject.name lastPathComponent];
1903             if ([node.pithosObject.name hasSuffix:@"/"])
1904                 fileName = [fileName stringByAppendingString:@"/"];
1905             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name 
1906                                                                                                       objectName:node.pithosObject.name];
1907             objectRequest.delegate = self;
1908             objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
1909             objectRequest.didFailSelector = @selector(deleteObjectFailed:);
1910             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1911                                                                        message:[NSString stringWithFormat:@"Deleting '%@'", fileName]];
1912             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1913                                       fileName, @"fileName", 
1914                                       [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1915                                       activity, @"activity", 
1916                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1917                                       nil];
1918             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1919         } else if ([node class] == [PithosSubdirNode class]) {
1920             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1921             dispatch_async(queue, ^{
1922                 NSArray *objectRequests = [PithosUtilities deleteObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
1923                                                                                                objectName:node.pithosObject.name];
1924                 if (objectRequests) {
1925                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1926                         objectRequest.delegate = self;
1927                         objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
1928                         objectRequest.didFailSelector = @selector(deleteObjectFailed:);
1929                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1930                                                                                    message:[NSString stringWithFormat:@"Deleting '%@'", 
1931                                                                                             [objectRequest.userInfo objectForKey:@"fileName"]]];
1932                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1933                          [NSDictionary dictionaryWithObjectsAndKeys:
1934                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1935                           activity, @"activity", 
1936                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1937                           nil]];
1938                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1939                     }
1940                 }
1941             });
1942         }
1943     }
1944 }
1945
1946 - (void)menuMoveToTrash:(NSMenuItem *)sender {
1947     for (PithosNode *node in ((NSArray *)[sender representedObject])) {
1948         if (([node class] == [PithosObjectNode class]) || 
1949             (([node class] == [PithosSubdirNode class]) && 
1950              !node.pithosObject.subdir &&
1951              [node.pithosObject.name hasSuffix:@"/"])) {
1952             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1953             dispatch_async(queue, ^{
1954                 NSString *safeObjectName = [PithosUtilities safeObjectNameForContainerName:@"trash" 
1955                                                                                     objectName:node.pithosObject.name];
1956                 if (safeObjectName) {
1957                     ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:node.pithosContainer.name 
1958                                                                                                          objectName:node.pithosObject.name 
1959                                                                                            destinationContainerName:@"trash" 
1960                                                                                               destinationObjectName:safeObjectName 
1961                                                                                                       checkIfExists:NO];
1962                     if (objectRequest) {
1963                         objectRequest.delegate = self;
1964                         objectRequest.didFinishSelector = @selector(moveFinished:);
1965                         objectRequest.didFailSelector = @selector(moveFailed:);
1966                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1967                                                                                    message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
1968                                                                                             [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
1969                                                                                             [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
1970                                                                                             [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
1971                                                                                             [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
1972                         [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
1973                          [NSDictionary dictionaryWithObjectsAndKeys:
1974                           [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
1975                           activity, @"activity", 
1976                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1977                           nil]];
1978                         [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
1979                     }
1980                 }
1981             });
1982         } else if ([node class] == [PithosSubdirNode class]) {
1983             dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1984             dispatch_async(queue, ^{
1985                 NSString *safeObjectName = [PithosUtilities safeSubdirNameForContainerName:@"trash" 
1986                                                                                     subdirName:node.pithosObject.name];
1987                 if (safeObjectName) {
1988                     NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
1989                                                                                                      objectName:node.pithosObject.name 
1990                                                                                        destinationContainerName:@"trash" 
1991                                                                                           destinationObjectName:safeObjectName 
1992                                                                                                   checkIfExists:NO];
1993                     if (objectRequests) {
1994                         for (ASIPithosObjectRequest *objectRequest in objectRequests) {
1995                             objectRequest.delegate = self;
1996                             objectRequest.didFinishSelector = @selector(moveFinished:);
1997                             objectRequest.didFailSelector = @selector(moveFailed:);
1998                             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove 
1999                                                                                        message:[NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", 
2000                                                                                                 [objectRequest.userInfo objectForKey:@"sourceContainerName"], 
2001                                                                                                 [objectRequest.userInfo objectForKey:@"sourceObjectName"], 
2002                                                                                                 [objectRequest.userInfo objectForKey:@"destinationContainerName"], 
2003                                                                                                 [objectRequest.userInfo objectForKey:@"destinationObjectName"]]];
2004                             [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary:
2005                              [NSDictionary dictionaryWithObjectsAndKeys:
2006                               [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", 
2007                               activity, @"activity", 
2008                               [NSNumber numberWithUnsignedInteger:10], @"retries", 
2009                               nil]];
2010                             [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
2011                         }
2012                     }
2013                 }
2014             });
2015         }
2016     }
2017 }
2018
2019 - (void)menuCut:(NSMenuItem *)sender {
2020     self.clipboardNodes = (NSArray *)[sender representedObject];
2021     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2022     self.clipboardCopy = NO;
2023 }
2024
2025 - (void)menuCopy:(NSMenuItem *)sender {
2026     self.clipboardNodes = (NSArray *)[sender representedObject];
2027     self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent;
2028     self.clipboardCopy = YES;
2029 }
2030
2031 - (void)menuPaste:(NSMenuItem *)sender {
2032     if (!clipboardNodes || ![clipboardNodes count])
2033         return;
2034     PithosNode *dropNode = (PithosNode *)[sender representedObject];
2035     NSArray *localClipboardNodes = [NSArray arrayWithArray:clipboardNodes];
2036     if (!clipboardCopy && ![dropNode isEqualTo:clipboardParentNode]) {
2037         self.clipboardNodes = nil;
2038         self.clipboardParentNode = nil;
2039         [self moveNodes:localClipboardNodes toNode:dropNode];
2040     } else {
2041         [self copyNodes:localClipboardNodes toNode:dropNode];
2042     }
2043 }
2044     
2045 #pragma mark -
2046 #pragma mark PithosActivityFacilityDelegate
2047
2048 - (void)activityUpdate:(NSDictionary *)info {
2049     NSString *message = [info objectForKey:@"message"];
2050     NSUInteger runningActivitiesCount = [[info objectForKey:@"runningActivitiesCount"] unsignedIntegerValue];
2051 //    NSUInteger endingActivitiesCount = [[info objectForKey:@"endingActivitiesCount"] unsignedIntegerValue];
2052     NSUInteger totalUploadBytes = [[info objectForKey:@"totalUploadBytes"] unsignedIntegerValue];
2053     NSUInteger currentUploadBytes = [[info objectForKey:@"currentUploadBytes"] unsignedIntegerValue];
2054     NSUInteger totalDownloadBytes = [[info objectForKey:@"totalDownloadBytes"] unsignedIntegerValue];
2055     NSUInteger currentDownloadBytes = [[info objectForKey:@"currentDownloadBytes"] unsignedIntegerValue];
2056     NSUInteger totalBytes = totalUploadBytes + totalDownloadBytes;
2057     if (runningActivitiesCount && totalBytes) {
2058         [activityProgressIndicator setDoubleValue:((currentUploadBytes + currentDownloadBytes + 0.0)/(totalBytes + 0.0))];
2059         [activityProgressIndicator startAnimation:self];
2060     } else {
2061         [activityProgressIndicator setDoubleValue:1.0];
2062         [activityProgressIndicator stopAnimation:self];
2063     }
2064     
2065     if (!message)
2066         message = [[[[UsingSizeTransformer alloc] init] autorelease] transformedValue:accountNode.pithosAccount];
2067     [activityTextField setStringValue:message];
2068 }
2069
2070 @end