Initial implementation of drag and drop upload for files.
[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 : NSBrowserCell {}
55 @interface PithosBrowserCell : FileSystemBrowserCell {}
56 @end
57
58 @implementation PithosBrowserCell
59
60 - (id)init {
61     if ((self = [super init])) {
62         [self setLineBreakMode:NSLineBreakByTruncatingMiddle];
63     }
64     return self;
65 }
66
67 - (void)setObjectValue:(id)object {
68     if ([object isKindOfClass:[PithosNode class]]) {
69         PithosNode *node = (PithosNode *)object;
70         [self setStringValue:node.displayName];
71         [self setImage:node.icon];
72 //        // All cells are set as leafs because a branchingImage is already set!
73 //        // Maybe this cell is already inside an NSBrowserCell
74 //        [self setLeaf:YES];
75     } else {
76         [super setObjectValue:object];
77     }
78 }
79
80 @end
81
82 @interface PithosOutlineViewCell : ImageAndTextCell {}
83 @end
84
85 @implementation PithosOutlineViewCell
86
87 - (void)setObjectValue:(id)object {
88     if ([object isKindOfClass:[PithosNode class]]) {
89         PithosNode *node = (PithosNode *)object;
90         [self setStringValue:node.displayName];
91         [self setImage:node.icon];
92         [self setEditable:NO];
93     } else {
94         [super setObjectValue:object];
95     }
96 }
97
98 @end
99
100 @interface PithosBrowserController (Private) {}
101 - (void)resetContainers;
102 - (void)getInfo:(NSMenuItem *)sender;
103 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest;
104 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest;
105 - (void)uploadMissingHashesFinished:(ASIPithosObjectRequest *)objectRequest;
106 - (void)uploadMissingHashesFailed:(ASIPithosObjectRequest *)objectRequest;
107 @end
108
109 @implementation PithosBrowserController
110 @synthesize outlineViewDataSourceArray, splitView, outlineView, browser;
111
112 #pragma mark -
113 #pragma Object Lifecycle
114
115 - (id)init {
116     return [super initWithWindowNibName:@"PithosBrowserController"];
117 }
118
119 - (void)dealloc {
120     [[NSNotificationCenter defaultCenter] removeObserver:self];
121     [browserMenu release];
122     [sharedPreviewController release];
123     [outlineViewDataSourceArray release];
124     [accountNode release];
125     [rootNode release];
126     [super dealloc];
127 }
128
129 - (void)awakeFromNib {
130     [super awakeFromNib];
131     
132     [browser registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
133     [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
134     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
135     
136     [browser setCellClass:[PithosBrowserCell class]];
137     
138     browserMenu = [[NSMenu alloc] init];
139     [browserMenu setDelegate:self];
140     [browser setMenu:browserMenu];
141 }
142
143 - (void)resetContainers {
144     rootNode = nil;
145     [browser loadColumnZero];
146     self.outlineViewDataSourceArray = nil;
147     
148     // Create the outlineView tree
149     // CONTAINERS
150         NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
151                             [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]];
152 //    // CONTAINERS/pithos
153 //      [[containersTreeNode mutableChildNodes] addObject:
154 //     [NSTreeNode treeNodeWithRepresentedObject:
155 //      [[[PithosContainerNode alloc] initWithContainerName:@"pithos" 
156 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)]
157 //        ] autorelease]]];
158 //    // CONTAINERS/trash
159 //      [[containersTreeNode mutableChildNodes] addObject:
160 //     [NSTreeNode treeNodeWithRepresentedObject:
161 //      [[[PithosContainerNode alloc] initWithContainerName:@"trash"
162 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)]
163 //        ] autorelease]]];
164     // SHARED
165         NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
166                                       [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]];
167     // SHARED/my shared
168         [[sharedTreeNode mutableChildNodes] addObject:
169      [NSTreeNode treeNodeWithRepresentedObject:
170       [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" 
171                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]
172         ] autorelease]]];
173     // SHARED/others shared
174         [[sharedTreeNode mutableChildNodes] addObject:
175      [NSTreeNode treeNodeWithRepresentedObject:
176       [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared"
177                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]
178         ] autorelease]]];
179     
180     self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil];
181     
182         // Expand the folder outline view
183     [outlineView expandItem:nil expandChildren:YES];
184         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
185     
186     // Create accountNode and trigger a refresh
187     accountNode = [[PithosAccountNode alloc] init];
188     accountNode.children;
189 }
190
191 - (void)windowDidLoad {
192     [super windowDidLoad];
193     
194     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
195     
196     // Register for updates
197     [[NSNotificationCenter defaultCenter] addObserver:self 
198                                              selector:@selector(pithosNodeChildrenUpdated:) 
199                                                  name:@"PithosContainerNodeChildrenUpdated" 
200                                                object:nil];
201     [[NSNotificationCenter defaultCenter] addObserver:self 
202                                              selector:@selector(pithosNodeChildrenUpdated:) 
203                                                  name:@"PithosSubdirNodeChildrenUpdated" 
204                                                object:nil];
205     [[NSNotificationCenter defaultCenter] addObserver:self 
206                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
207                                                  name:@"PithosAccountNodeChildrenUpdated" 
208                                                object:nil];
209     [[NSNotificationCenter defaultCenter] addObserver:self 
210                                              selector:@selector(resetContainers) 
211                                                  name:@"PithosAuthenticationCredentialsUpdated" 
212                                                object:nil];
213 }
214
215 #pragma mark -
216 #pragma Observers
217
218 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
219     PithosNode *node = (PithosNode *)[notification object];
220     NSInteger lastColumn = [browser lastColumn];
221     for (NSInteger column = lastColumn; column >= 0; column--) {
222         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
223             [browser reloadColumn:column];
224             if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) {
225                 // This reloads the preview column
226                 [browser setLastColumn:column];
227                 [browser addColumn];
228             }
229             return;
230         }
231     }
232 }
233
234 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
235     BOOL containerPithosFound = NO;
236     BOOL containerTrashFound = NO;
237     //NSMutableArray *containersTreeNodeChildren = [[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes];
238     NSMutableArray *containersTreeNodeChildren = [NSMutableArray array];
239     for (PithosContainerNode *containerNode in accountNode.children) {
240         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
241             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
242             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:0];
243             containerPithosFound = YES;
244         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
245             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];            
246             NSUInteger insertIndex = 1;
247             if (!containerPithosFound)
248                 insertIndex = 0;
249             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:insertIndex];
250             containerTrashFound = YES;
251         } else {
252             [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]];
253         }
254     }
255     BOOL refreshAccountNode = NO;
256     if (!containerPithosFound) {
257         // create pithos
258         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
259         [containerRequest startSynchronous];
260         if ([containerRequest error]) {
261             NSLog(@"error:%@", [containerRequest error]);
262             // XXX do something on error
263         } else {
264             refreshAccountNode = YES;
265         }
266     }
267     if (!containerTrashFound) {
268         // create trash
269         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
270         [containerRequest startSynchronous];
271         if ([containerRequest error]) {
272             NSLog(@"error:%@", [containerRequest error]);
273             // XXX do something on error
274         } else {
275             refreshAccountNode = YES;
276         }
277     }
278     if (refreshAccountNode) {
279         [accountNode invalidateChildren];
280         accountNode.children;
281     } else {
282         [[[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes] setArray:containersTreeNodeChildren];
283         self.outlineViewDataSourceArray = outlineViewDataSourceArray;
284         
285         // Expand the folder outline view
286         [outlineView expandItem:nil expandChildren:YES];
287         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
288         
289         [self refresh:nil];
290     }
291 }
292
293 #pragma mark -
294 #pragma Actions
295
296 - (IBAction)refresh:(id)sender {
297     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
298         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
299     }
300     [browser validateVisibleColumns];
301 }
302
303 #pragma mark -
304 #pragma NSBrowserDelegate
305
306 - (id)rootItemForBrowser:(NSBrowser *)browser {
307     return rootNode;    
308 }
309
310 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
311     PithosNode *node = (PithosNode *)item;
312     return node.children.count;
313 }
314
315 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
316     PithosNode *node = (PithosNode *)item;
317     return [node.children objectAtIndex:index];
318 }
319
320 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
321     PithosNode *node = (PithosNode *)item;
322     return node.isLeafItem;
323 }
324
325 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
326     PithosNode *node = (PithosNode *)item;
327     return node;
328 }
329
330 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
331     if (sharedPreviewController == nil)
332         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
333     return sharedPreviewController;
334 }
335
336 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
337 //    if (!forUserResize) {
338 //        id item = [browser parentForItemsInColumn:columnIndex]; 
339 //        if ([self browser:browser isLeafItem:item]) {
340 //            suggestedWidth = 200; 
341 //        }
342 //    }
343 //    return suggestedWidth;
344 //}
345
346 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
347     return NO;
348 }
349
350 #pragma mark Drag and Drop source
351
352 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
353    toPasteboard:(NSPasteboard *)pasteboard {
354     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
355     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
356     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
357         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
358         [propertyList addObject:[node.pithosObject.name pathExtension]];
359     }
360
361     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
362     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
363     
364     return YES;
365 }
366
367 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
368 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
369     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
370     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
371     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
372         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
373         
374         // If the node is a subdir ask if the whole tree should be downloaded
375         if (node.pithosObject.subdir) {
376             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
377             [alert setMessageText:@"Download directory"];
378             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
379             [alert addButtonWithTitle:@"OK"];
380             [alert addButtonWithTitle:@"Cancel"];
381             NSInteger choice = [alert runModal];
382             if (choice == NSAlertFirstButtonReturn) {
383                 NSArray *objectRequests = [PithosFileUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
384                                                                                                  objectName:node.pithosObject.name 
385                                                                                                 toDirectory:[dropDestination path] 
386                                                                                               checkIfExists:YES];
387                 if (objectRequests) {
388                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
389                         [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
390                         // XXX set delegates and queue
391                         [objectRequest setCompletionBlock:^{
392                             NSLog(@"dl completed: %@", [objectRequest url]);
393                         }];
394                         [objectRequest setFailedBlock:^{
395                             NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
396                         }];
397                         [objectRequest startAsynchronous];
398                     }
399                 }
400             }
401         } else {
402             ASIPithosObjectRequest *objectRequest = [PithosFileUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
403                                                                                                  objectName:node.pithosObject.name 
404                                                                                                 toDirectory:[dropDestination path] 
405                                                                                               checkIfExists:YES];
406             if (objectRequest) {
407                 [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
408                 // XXX set delegates and queue
409                 [objectRequest setCompletionBlock:^{
410                     NSLog(@"dl completed: %@", [objectRequest url]);
411                 }];
412                 [objectRequest setFailedBlock:^{
413                     NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
414                 }];
415                 [objectRequest startAsynchronous];
416             }
417         }
418     }
419     return names;
420 }
421
422 #pragma mark Drag and Drop destination
423
424 - (NSDragOperation)browser:aBrowser 
425               validateDrop:(id<NSDraggingInfo>)info 
426                proposedRow:(NSInteger *)row 
427                     column:(NSInteger *)column 
428              dropOperation:(NSBrowserDropOperation *)dropOperation {
429     NSDragOperation result = NSDragOperationNone;
430     // Files from the finder are accepted
431     if ([[[info draggingPasteboard] types] indexOfObject:NSFilenamesPboardType] != -1) {
432         // For a between drop, we let the user drop "on" the parent item
433         if (*dropOperation == NSBrowserDropAbove)
434             *row = -1;
435         // Only allow dropping in folders
436         if (*column != -1) {
437             if (*row != -1) {
438                 PithosNode *node = [browser itemAtRow:*row inColumn:*column];
439                 if ([node class] != [PithosSubdirNode class])
440                     *row = -1;
441             *dropOperation = NSBrowserDropOn;
442             result = NSDragOperationCopy;
443             }
444         }
445     }
446     // XXX else local file promises
447     return result;
448 }
449
450 - (BOOL)browser:(NSBrowser *)aBrowser 
451      acceptDrop:(id<NSDraggingInfo>)info 
452           atRow:(NSInteger)row 
453          column:(NSInteger)column 
454   dropOperation:(NSBrowserDropOperation)dropOperation {
455     NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
456     NSLog(@"drag in filenames: %@", filenames);
457     PithosNode *node = nil;
458     if ((column != -1) && (filenames != nil)) {
459         if (row != -1)
460             node = [browser itemAtRow:row inColumn:column];
461         else
462             node = [browser parentForItemsInColumn:column];
463         NSLog(@"drag in node: %@", node.url);
464         if (([node class] != [PithosSubdirNode class]) && ([node class] != [PithosContainerNode class]))
465             return NO;
466         
467         NSFileManager *defaultManager = [NSFileManager defaultManager];
468         NSString *containerName = [NSString stringWithString:node.pithosContainer.name];
469         NSString *objectNamePrefix;
470         if ([node class] == [PithosSubdirNode class])
471             objectNamePrefix = [NSString stringWithString:node.pithosObject.name];
472         else
473             objectNamePrefix = [NSString stringWithString:@""];
474         NSUInteger blockSize = node.pithosContainer.blockSize;
475         NSString *blockHash = node.pithosContainer.blockHash;
476         
477         for (NSString *filePath in filenames) {
478             BOOL isDirectory;
479             if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
480                 if (!isDirectory) {
481                     // Upload file
482                     NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
483                     // XXX request
484                     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
485                     dispatch_async(queue, ^{
486                         NSError *error = nil;
487                         NSURLResponse *response = nil;
488                         [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath] 
489                                                                                  cachePolicy:NSURLCacheStorageNotAllowed 
490                                                                              timeoutInterval:.1] 
491                                               returningResponse:&response 
492                                                           error:&error];
493                         NSString *contentType = [response MIMEType];
494                         if (contentType == nil)
495                             contentType = @"application/binary";
496                         if (error)
497                             NSLog(@"contentType detection error: %@", error);
498                         NSArray *hashes = nil;
499                         ASIPithosObjectRequest *objectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:containerName 
500                                                                                                                   objectName:objectName 
501                                                                                                                  contentType:contentType 
502                                                                                                                    blockSize:blockSize 
503                                                                                                                    blockHash:blockHash 
504                                                                                                                      forFile:filePath 
505                                                                                                                checkIfExists:YES 
506                                                                                                                       hashes:&hashes];
507                         if (objectRequest) {
508                             // XXX set delegates and queue
509                             objectRequest.delegate = self;
510                             objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
511                             objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
512                             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
513                                                       containerName, @"containerName", 
514                                                       objectName, @"objectName", 
515                                                       contentType, @"contentType", 
516                                                       [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
517                                                       blockHash, @"blockHash", 
518                                                       filePath, @"filePath", 
519                                                       hashes, @"hashes", 
520                                                       [NSNumber numberWithBool:YES], @"checkIfExists", 
521                                                       node, @"node", 
522                                                       [NSNumber numberWithUnsignedInteger:0], @"iteration", 
523                                                       nil];
524                             [objectRequest startAsynchronous];
525                         }
526                         // XXX else show alert?
527                     });
528                 } else {
529                     // Upload directory, confirm first
530                     // XXX implement this
531                 }
532             }
533             
534         }
535         return YES;
536     }
537
538     return NO;
539 }
540
541 #pragma mark -
542 #pragma mark ASIHTTPRequestDelegate
543
544 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {    
545     NSLog(@"upload using hashmap completed: %@", [objectRequest url]);
546     if (objectRequest.responseStatusCode == 201) {
547         NSLog(@"object created: %@", [objectRequest url]);
548         PithosNode *node = [objectRequest.userInfo objectForKey:@"node"];
549         [node invalidateChildren];
550         node.children;
551     } else if (objectRequest.responseStatusCode == 409) {
552         NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue] + 1;
553         if (iteration > 10) {
554             NSLog(@"upload iteration limit reached: %@", [objectRequest url]);
555             // XXX show alert
556             return;
557         }
558         NSLog(@"object is missing hashes: %@", [objectRequest url]);
559         ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
560                                                                                                       objectName:@".upload" 
561                                                                                                      contentType:[objectRequest.userInfo objectForKey:@"contentType"]
562                                                                                                        blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
563                                                                                                          forFile:[objectRequest.userInfo objectForKey:@"filePath"]
564                                                                                                           hashes:[objectRequest.userInfo objectForKey:@"hashes"] 
565                                                                                            missingHashesResponse:[objectRequest responseString] 
566                                                                                                    checkIfExists:[[objectRequest.userInfo objectForKey:@"checkIfExists"] boolValue]];
567         newObjectRequest.shouldAttemptPersistentConnection = NO;
568         newObjectRequest.delegate = self;
569         newObjectRequest.didFinishSelector = @selector(uploadMissingHashesFinished:);
570         newObjectRequest.didFailSelector = @selector(uploadMissingHashesFailed:);
571         newObjectRequest.userInfo = objectRequest.userInfo;
572         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithBool:NO] forKey:@"checkIfExists"];
573         [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:iteration] forKey:@"iteration"];
574         [newObjectRequest startAsynchronous];
575     } else {
576         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
577         [alert setMessageText:@"Unexpected Response Status"];
578         [alert setInformativeText:[NSString stringWithFormat:@"Unexpected response status %d - %@", objectRequest.responseStatusCode, objectRequest.responseStatusMessage]];
579         [alert addButtonWithTitle:@"OK"];
580         [alert runModal];
581     }
582 }
583
584 - (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
585     NSLog(@"upload failed: %@, error: %@", [objectRequest url], [objectRequest error]);
586     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
587     [alert setMessageText:@"HTTP Request Error"];
588     [alert setInformativeText:[NSString stringWithFormat:@"An error occured: %@", [objectRequest error]]];
589     [alert addButtonWithTitle:@"OK"];
590     [alert runModal];
591 }
592
593 - (void)uploadMissingHashesFinished:(ASIPithosObjectRequest *)objectRequest {
594     NSLog(@"upload of missing hashes completed: %@", [objectRequest url]);
595     if ((objectRequest.responseStatusCode == 201) || (objectRequest.responseStatusCode == 204)) {
596         NSArray *hashes = [objectRequest.userInfo objectForKey:@"hashes"];
597         ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
598                                                                                                      objectName:[objectRequest.userInfo objectForKey:@"objectName"] 
599                                                                                                     contentType:[objectRequest.userInfo objectForKey:@"contentType"] 
600                                                                                                       blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] 
601                                                                                                       blockHash:[objectRequest.userInfo objectForKey:@"blockHash"]
602                                                                                                         forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
603                                                                                                   checkIfExists:NO 
604                                                                                                          hashes:&hashes];
605         newObjectRequest.delegate = self;
606         newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
607         newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
608         newObjectRequest.userInfo = objectRequest.userInfo;
609         [newObjectRequest startAsynchronous];
610     } else {
611         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
612         [alert setMessageText:@"Unexpected Response Status"];
613         [alert setInformativeText:[NSString stringWithFormat:@"Unexpected response status %d - %@", objectRequest.responseStatusCode, objectRequest.responseStatusMessage]];
614         [alert addButtonWithTitle:@"OK"];
615         [alert runModal];
616     }
617 }
618
619 - (void)uploadMissingHashesFailed:(ASIPithosObjectRequest *)objectRequest {    
620     NSLog(@"upload of missing hashes failed: %@, error: %@", [objectRequest url], [objectRequest error]);
621     NSAlert *alert = [[[NSAlert alloc] init] autorelease];
622     [alert setMessageText:@"HTTP Request Error"];
623     [alert setInformativeText:[NSString stringWithFormat:@"An error occured: %@", [objectRequest error]]];
624     [alert addButtonWithTitle:@"OK"];
625     [alert runModal];
626 }
627
628 #pragma mark -
629 #pragma mark NSSplitViewDelegate
630
631 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
632     return 120;
633 }
634
635 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
636     return 220;
637 }
638
639 #pragma mark -
640 #pragma mark NSOutlineViewDelegate
641
642 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
643     return ([[item representedObject] isLeaf]);
644 }
645
646 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
647         return (![[item representedObject] isLeaf]);
648 }
649
650 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
651     PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject];
652     if (node) {
653         rootNode = node;
654         [browser loadColumnZero];
655     }
656 }
657
658 #pragma mark -
659 #pragma mark NSMenuDelegate
660
661 - (void)menuNeedsUpdate:(NSMenu *)menu {
662     NSInteger column = [browser clickedColumn];
663     NSInteger row = [browser clickedRow];
664     [menu removeAllItems];
665     if ((column == -1) || (row == -1)) {
666         // General context menu has 0
667     } else {
668         // PithosNode menu has 1 items
669         // Get Info
670         NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""];
671         [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
672         [menu addItem:menuItem];
673     }
674 }
675
676 #pragma mark -
677 #pragma mark Menu Actions
678
679 - (void)getInfo:(NSMenuItem *)sender {
680     [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender];
681 }
682
683 @end