Minor fixes and changes.
[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                             NSMutableArray *directoryObjectRequests = nil;
514                             NSArray *objectRequests = [PithosFileUtilities writeObjectDataRequestsWithContainerName:containerName 
515                                                                                                          objectName:objectName 
516                                                                                                           blockSize:blockSize 
517                                                                                                           blockHash:blockHash 
518                                                                                                        forDirectory:filePath 
519                                                                                                       checkIfExists:YES 
520                                                                                                         objectNames:&objectNames
521                                                                                                        contentTypes:&contentTypes
522                                                                                                           filePaths:&filePaths
523                                                                                                          hashesArrays:&hashesArrays 
524                                                                                             directoryObjectRequests:&directoryObjectRequests];
525                             for (ASIPithosObjectRequest *objectRequest in directoryObjectRequests) {
526                                 objectRequest.delegate = self;
527                                 objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
528                                 objectRequest.didFailSelector = @selector(uploadDirectoryObjectFailed:);
529                                 [objectRequest startAsynchronous];
530                             }
531                             if (objectRequests) {
532                                 for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) {
533                                     ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i];
534                                     objectRequest.delegate = self;
535                                     objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
536                                     objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
537                                     objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
538                                                               containerName, @"containerName", 
539                                                               [objectNames objectAtIndex:i], @"objectName", 
540                                                               [contentTypes objectAtIndex:i], @"contentType", 
541                                                               [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
542                                                               blockHash, @"blockHash", 
543                                                               [filePaths objectAtIndex:i], @"filePath", 
544                                                               [hashesArrays objectAtIndex:i], @"hashes", 
545                                                               [NSNull null], @"node", 
546                                                               [NSNumber numberWithUnsignedInteger:10], @"iteration", 
547                                                               nil];
548                                     [objectRequest startAsynchronous];
549                                 }
550                             }
551                         });
552                     }
553                 }
554             }
555             
556         }
557         return YES;
558     }
559
560     return NO;
561 }
562
563 #pragma mark -
564 #pragma mark ASIHTTPRequestDelegate
565
566 - (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
567     NSLog(@"Download completed: %@", [objectRequest url]);
568     if (objectRequest.responseStatusCode == 200) {
569         if ([objectRequest contentLength] == 0) {
570             NSLog(@"Downloaded  0 bytes");
571             NSFileManager *defaultManager = [NSFileManager defaultManager];
572             NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
573             if (![defaultManager fileExistsAtPath:filePath]) {
574                 if (![defaultManager createFileAtPath:filePath contents:nil attributes:nil]) {
575                     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
576                     [alert setMessageText:@"Create File Error"];
577                     [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]];
578                     [alert addButtonWithTitle:@"OK"];
579                     [alert runModal];
580                 }
581             }
582         }
583     } else {
584         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
585     }
586 }
587
588 - (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest {
589     NSLog(@"Download failed");
590     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
591 }
592
593 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
594     NSLog(@"Upload directory object completed: %@", [objectRequest url]);
595     if (objectRequest.responseStatusCode == 201) {
596         NSLog(@"Directory object created: %@", [objectRequest url]);
597         [self refresh:nil];
598     } else {
599         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
600     }
601 }
602
603 - (void)uploadDirectoryObjectFailed:(ASIPithosObjectRequest *)objectRequest {
604     NSLog(@"Upload directory object failed");
605     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
606 }
607
608 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
609     NSLog(@"Upload using hashmap completed: %@", [objectRequest url]);
610     if (objectRequest.responseStatusCode == 201) {
611         NSLog(@"Object created: %@", [objectRequest url]);
612         PithosNode *node = [objectRequest.userInfo objectForKey:@"node"];
613         if (node != (id)[NSNull null]) {
614             [node.parent invalidateChildren];
615             node.parent.children;
616         } else {
617             [self refresh:nil];
618         }
619     } else if (objectRequest.responseStatusCode == 409) {
620         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue] - 1;
621         if (iteration == 0) {
622             NSLog(@"Upload iteration limit reached: %@", [objectRequest url]);
623             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
624             [alert setMessageText:@"Upload Timeout"];
625             [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", 
626                                        [objectRequest.userInfo objectForKey:@"objectName"], [objectRequest.userInfo objectForKey:@"containerName"]]];
627             [alert addButtonWithTitle:@"OK"];
628             [alert runModal];
629             return;
630         }
631         NSLog(@"object is missing hashes: %@", [objectRequest url]);
632         NSIndexSet *missingBlocks = [PithosFileUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
633                                                       withMissingHashesResponse:[objectRequest responseString]];
634         NSUInteger missingBlockIndex = [missingBlocks firstIndex];
635         ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
636                                                                                                       objectName:@".upload" 
637                                                                                                        blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
638                                                                                                          forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
639                                                                                                missingBlockIndex:missingBlockIndex];
640         newObjectRequest.delegate = self;
641         newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
642         newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
643         newObjectRequest.userInfo = objectRequest.userInfo;
644         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:iteration] forKey:@"iteration"];
645         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
646         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
647         [newObjectRequest startAsynchronous];
648     } else {
649         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
650     }
651 }
652
653 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
654     NSLog(@"Upload using hashmap failed");
655     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
656 }
657
658 - (void)uploadMissingBlockFinished:(ASIPithosObjectRequest *)objectRequest {
659     NSLog(@"Upload of missing block completed: %@", [objectRequest url]);
660     if (objectRequest.responseStatusCode == 201) {
661         NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
662         NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
663         missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
664         if (missingBlockIndex == NSNotFound) {
665             NSArray *hashes = [objectRequest.userInfo objectForKey:@"hashes"];
666             ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
667                                                                                                          objectName:[objectRequest.userInfo objectForKey:@"objectName"] 
668                                                                                                         contentType:[objectRequest.userInfo objectForKey:@"contentType"] 
669                                                                                                           blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] 
670                                                                                                           blockHash:[objectRequest.userInfo objectForKey:@"blockHash"]
671                                                                                                             forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
672                                                                                                       checkIfExists:NO 
673                                                                                                              hashes:&hashes];
674             newObjectRequest.delegate = self;
675             newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
676             newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
677             newObjectRequest.userInfo = objectRequest.userInfo;
678             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
679             [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
680             [newObjectRequest startAsynchronous];
681         } else {
682             ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
683                                                                                                           objectName:@".upload" 
684                                                                                                            blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
685                                                                                                              forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
686                                                                                                    missingBlockIndex:missingBlockIndex];
687             newObjectRequest.delegate = self;
688             newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
689             newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:);
690             newObjectRequest.userInfo = objectRequest.userInfo;
691             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
692             [newObjectRequest startAsynchronous];
693         }
694     } else {
695         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
696     }
697 }
698
699 - (void)uploadMissingBlockFailed:(ASIPithosObjectRequest *)objectRequest {
700     NSLog(@"Upload of missing block failed");
701     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
702 }
703
704 #pragma mark -
705 #pragma mark NSSplitViewDelegate
706
707 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
708     return 120;
709 }
710
711 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
712     return 220;
713 }
714
715 #pragma mark -
716 #pragma mark NSOutlineViewDelegate
717
718 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
719     return ([[item representedObject] isLeaf]);
720 }
721
722 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
723         return (![[item representedObject] isLeaf]);
724 }
725
726 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
727     PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject];
728     if (node) {
729         rootNode = node;
730         [browser loadColumnZero];
731         [self refresh:nil];                                  
732     }
733 }
734
735 #pragma mark -
736 #pragma mark NSMenuDelegate
737
738 - (void)menuNeedsUpdate:(NSMenu *)menu {
739     NSInteger column = [browser clickedColumn];
740     NSInteger row = [browser clickedRow];
741     [menu removeAllItems];
742     NSMenuItem *menuItem;
743     if ((column == -1) || (row == -1)) {
744         // General context menu
745     } else {
746         // Move to Trash (pithos container only)
747         // Delete
748         if ([rootNode class] == [PithosContainerNode class]) {
749             if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) {
750                 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(moveToTrash:) keyEquivalent:@""] autorelease];
751                 [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
752                 [menu addItem:menuItem];
753             }
754             menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteObject:) keyEquivalent:@""] autorelease];
755             [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
756             [menu addItem:menuItem];
757             [menu addItem:[NSMenuItem separatorItem]];
758         }
759         // Get Info
760         menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""] autorelease];
761         [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
762         [menu addItem:menuItem];
763     }
764 }
765
766 #pragma mark -
767 #pragma mark Menu Actions
768
769 - (void)getInfo:(NSMenuItem *)sender {
770     [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender];
771 }
772
773 - (void)deleteObject:(NSMenuItem *)sender {
774     PithosNode *node = (PithosNode *)[sender representedObject];
775     if (([node class] == [PithosObjectNode class]) || 
776         (([node class] == [PithosSubdirNode class]) && 
777          !node.pithosObject.subdir &&
778          [node.pithosObject.name hasSuffix:@"/"])) {
779         ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name 
780                                                                                                   objectName:node.pithosObject.name];
781         objectRequest.delegate = self;
782         objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
783         objectRequest.didFailSelector = @selector(deleteObjectFailed:);
784         [objectRequest startAsynchronous];
785     } else if ([node class] == [PithosSubdirNode class]) {
786         NSArray *objectRequests = [PithosFileUtilities deleteObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
787                                                                                            objectName:node.pithosObject.name];
788         if (objectRequests) {
789             for (ASIPithosObjectRequest *objectRequest in objectRequests) {
790                 objectRequest.delegate = self;
791                 objectRequest.didFinishSelector = @selector(deleteObjectFinished:);
792                 objectRequest.didFailSelector = @selector(deleteObjectFailed:);
793                 [objectRequest startAsynchronous];
794             }
795         }
796         
797     }    
798 }
799
800 - (void)moveToTrash:(NSMenuItem *)sender {
801     PithosNode *node = (PithosNode *)[sender representedObject];
802     if (([node class] == [PithosObjectNode class]) || 
803         (([node class] == [PithosSubdirNode class]) && 
804          !node.pithosObject.subdir &&
805          [node.pithosObject.name hasSuffix:@"/"])) {
806         NSString *safeObjectName = [PithosFileUtilities safeObjectNameForContainerName:@"trash" 
807                                                                             objectName:node.pithosObject.name];
808         if (safeObjectName) {
809             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest moveObjectDataRequestWithContainerName:node.pithosContainer.name 
810                                                                                                         objectName:node.pithosObject.name 
811                                                                                                        contentType:nil 
812                                                                                                    contentEncoding:nil 
813                                                                                                 contentDisposition:nil 
814                                                                                                           manifest:nil 
815                                                                                                            sharing:nil 
816                                                                                                           isPublic:ASIPithosObjectRequestPublicIgnore 
817                                                                                                           metadata:nil 
818                                                                                           destinationContainerName:@"trash" 
819                                                                                              destinationObjectName:safeObjectName];
820             objectRequest.delegate = self;
821             objectRequest.didFinishSelector = @selector(moveToTrashFinished:);
822             objectRequest.didFailSelector = @selector(moveToTrashFailed:);
823             [objectRequest startAsynchronous];
824         }
825     } else if ([node class] == [PithosSubdirNode class]) {
826         NSString *safeObjectName = [PithosFileUtilities safeSubdirNameForContainerName:@"trash" 
827                                                                             subdirName:node.pithosObject.name];
828         if (safeObjectName) {
829             NSArray *objectRequests = [PithosFileUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name 
830                                                                                              objectName:node.pithosObject.name 
831                                                                                destinationContainerName:@"trash" 
832                                                                                   destinationObjectName:safeObjectName];
833             if (objectRequests) {
834                 for (ASIPithosObjectRequest *objectRequest in objectRequests) {
835                     objectRequest.delegate = self;
836                     objectRequest.didFinishSelector = @selector(moveToTrashFinished:);
837                     objectRequest.didFailSelector = @selector(moveToTrashFailed:);
838                     [objectRequest startAsynchronous];
839                 }
840             }
841         }        
842     }
843 }
844
845 #pragma mark -
846 #pragma mark Menu Actions ASIHTTPRequestDelegate
847
848 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
849     if (objectRequest.responseStatusCode == 204) {
850         NSLog(@"Object deleted: %@", [objectRequest url]);
851         [self refresh:nil];
852     } else {
853         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
854     }
855 }
856
857 - (void)deleteObjectFailed:(ASIPithosObjectRequest *)objectRequest {
858     NSLog(@"Delete of object failed");
859     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
860 }
861
862 - (void)moveToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
863     if (objectRequest.responseStatusCode == 201) {
864         NSLog(@"Object moved: %@", [objectRequest url]);
865         [self refresh:nil];
866     } else {
867         [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest];
868     }
869 }
870
871 - (void)moveToTrashFailed:(ASIPithosObjectRequest *)objectRequest {
872     NSLog(@"Move of object failed");
873     [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest];
874 }
875
876
877 @end