Implemented menu delete and move to trash.
[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 "PithosEmptyNode.h"
45 #import "ImageAndTextCell.h"
46 #import "FileSystemBrowserCell.h"
47 #import "ASIPithosRequest.h"
48 #import "ASIPithosContainerRequest.h"
49 #import "ASIPithosObjectRequest.h"
50 #import "ASIPithosContainer.h"
51 #import "ASIPithosObject.h"
52 #import "PithosFileUtilities.h"
53
54 @interface PithosBrowserCell : FileSystemBrowserCell {}
55 @end
56
57 @implementation PithosBrowserCell
58
59 - (id)init {
60     if ((self = [super init])) {
61         [self setLineBreakMode:NSLineBreakByTruncatingMiddle];
62     }
63     return self;
64 }
65
66 - (void)setObjectValue:(id)object {
67     if ([object isKindOfClass:[PithosNode class]]) {
68         PithosNode *node = (PithosNode *)object;
69         [self setStringValue:node.displayName];
70         [self setImage:node.icon];
71     } else {
72         [super setObjectValue:object];
73     }
74 }
75
76 @end
77
78 @interface PithosOutlineViewCell : ImageAndTextCell {}
79 @end
80
81 @implementation PithosOutlineViewCell
82
83 - (void)setObjectValue:(id)object {
84     if ([object isKindOfClass:[PithosNode class]]) {
85         PithosNode *node = (PithosNode *)object;
86         [self setStringValue:node.displayName];
87         [self setImage:node.icon];
88         [self setEditable:NO];
89     } else {
90         [super setObjectValue:object];
91     }
92 }
93
94 @end
95
96 @interface PithosBrowserController (Private) {}
97 - (void)resetContainers:(NSNotification *)notification;
98 - (void)getInfo:(NSMenuItem *)sender;
99 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest;
100 - (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest;
101 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest;
102 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest;
103 - (void)uploadMissingBlockFinished:(ASIPithosObjectRequest *)objectRequest;
104 - (void)uploadMissingBlockFailed:(ASIPithosObjectRequest *)objectRequest;
105 @end
106
107 @implementation PithosBrowserController
108 @synthesize outlineViewDataSourceArray, splitView, outlineView, browser;
109
110 #pragma mark -
111 #pragma Object Lifecycle
112
113 - (id)init {
114     return [super initWithWindowNibName:@"PithosBrowserController"];
115 }
116
117 - (void)dealloc {
118     [[NSNotificationCenter defaultCenter] removeObserver:self];
119     [browserMenu release];
120     [sharedPreviewController release];
121     [outlineViewDataSourceArray release];
122     [accountNode release];
123     [rootNode release];
124     [super dealloc];
125 }
126
127 - (void)awakeFromNib {
128     [super awakeFromNib];
129     
130     [browser registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
131     [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
132     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
133     
134     [browser setCellClass:[PithosBrowserCell class]];
135     
136     browserMenu = [[NSMenu alloc] init];
137     [browserMenu setDelegate:self];
138     [browser setMenu:browserMenu];
139 }
140
141 - (void)resetContainers:(NSNotification *)notification {
142     rootNode = nil;
143     [browser loadColumnZero];
144     self.outlineViewDataSourceArray = nil;
145     
146     // Create the outlineView tree
147     // CONTAINERS
148         NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
149                             [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]];
150
151     // SHARED
152         NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
153                                       [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]];
154     // SHARED/my shared
155         [[sharedTreeNode mutableChildNodes] addObject:
156      [NSTreeNode treeNodeWithRepresentedObject:
157       [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" 
158                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]
159         ] autorelease]]];
160     // SHARED/others shared
161         [[sharedTreeNode mutableChildNodes] addObject:
162      [NSTreeNode treeNodeWithRepresentedObject:
163       [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared"
164                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]
165         ] autorelease]]];
166     
167     self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil];
168     
169         // Expand the folder outline view
170     [outlineView expandItem:nil expandChildren:YES];
171         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
172     
173     // Create accountNode and trigger a refresh
174     accountNode = [[PithosAccountNode alloc] init];
175     accountNode.children;
176 }
177
178 - (void)windowDidLoad {
179     [super windowDidLoad];
180     
181     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
182     
183     // Register for updates
184     [[NSNotificationCenter defaultCenter] addObserver:self 
185                                              selector:@selector(pithosNodeChildrenUpdated:) 
186                                                  name:@"PithosContainerNodeChildrenUpdated" 
187                                                object:nil];
188     [[NSNotificationCenter defaultCenter] addObserver:self 
189                                              selector:@selector(pithosNodeChildrenUpdated:) 
190                                                  name:@"PithosSubdirNodeChildrenUpdated" 
191                                                object:nil];
192     [[NSNotificationCenter defaultCenter] addObserver:self 
193                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
194                                                  name:@"PithosAccountNodeChildrenUpdated" 
195                                                object:nil];
196     [[NSNotificationCenter defaultCenter] addObserver:self 
197                                              selector:@selector(resetContainers:) 
198                                                  name:@"PithosAuthenticationCredentialsUpdated" 
199                                                object:nil];
200 }
201
202 #pragma mark -
203 #pragma Observers
204
205 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
206     PithosNode *node = (PithosNode *)[notification object];
207     NSLog(@"pithosNodeChildrenUpdated:%@", node.url);
208     NSInteger lastColumn = [browser lastColumn];
209     for (NSInteger column = lastColumn; column >= 0; column--) {
210         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
211             [browser reloadColumn:column];
212             // The following code is unnecessary since the pithosObject is set in the PithosNode in each refresh
213             // Furthermore it caused problems on delete, because a non-existing parent was asked for
214             //if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) {
215             //    // This reloads the preview column
216             //    [browser setLastColumn:column];
217             //    [browser addColumn];
218             //}
219             return;
220         }
221     }
222 }
223
224 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
225     BOOL containerPithosFound = NO;
226     BOOL containerTrashFound = NO;
227     NSMutableArray *containersTreeNodeChildren = [NSMutableArray array];
228     for (PithosContainerNode *containerNode in accountNode.children) {
229         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
230             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
231             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:0];
232             containerPithosFound = YES;
233         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
234             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];            
235             NSUInteger insertIndex = 1;
236             if (!containerPithosFound)
237                 insertIndex = 0;
238             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:insertIndex];
239             containerTrashFound = YES;
240         } else {
241             [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]];
242         }
243     }
244     BOOL refreshAccountNode = NO;
245     if (!containerPithosFound) {
246         // create pithos
247         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
248         [containerRequest startSynchronous];
249         if ([containerRequest error]) {
250             [PithosFileUtilities httpRequestErrorAlertWithRequest:containerRequest];
251         } else {
252             refreshAccountNode = YES;
253         }
254     }
255     if (!containerTrashFound) {
256         // create trash
257         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
258         [containerRequest startSynchronous];
259         if ([containerRequest error]) {
260             [PithosFileUtilities httpRequestErrorAlertWithRequest:containerRequest];
261         } else {
262             refreshAccountNode = YES;
263         }
264     }
265     if (refreshAccountNode) {
266         [accountNode invalidateChildren];
267         accountNode.children;
268     } else {
269         [[[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes] setArray:containersTreeNodeChildren];
270         self.outlineViewDataSourceArray = outlineViewDataSourceArray;
271         
272         // Expand the folder outline view
273         [outlineView expandItem:nil expandChildren:YES];
274         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
275         
276         [self refresh:nil];
277     }
278 }
279
280 #pragma mark -
281 #pragma Actions
282
283 - (IBAction)refresh:(id)sender {
284     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
285         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
286     }
287     [browser validateVisibleColumns];
288 }
289
290 #pragma mark -
291 #pragma NSBrowserDelegate
292
293 - (id)rootItemForBrowser:(NSBrowser *)browser {
294     return rootNode;    
295 }
296
297 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
298     PithosNode *node = (PithosNode *)item;
299     return node.children.count;
300 }
301
302 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
303     PithosNode *node = (PithosNode *)item;
304     return [node.children objectAtIndex:index];
305 }
306
307 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
308     PithosNode *node = (PithosNode *)item;
309     return node.isLeafItem;
310 }
311
312 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
313     PithosNode *node = (PithosNode *)item;
314     return node;
315 }
316
317 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
318     if (sharedPreviewController == nil)
319         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
320     return sharedPreviewController;
321 }
322
323 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
324 //    if (!forUserResize) {
325 //        id item = [browser parentForItemsInColumn:columnIndex]; 
326 //        if ([self browser:browser isLeafItem:item]) {
327 //            suggestedWidth = 200; 
328 //        }
329 //    }
330 //    return suggestedWidth;
331 //}
332
333 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
334     return NO;
335 }
336
337 #pragma mark Drag and Drop source
338
339 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
340    toPasteboard:(NSPasteboard *)pasteboard {
341     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
342     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
343     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
344         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
345         [propertyList addObject:[node.pithosObject.name pathExtension]];
346     }
347
348     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
349     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
350     
351     return YES;
352 }
353
354 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
355 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
356     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
357     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
358     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
359         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
360         
361         // If the node is a subdir ask if the whole tree should be downloaded
362         if ([node class] == [PithosSubdirNode class]) {
363             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
364             [alert setMessageText:@"Download directory"];
365             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
366             [alert addButtonWithTitle:@"OK"];
367             [alert addButtonWithTitle:@"Cancel"];
368             NSInteger choice = [alert runModal];
369             if (choice == NSAlertFirstButtonReturn) {
370                 NSArray *objectRequests = [PithosFileUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
371                                                                                                  objectName:node.pithosObject.name 
372                                                                                                 toDirectory:[dropDestination path] 
373                                                                                               checkIfExists:YES];
374                 if (objectRequests) {
375                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
376                         [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
377                         objectRequest.delegate = self;
378                         objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
379                         objectRequest.didFailSelector = @selector(downloadObjectFailed:);
380                         [objectRequest startAsynchronous];
381                     }
382                 }
383             }
384         } else {
385             ASIPithosObjectRequest *objectRequest = [PithosFileUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
386                                                                                                  objectName:node.pithosObject.name 
387                                                                                                 toDirectory:[dropDestination path] 
388                                                                                               checkIfExists:YES];
389             if (objectRequest) {
390                 [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
391                 objectRequest.delegate = self;
392                 objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
393                 objectRequest.didFailSelector = @selector(downloadObjectFailed:);
394                 [objectRequest startAsynchronous];
395             }
396         }
397     }
398     return names;
399 }
400
401 #pragma mark Drag and Drop destination
402
403 - (NSDragOperation)browser:aBrowser 
404               validateDrop:(id<NSDraggingInfo>)info 
405                proposedRow:(NSInteger *)row 
406                     column:(NSInteger *)column 
407              dropOperation:(NSBrowserDropOperation *)dropOperation {
408     NSDragOperation result = NSDragOperationNone;
409     // Files from the finder are accepted
410     if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
411         // For a between drop, we let the user drop "on" the parent item
412         if (*dropOperation == NSBrowserDropAbove)
413             *row = -1;
414         // Only allow dropping in folders
415         if (*column != -1) {
416             if (*row != -1) {
417                 PithosNode *node = [browser itemAtRow:*row inColumn:*column];
418                 if ([node class] != [PithosSubdirNode class])
419                     *row = -1;
420             }
421             *dropOperation = NSBrowserDropOn;
422             result = NSDragOperationCopy;
423         }
424     }
425     // XXX else local file promises
426     return result;
427 }
428
429 - (BOOL)browser:(NSBrowser *)aBrowser 
430      acceptDrop:(id<NSDraggingInfo>)info 
431           atRow:(NSInteger)row 
432          column:(NSInteger)column 
433   dropOperation:(NSBrowserDropOperation)dropOperation {
434     NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
435     NSLog(@"drag in filenames: %@", filenames);
436     PithosNode *node = nil;
437     if ((column != -1) && (filenames != nil)) {
438         if (row != -1)
439             node = [browser itemAtRow:row inColumn:column];
440         else
441             node = [browser parentForItemsInColumn:column];
442         NSLog(@"drag in node: %@", node.url);
443         if (([node class] != [PithosSubdirNode class]) && ([node class] != [PithosContainerNode class]))
444             return NO;
445         
446         NSFileManager *defaultManager = [NSFileManager defaultManager];
447         NSString *containerName = [NSString stringWithString:node.pithosContainer.name];
448         NSString *objectNamePrefix;
449         if ([node class] == [PithosSubdirNode class])
450             objectNamePrefix = [NSString stringWithString:node.pithosObject.name];
451         else
452             objectNamePrefix = [NSString stringWithString:@""];
453         NSUInteger blockSize = node.pithosContainer.blockSize;
454         NSString *blockHash = node.pithosContainer.blockHash;
455         
456         for (NSString *filePath in filenames) {
457             BOOL isDirectory;
458             if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
459                 if (!isDirectory) {
460                     // Upload file
461                     NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
462                     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
463                     dispatch_async(queue, ^{
464                         NSError *error = nil;
465                         NSString *contentType = [PithosFileUtilities contentTypeOfFile:filePath error:&error];
466                         if (contentType == nil)
467                             contentType = @"application/octet-stream";
468                         if (error)
469                             NSLog(@"contentType detection error: %@", error);
470                         NSArray *hashes = nil;
471                         ASIPithosObjectRequest *objectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:containerName 
472                                                                                                                   objectName:objectName 
473                                                                                                                  contentType:contentType 
474                                                                                                                    blockSize:blockSize 
475                                                                                                                    blockHash:blockHash 
476                                                                                                                      forFile:filePath 
477                                                                                                                checkIfExists:YES 
478                                                                                                                       hashes:&hashes];
479                         if (objectRequest) {
480                             objectRequest.delegate = self;
481                             objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
482                             objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
483                             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
484                                                       containerName, @"containerName", 
485                                                       objectName, @"objectName", 
486                                                       contentType, @"contentType", 
487                                                       [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
488                                                       blockHash, @"blockHash", 
489                                                       filePath, @"filePath", 
490                                                       hashes, @"hashes", 
491                                                       node, @"node", 
492                                                       [NSNumber numberWithUnsignedInteger:10], @"iteration", 
493                                                       nil];
494                             [objectRequest startAsynchronous];
495                         }
496                     });
497                 } else {
498                     // Upload directory, confirm first
499                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
500                     [alert setMessageText:@"Upload directory"];
501                     [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]];
502                     [alert addButtonWithTitle:@"OK"];
503                     [alert addButtonWithTitle:@"Cancel"];
504                     NSInteger choice = [alert runModal];
505                     if (choice == NSAlertFirstButtonReturn) {
506                         NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
507                         dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
508                         dispatch_async(queue, ^{
509                             NSMutableArray *objectNames = nil;
510                             NSMutableArray *contentTypes = nil;
511                             NSMutableArray *filePaths = nil;
512                             NSMutableArray *hashesArrays = nil;
513                             NSArray *objectRequests = [PithosFileUtilities writeObjectDataRequestsWithContainerName:containerName 
514                                                                                                          objectName:objectName 
515                                                                                                           blockSize:blockSize 
516                                                                                                           blockHash:blockHash 
517                                                                                                        forDirectory:filePath 
518                                                                                                       checkIfExists:YES 
519                                                                                                         objectNames:&objectNames
520                                                                                                        contentTypes:&contentTypes
521                                                                                                           filePaths:&filePaths
522                                                                                                          hashesArrays:&hashesArrays];
523                             if (objectRequests) {
524                                 for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) {
525                                     ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i];
526                                     // XXX if dir creation requests differentiate
527                                     objectRequest.delegate = self;
528                                     objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
529                                     objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
530                                     objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
531                                                               containerName, @"containerName", 
532                                                               [objectNames objectAtIndex:i], @"objectName", 
533                                                               [contentTypes objectAtIndex:i], @"contentType", 
534                                                               [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
535                                                               blockHash, @"blockHash", 
536                                                               [filePaths objectAtIndex:i], @"filePath", 
537                                                               [hashesArrays objectAtIndex:i], @"hashes", 
538                                                               [NSNull null], @"node", 
539                                                               [NSNumber numberWithUnsignedInteger:10], @"iteration", 
540                                                               nil];
541                                     [objectRequest startAsynchronous];
542                                 }
543                             }
544                         });
545                     }
546                 }
547             }
548             
549         }
550         return YES;
551     }
552
553     return NO;
554 }
555
556 #pragma mark -
557 #pragma mark ASIHTTPRequestDelegate
558
559 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
560     NSLog(@"Download completed: %@", [objectRequest url]);
561     if ([objectRequest bytes] == 0) {
562         NSLog(@"Downloaded  0 bytes");
563         NSFileManager *defaultManager = [NSFileManager defaultManager];
564         NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
565         if (![defaultManager fileExistsAtPath:filePath]) {
566             if (![defaultManager createFileAtPath:filePath contents:nil attributes:nil]) {
567                 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
568                 [alert setMessageText:@"Create File Error"];
569                 [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
570                 [alert addButtonWithTitle:@"OK"];
571                 [alert runModal];
572             }
573         }
574     }
575 }
576
577 - (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest {
578     NSLog(@"Download failed");
579     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
580 }
581
582 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
583     NSLog(@"Upload using hashmap completed: %@", [objectRequest url]);
584     if (objectRequest.responseStatusCode == 201) {
585         NSLog(@"Object created: %@", [objectRequest url]);
586         PithosNode *node = [objectRequest.userInfo objectForKey:@"node"];
587         if (node != (id)[NSNull null]) {
588             [node invalidateChildren];
589             node.children;
590         } // XXX else total refresh?
591     } else if (objectRequest.responseStatusCode == 409) {
592         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue] - 1;
593         if (iteration == 0) {
594             NSLog(@"Upload iteration limit reached: %@", [objectRequest url]);
595             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
596             [alert setMessageText:@"Upload Timeout"];
597             [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
598                                        [objectRequest.userInfo objectForKey:@"objectName"], [objectRequest.userInfo objectForKey:@"containerName"]]];
599             [alert addButtonWithTitle:@"OK"];
600             [alert runModal];
601             return;
602         }
603         NSLog(@"object is missing hashes: %@", [objectRequest url]);
604         NSIndexSet *missingBlocks = [PithosFileUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
605                                                       withMissingHashesResponse:[objectRequest responseString]];
606         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
607         ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
608                                                                                                       objectName:@".upload" 
609                                                                                                        blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
610                                                                                                          forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
611                                                                                                missingBlockIndex:missingBlockIndex];
612         newObjectRequest.delegate = self;
613         newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
614         newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
615         newObjectRequest.userInfo = objectRequest.userInfo;
616         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:iteration] forKey:@"iteration"];
617         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
618         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
619         [newObjectRequest startAsynchronous];
620     } else {
621         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
622     }
623 }
624
625 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
626     NSLog(@"Upload using hashmap failed");
627     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
628 }
629
630 - (void)uploadMissingBlockFinished:(ASIPithosObjectRequest *)objectRequest {
631     NSLog(@"Upload of missing block completed: %@", [objectRequest url]);
632     if (objectRequest.responseStatusCode == 201) {
633         NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
634         NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
635         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
636         if (missingBlockIndex == NSNotFound) {
637             NSArray *hashes = [objectRequest.userInfo objectForKey:@"hashes"];
638             ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
639                                                                                                          objectName:[objectRequest.userInfo objectForKey:@"objectName"] 
640                                                                                                         contentType:[objectRequest.userInfo objectForKey:@"contentType"] 
641                                                                                                           blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] 
642                                                                                                           blockHash:[objectRequest.userInfo objectForKey:@"blockHash"]
643                                                                                                             forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
644                                                                                                       checkIfExists:NO 
645                                                                                                              hashes:&hashes];
646             newObjectRequest.delegate = self;
647             newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
648             newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
649             newObjectRequest.userInfo = objectRequest.userInfo;
650             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
651             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
652             [newObjectRequest startAsynchronous];
653         } else {
654             ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
655                                                                                                           objectName:@".upload" 
656                                                                                                            blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
657                                                                                                              forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
658                                                                                                    missingBlockIndex:missingBlockIndex];
659             newObjectRequest.delegate = self;
660             newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
661             newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
662             newObjectRequest.userInfo = objectRequest.userInfo;
663             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
664             [newObjectRequest startAsynchronous];
665         }
666     } else {
667         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
668     }
669 }
670
671 - (void)uploadMissingBlockFailed:(ASIPithosObjectRequest *)objectRequest {
672     NSLog(@"Upload of missing block failed");
673     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
674 }
675
676 #pragma mark -
677 #pragma mark NSSplitViewDelegate
678
679 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
680     return 120;
681 }
682
683 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
684     return 220;
685 }
686
687 #pragma mark -
688 #pragma mark NSOutlineViewDelegate
689
690 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
691     return ([[item representedObject] isLeaf]);
692 }
693
694 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
695         return (![[item representedObject] isLeaf]);
696 }
697
698 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
699     PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject];
700     if (node) {
701         rootNode = node;
702         [browser loadColumnZero];
703         [self refresh:nil];                                  
704     }
705 }
706
707 #pragma mark -
708 #pragma mark NSMenuDelegate
709
710 - (void)menuNeedsUpdate:(NSMenu *)menu {
711     NSInteger column = [browser clickedColumn];
712     NSInteger row = [browser clickedRow];
713     [menu removeAllItems];
714     NSMenuItem *menuItem;
715     if ((column == -1) || (row == -1)) {
716         // General context menu
717     } else {
718         // Move to Trash (pithos container only)
719         // Delete
720         if ([rootNode class] == [PithosContainerNode class]) {
721             if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
722                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(moveToTrash:) keyEquivalent:@""] autorelease];
723                 [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
724                 [menu addItem:menuItem];
725             }
726             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteObject:) keyEquivalent:@""] autorelease];
727             [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
728             [menu addItem:menuItem];
729             [menu addItem:[NSMenuItem separatorItem]];
730         }
731         // Get Info
732         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""] autorelease];
733         [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
734         [menu addItem:menuItem];
735     }
736 }
737
738 #pragma mark -
739 #pragma mark Menu Actions
740
741 - (void)getInfo:(NSMenuItem *)sender {
742     [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender];
743 }
744
745 - (void)deleteObject:(NSMenuItem *)sender {
746     PithosNode *node = (PithosNode *)[sender representedObject];
747     if (([node class] == [PithosObjectNode class]) || 
748         (([node class] == [PithosSubdirNode class]) && 
749          !node.pithosObject.subdir &&
750          [node.pithosObject.name hasSuffix:@"/"])) {
751         ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name 
752                                                                                                   objectName:node.pithosObject.name];
753         objectRequest.delegate = self;
754         objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
755         objectRequest.didFailSelector = @selector(deleteObjectFailed:);
756         [objectRequest startAsynchronous];
757     } else if ([node class] == [PithosSubdirNode class]) {
758         NSArray *objectRequests = [PithosFileUtilities deleteObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
759                                                                                            objectName:node.pithosObject.name];
760         if (objectRequests) {
761             for (ASIPithosObjectRequest *objectRequest in objectRequests) {
762                 objectRequest.delegate = self;
763                 objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
764                 objectRequest.didFailSelector = @selector(deleteObjectFailed:);
765                 [objectRequest startAsynchronous];
766             }
767         }
768         
769     }    
770 }
771
772 - (void)moveToTrash:(NSMenuItem *)sender {
773     PithosNode *node = (PithosNode *)[sender representedObject];
774     if (([node class] == [PithosObjectNode class]) || 
775         (([node class] == [PithosSubdirNode class]) && 
776          !node.pithosObject.subdir &&
777          [node.pithosObject.name hasSuffix:@"/"])) {
778         NSString *safeObjectName = [PithosFileUtilities safeObjectNameForContainerName:@"trash" 
779                                                                             objectName:node.pithosObject.name];
780         if (safeObjectName) {
781             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest moveObjectDataRequestWithContainerName:node.pithosContainer.name 
782                                                                                                         objectName:node.pithosObject.name 
783                                                                                                        contentType:nil 
784                                                                                                    contentEncoding:nil 
785                                                                                                 contentDisposition:nil 
786                                                                                                           manifest:nil 
787                                                                                                            sharing:nil 
788                                                                                                           isPublic:ASIPithosObjectRequestPublicIgnore 
789                                                                                                           metadata:nil 
790                                                                                           destinationContainerName:@"trash" 
791                                                                                              destinationObjectName:safeObjectName];
792             objectRequest.delegate = self;
793             objectRequest.didFinishSelector = @selector(moveToTrashFinished:);
794             objectRequest.didFailSelector = @selector(moveToTrashFailed:);
795             [objectRequest startAsynchronous];
796         }
797     } else if ([node class] == [PithosSubdirNode class]) {
798         NSString *safeObjectName = [PithosFileUtilities safeSubdirNameForContainerName:@"trash" 
799                                                                             subdirName:node.pithosObject.name];
800         if (safeObjectName) {
801             NSArray *objectRequests = [PithosFileUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
802                                                                                              objectName:node.pithosObject.name 
803                                                                                destinationContainerName:@"trash" 
804                                                                                   destinationObjectName:safeObjectName];
805             if (objectRequests) {
806                 for (ASIPithosObjectRequest *objectRequest in objectRequests) {
807                     objectRequest.delegate = self;
808                     objectRequest.didFinishSelector = @selector(moveToTrashFinished:);
809                     objectRequest.didFailSelector = @selector(moveToTrashFailed:);
810                     [objectRequest startAsynchronous];
811                 }
812             }
813         }        
814     }
815 }
816
817 #pragma mark -
818 #pragma mark Menu Actions ASIHTTPRequestDelegate
819
820 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
821     if (objectRequest.responseStatusCode == 204) {
822         NSLog(@"Object deleted: %@", [objectRequest url]);
823         [self refresh:nil];
824     } else {
825         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
826     }
827 }
828
829 - (void)deleteObjectFailed:(ASIPithosObjectRequest *)objectRequest {
830     NSLog(@"Delete of object failed");
831     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
832 }
833
834 - (void)moveToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
835     if (objectRequest.responseStatusCode == 201) {
836         NSLog(@"Object moved: %@", [objectRequest url]);
837         [self refresh:nil];
838     } else {
839         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
840     }
841 }
842
843 - (void)moveToTrashFailed:(ASIPithosObjectRequest *)objectRequest {
844     NSLog(@"Move of object failed");
845     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
846 }
847
848
849 @end