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