Drag and drop download fixes.
[pithos-macos] / pithos-macos / PithosBrowserController.m
index f5d2738..64916ba 100644 (file)
@@ -1,4 +1,4 @@
-    //
+//
 //  PithosBrowserController.m
 //  pithos-macos
 //
 // or implied, of GRNET S.A.
 
 #import "PithosBrowserController.h"
-#import "ASIPithosRequest.h"
+#import "PithosNode.h"
 #import "PithosAccountNode.h"
 #import "PithosContainerNode.h"
+#import "PithosSubdirNode.h"
+#import "PithosObjectNode.h"
 #import "PithosEmptyNode.h"
 #import "ImageAndTextCell.h"
 #import "FileSystemBrowserCell.h"
+#import "ASIPithosRequest.h"
+#import "ASIPithosContainerRequest.h"
+#import "ASIPithosObjectRequest.h"
+#import "ASIPithosContainer.h"
+#import "ASIPithosObject.h"
+#import "PithosFileUtilities.h"
 
 //@interface PithosBrowserCell : NSBrowserCell {}
 @interface PithosBrowserCell : FileSystemBrowserCell {}
 @end
 
 @interface PithosBrowserController (Private) {}
-- (void)authenticateWithAuthUser:(NSString *)authUser authToken:(NSString *)authToken;
 - (void)resetContainers;
+- (void)getInfo:(NSMenuItem *)sender;
+- (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest;
+- (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest;
+- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest;
+- (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest;
+- (void)uploadMissingHashesFinished:(ASIPithosObjectRequest *)objectRequest;
+- (void)uploadMissingHashesFailed:(ASIPithosObjectRequest *)objectRequest;
 @end
 
 @implementation PithosBrowserController
-@synthesize userDefaultsController, outlineViewDataSourceArray, splitView, outlineView, browser;
-@synthesize authenticationPanel, authenticationUserTextField, authenticationTokenTextField, authenticationRenewCheckBox, 
-            authenticationCancelPushButton, authenticationManualPushButton, authenticationLoginPushButton;
+@synthesize outlineViewDataSourceArray, splitView, outlineView, browser;
 
 #pragma mark -
 #pragma Object Lifecycle
 
 - (void)dealloc {
     [[NSNotificationCenter defaultCenter] removeObserver:self];
+    [browserMenu release];
     [sharedPreviewController release];
     [outlineViewDataSourceArray release];
+    [accountNode release];
     [rootNode release];
-    [authenticationLoginPushButton release];
-    [authenticationManualPushButton release];
-    [authenticationCancelPushButton release];
-    [authenticationRenewCheckBox release];
-    [authenticationTokenTextField release];
-    [authenticationUserTextField release];
-    [authenticationPanel release];
-    [browser release];
-    [splitView release];
-    [outlineView release];
-    [userDefaultsController release];
     [super dealloc];
 }
 
 - (void)awakeFromNib {
+    [super awakeFromNib];
+    
+    [browser registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
+    [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
+    [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
+    
     [browser setCellClass:[PithosBrowserCell class]];
+    
+    browserMenu = [[NSMenu alloc] init];
+    [browserMenu setDelegate:self];
+    [browser setMenu:browserMenu];
 }
 
 - (void)resetContainers {
     self.outlineViewDataSourceArray = nil;
     
     // Create the outlineView tree
-       NSTreeNode *treeNode = [NSTreeNode treeNodeWithRepresentedObject:
+    // CONTAINERS
+       NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
                             [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]];
-       [[treeNode mutableChildNodes] addObject:
+//    // CONTAINERS/pithos
+//     [[containersTreeNode mutableChildNodes] addObject:
+//     [NSTreeNode treeNodeWithRepresentedObject:
+//      [[[PithosContainerNode alloc] initWithContainerName:@"pithos" 
+//                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)]
+//        ] autorelease]]];
+//    // CONTAINERS/trash
+//     [[containersTreeNode mutableChildNodes] addObject:
+//     [NSTreeNode treeNodeWithRepresentedObject:
+//      [[[PithosContainerNode alloc] initWithContainerName:@"trash"
+//                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)]
+//        ] autorelease]]];
+    // SHARED
+       NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
+                                      [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]];
+    // SHARED/my shared
+       [[sharedTreeNode mutableChildNodes] addObject:
      [NSTreeNode treeNodeWithRepresentedObject:
-      [[[PithosContainerNode alloc] initWithContainerName:@"pithos"] autorelease]]];
-       [[treeNode mutableChildNodes] addObject:
+      [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" 
+                                               icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]
+        ] autorelease]]];
+    // SHARED/others shared
+       [[sharedTreeNode mutableChildNodes] addObject:
      [NSTreeNode treeNodeWithRepresentedObject:
-      [[[PithosContainerNode alloc] initWithContainerName:@"trash"] autorelease]]];
+      [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared"
+                                               icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]
+        ] autorelease]]];
     
-    self.outlineViewDataSourceArray = [NSMutableArray arrayWithObject:treeNode];
+    self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil];
     
        // Expand the folder outline view
     [outlineView expandItem:nil expandChildren:YES];
        [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
+    
+    // Create accountNode and trigger a refresh
+    accountNode = [[PithosAccountNode alloc] init];
+    accountNode.children;
 }
 
 - (void)windowDidLoad {
-//    [userDefaultsController setAppliesImmediately:NO];
+    [super windowDidLoad];
     
     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
     
     // Register for updates
     [[NSNotificationCenter defaultCenter] addObserver:self 
                                              selector:@selector(pithosNodeChildrenUpdated:) 
-                                                 name:@"PithosNodeChildrenUpdated" 
+                                                 name:@"PithosContainerNodeChildrenUpdated" 
+                                               object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self 
+                                             selector:@selector(pithosNodeChildrenUpdated:) 
+                                                 name:@"PithosSubdirNodeChildrenUpdated" 
+                                               object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self 
+                                             selector:@selector(pithosAccountNodeChildrenUpdated:) 
+                                                 name:@"PithosAccountNodeChildrenUpdated" 
+                                               object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self 
+                                             selector:@selector(resetContainers) 
+                                                 name:@"PithosAuthenticationCredentialsUpdated" 
                                                object:nil];
-
-    [self authenticateWithAuthUser:[authenticationUserTextField stringValue] authToken:[authenticationTokenTextField stringValue]];
 }
 
 #pragma mark -
 #pragma Observers
 
 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
-    if ([[browser parentForItemsInColumn:[browser lastColumn]] isEqualTo:[notification object]]) 
-        [browser reloadColumn:[browser lastColumn]];
-}
-
-#pragma mark -
-#pragma Actions
-
-- (IBAction)refresh:(id)sender {
-    [[browser parentForItemsInColumn:[browser lastColumn]] invalidateChildren];
-    [browser reloadColumn:[browser lastColumn]];
-}
-
-#pragma mark -
-#pragma Authentication
-
-- (void)authenticateFromURLWithAuthUser:(NSString *)authUser authToken:(NSString *)authToken {
-    if ([authUser length] && [authToken length]) {
-        [authenticationUserTextField setStringValue:authUser];
-        [authenticationTokenTextField setStringValue:authToken];
-        [userDefaultsController save:self];
-        [self authenticateWithAuthUser:authUser authToken:authToken];
+    PithosNode *node = (PithosNode *)[notification object];
+    NSInteger lastColumn = [browser lastColumn];
+    for (NSInteger column = lastColumn; column >= 0; column--) {
+        if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
+            [browser reloadColumn:column];
+            if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) {
+                // This reloads the preview column
+                [browser setLastColumn:column];
+                [browser addColumn];
+            }
+            return;
+        }
     }
-    // XXX else maybe an error message?
 }
 
-- (void)authenticateWithAuthUser:(NSString *)authUser authToken:(NSString *)authToken {
-    // XXX hardcoded for now
-    NSString *storageURLPrefix = @"https://pithos.dev.grnet.gr/v1/";
-    
-    NSLog(@"Authentication - storageURLPrefix:%@, authUser:%@, authToken:%@", storageURLPrefix, authUser, authToken);
-    if ([authUser length] && [authToken length]) {
-    //if (authUser && ([authUser length] > 0) && authToken && ([authToken length] > 0)) {
-        [ASIPithosRequest setStorageURL:[storageURLPrefix stringByAppendingString:authUser]];
-        [ASIPithosRequest setAuthToken:authToken];
-        [self resetContainers];
+- (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
+    BOOL containerPithosFound = NO;
+    BOOL containerTrashFound = NO;
+    //NSMutableArray *containersTreeNodeChildren = [[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes];
+    NSMutableArray *containersTreeNodeChildren = [NSMutableArray array];
+    for (PithosContainerNode *containerNode in accountNode.children) {
+        if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
+            containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
+            [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:0];
+            containerPithosFound = YES;
+        } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
+            containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];            
+            NSUInteger insertIndex = 1;
+            if (!containerPithosFound)
+                insertIndex = 0;
+            [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:insertIndex];
+            containerTrashFound = YES;
+        } else {
+            [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]];
+        }
+    }
+    BOOL refreshAccountNode = NO;
+    if (!containerPithosFound) {
+        // create pithos
+        ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
+        [containerRequest startSynchronous];
+        if ([containerRequest error]) {
+            NSLog(@"error:%@", [containerRequest error]);
+            // XXX do something on error
+        } else {
+            refreshAccountNode = YES;
+        }
+    }
+    if (!containerTrashFound) {
+        // create trash
+        ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
+        [containerRequest startSynchronous];
+        if ([containerRequest error]) {
+            NSLog(@"error:%@", [containerRequest error]);
+            // XXX do something on error
+        } else {
+            refreshAccountNode = YES;
+        }
+    }
+    if (refreshAccountNode) {
+        [accountNode invalidateChildren];
+        accountNode.children;
     } else {
-        [self authenticationSelect:nil];
+        [[[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes] setArray:containersTreeNodeChildren];
+        self.outlineViewDataSourceArray = outlineViewDataSourceArray;
+        
+        // Expand the folder outline view
+        [outlineView expandItem:nil expandChildren:YES];
+        [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
+        
+        [self refresh:nil];
     }
 }
 
-- (IBAction)authenticationSelect:(id)sender {
-       [self.window makeFirstResponder:nil];
-       [NSApp beginSheet:authenticationPanel
-          modalForWindow:self.window
-        modalDelegate:self
-          didEndSelector:NULL
-                 contextInfo:nil];
-}
-
-- (IBAction)authenticationLogin:(id)sender {
-       [NSApp endSheet:authenticationPanel];
-       [authenticationPanel orderOut:self];
-    // XXX hardcoded for now
-    NSProcessInfo *processInfo = [NSProcessInfo processInfo];
-    NSString *loginURL = [NSString stringWithFormat:@"https://pithos.dev.grnet.gr/login?next=pithos://%@_%d",
-                          [processInfo processName], [processInfo processIdentifier]];
-    if ([authenticationRenewCheckBox state] == NSOnState)
-        loginURL = [loginURL stringByAppendingString:@"&renew="];
-    NSLog(@"loginURL: %@", loginURL);
-    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:loginURL]];
-    // XXX Should we wait for results or do something else?
-    // XXX check the case where this happens for the first time
-    // XXX maybe don't remove the Panel, and let the handler do it
-}
-
-- (IBAction)authenticationCancel:(id)sender {
-       [NSApp endSheet:authenticationPanel];
-       [authenticationPanel orderOut:self];
-    [userDefaultsController revert:sender];
-}
+#pragma mark -
+#pragma Actions
 
-- (IBAction)authenticationManual:(id)sender {
-       [NSApp endSheet:authenticationPanel];
-       [authenticationPanel orderOut:self];
-    [userDefaultsController save:sender];
-    // Because of delayed saves of the userDefaultsController, we use the TextField values directly, instead of 
-    //NSString *authUser = [[userDefaultsController values] valueForKey:@"authUser"];
-    //NSString *authToken = [[userDefaultsController values] valueForKey:@"authToken"];    
-    [self authenticateWithAuthUser:[authenticationUserTextField stringValue] authToken:[authenticationTokenTextField stringValue]];
+- (IBAction)refresh:(id)sender {
+    for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
+        [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
+    }
+    [browser validateVisibleColumns];
 }
 
 #pragma mark -
     return sharedPreviewController;
 }
 
+//- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
+//    if (!forUserResize) {
+//        id item = [browser parentForItemsInColumn:columnIndex]; 
+//        if ([self browser:browser isLeafItem:item]) {
+//            suggestedWidth = 200; 
+//        }
+//    }
+//    return suggestedWidth;
+//}
+
+- (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
+    return NO;
+}
+
+#pragma mark Drag and Drop source
+
+- (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
+   toPasteboard:(NSPasteboard *)pasteboard {
+    NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
+    NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
+    for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
+        PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
+        [propertyList addObject:[node.pithosObject.name pathExtension]];
+    }
+
+    [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
+    [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
+    
+    return YES;
+}
+
+- (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
+forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
+    NSMutableArray *names = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
+    NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
+    for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
+        PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
+        
+        // If the node is a subdir ask if the whole tree should be downloaded
+        if ([node class] == [PithosSubdirNode class]) {
+            NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+            [alert setMessageText:@"Download directory"];
+            [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
+            [alert addButtonWithTitle:@"OK"];
+            [alert addButtonWithTitle:@"Cancel"];
+            NSInteger choice = [alert runModal];
+            if (choice == NSAlertFirstButtonReturn) {
+                NSArray *objectRequests = [PithosFileUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
+                                                                                                 objectName:node.pithosObject.name 
+                                                                                                toDirectory:[dropDestination path] 
+                                                                                              checkIfExists:YES];
+                if (objectRequests) {
+                    for (ASIPithosObjectRequest *objectRequest in objectRequests) {
+                        [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
+                        objectRequest.delegate = self;
+                        objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
+                        objectRequest.didFailSelector = @selector(downloadObjectFailed:);
+                        [objectRequest startAsynchronous];
+                    }
+                }
+            }
+        } else {
+            ASIPithosObjectRequest *objectRequest = [PithosFileUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
+                                                                                                 objectName:node.pithosObject.name 
+                                                                                                toDirectory:[dropDestination path] 
+                                                                                              checkIfExists:YES];
+            if (objectRequest) {
+                [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
+                objectRequest.delegate = self;
+                objectRequest.didFinishSelector = @selector(downloadObjectFinished:);
+                objectRequest.didFailSelector = @selector(downloadObjectFailed:);
+                [objectRequest startAsynchronous];
+            }
+        }
+    }
+    return names;
+}
+
+#pragma mark Drag and Drop destination
+
+- (NSDragOperation)browser:aBrowser 
+              validateDrop:(id<NSDraggingInfo>)info 
+               proposedRow:(NSInteger *)row 
+                    column:(NSInteger *)column 
+             dropOperation:(NSBrowserDropOperation *)dropOperation {
+    NSDragOperation result = NSDragOperationNone;
+    // Files from the finder are accepted
+    if ([[[info draggingPasteboard] types] indexOfObject:NSFilenamesPboardType] != -1) {
+        // For a between drop, we let the user drop "on" the parent item
+        if (*dropOperation == NSBrowserDropAbove)
+            *row = -1;
+        // Only allow dropping in folders
+        if (*column != -1) {
+            if (*row != -1) {
+                PithosNode *node = [browser itemAtRow:*row inColumn:*column];
+                if ([node class] != [PithosSubdirNode class])
+                    *row = -1;
+            *dropOperation = NSBrowserDropOn;
+            result = NSDragOperationCopy;
+            }
+        }
+    }
+    // XXX else local file promises
+    return result;
+}
+
+- (BOOL)browser:(NSBrowser *)aBrowser 
+     acceptDrop:(id<NSDraggingInfo>)info 
+          atRow:(NSInteger)row 
+         column:(NSInteger)column 
+  dropOperation:(NSBrowserDropOperation)dropOperation {
+    NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
+    NSLog(@"drag in filenames: %@", filenames);
+    PithosNode *node = nil;
+    if ((column != -1) && (filenames != nil)) {
+        if (row != -1)
+            node = [browser itemAtRow:row inColumn:column];
+        else
+            node = [browser parentForItemsInColumn:column];
+        NSLog(@"drag in node: %@", node.url);
+        if (([node class] != [PithosSubdirNode class]) && ([node class] != [PithosContainerNode class]))
+            return NO;
+        
+        NSFileManager *defaultManager = [NSFileManager defaultManager];
+        NSString *containerName = [NSString stringWithString:node.pithosContainer.name];
+        NSString *objectNamePrefix;
+        if ([node class] == [PithosSubdirNode class])
+            objectNamePrefix = [NSString stringWithString:node.pithosObject.name];
+        else
+            objectNamePrefix = [NSString stringWithString:@""];
+        NSUInteger blockSize = node.pithosContainer.blockSize;
+        NSString *blockHash = node.pithosContainer.blockHash;
+        
+        for (NSString *filePath in filenames) {
+            BOOL isDirectory;
+            if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
+                if (!isDirectory) {
+                    // Upload file
+                    NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]];
+                    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+                    dispatch_async(queue, ^{
+                        NSError *error = nil;
+                        NSURLResponse *response = nil;
+                        [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath] 
+                                                                                 cachePolicy:NSURLCacheStorageNotAllowed 
+                                                                             timeoutInterval:.1] 
+                                              returningResponse:&response 
+                                                          error:&error];
+                        NSString *contentType = [response MIMEType];
+                        if (contentType == nil)
+                            contentType = @"application/binary";
+                        if (error)
+                            NSLog(@"contentType detection error: %@", error);
+                        NSArray *hashes = nil;
+                        ASIPithosObjectRequest *objectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:containerName 
+                                                                                                                  objectName:objectName 
+                                                                                                                 contentType:contentType 
+                                                                                                                   blockSize:blockSize 
+                                                                                                                   blockHash:blockHash 
+                                                                                                                     forFile:filePath 
+                                                                                                               checkIfExists:YES 
+                                                                                                                      hashes:&hashes];
+                        if (objectRequest) {
+                            // XXX set delegates and queue
+                            objectRequest.delegate = self;
+                            objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
+                            objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
+                            objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+                                                      containerName, @"containerName", 
+                                                      objectName, @"objectName", 
+                                                      contentType, @"contentType", 
+                                                      [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", 
+                                                      blockHash, @"blockHash", 
+                                                      filePath, @"filePath", 
+                                                      hashes, @"hashes", 
+                                                      [NSNumber numberWithBool:YES], @"checkIfExists", 
+                                                      node, @"node", 
+                                                      [NSNumber numberWithUnsignedInteger:0], @"iteration", 
+                                                      nil];
+                            [objectRequest startAsynchronous];
+                        }
+                        // XXX else show alert?
+                    });
+                } else {
+                    // Upload directory, confirm first
+                    // XXX implement this
+                }
+            }
+            
+        }
+        return YES;
+    }
+
+    return NO;
+}
+
+#pragma mark -
+#pragma mark ASIHTTPRequestDelegate
+
+- (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest {
+    NSLog(@"download completed: %@", [objectRequest url]);
+    if ([objectRequest bytes] == 0) {
+        NSLog(@"downloaded  0 bytes");
+        NSFileManager *defaultManager = [NSFileManager defaultManager];
+        NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
+        if (![defaultManager fileExistsAtPath:filePath]) {
+            if (![defaultManager createFileAtPath:filePath contents:nil attributes:nil]) {
+                NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+                [alert setMessageText:@"File Creation Error"];
+                [alert setInformativeText:[NSString stringWithFormat:@"Couldn't create zero length file at %@", filePath]];
+                [alert addButtonWithTitle:@"OK"];
+                [alert runModal];
+            }
+        }
+    }
+}
+
+- (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest {
+    NSLog(@"download failed: %@, error: %@", [objectRequest url], [objectRequest error]);
+    NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+    [alert setMessageText:@"HTTP Request Error"];
+    [alert setInformativeText:[NSString stringWithFormat:@"An error occured: %@", [objectRequest error]]];
+    [alert addButtonWithTitle:@"OK"];
+    [alert runModal];
+}
+
+- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
+    NSLog(@"upload using hashmap completed: %@", [objectRequest url]);
+    if (objectRequest.responseStatusCode == 201) {
+        NSLog(@"object created: %@", [objectRequest url]);
+        PithosNode *node = [objectRequest.userInfo objectForKey:@"node"];
+        [node invalidateChildren];
+        node.children;
+    } else if (objectRequest.responseStatusCode == 409) {
+        NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue] + 1;
+        if (iteration > 10) {
+            NSLog(@"upload iteration limit reached: %@", [objectRequest url]);
+            // XXX show alert
+            return;
+        }
+        NSLog(@"object is missing hashes: %@", [objectRequest url]);
+        ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"]
+                                                                                                      objectName:@".upload" 
+                                                                                                     contentType:[objectRequest.userInfo objectForKey:@"contentType"]
+                                                                                                       blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]
+                                                                                                         forFile:[objectRequest.userInfo objectForKey:@"filePath"]
+                                                                                                          hashes:[objectRequest.userInfo objectForKey:@"hashes"] 
+                                                                                           missingHashesResponse:[objectRequest responseString] 
+                                                                                                   checkIfExists:[[objectRequest.userInfo objectForKey:@"checkIfExists"] boolValue]];
+        newObjectRequest.shouldAttemptPersistentConnection = NO;
+        newObjectRequest.delegate = self;
+        newObjectRequest.didFinishSelector = @selector(uploadMissingHashesFinished:);
+        newObjectRequest.didFailSelector = @selector(uploadMissingHashesFailed:);
+        newObjectRequest.userInfo = objectRequest.userInfo;
+        [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithBool:NO] forKey:@"checkIfExists"];
+        [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:iteration] forKey:@"iteration"];
+        [newObjectRequest startAsynchronous];
+    } else {
+        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+        [alert setMessageText:@"Unexpected Response Status"];
+        [alert setInformativeText:[NSString stringWithFormat:@"Unexpected response status %d - %@", objectRequest.responseStatusCode, objectRequest.responseStatusMessage]];
+        [alert addButtonWithTitle:@"OK"];
+        [alert runModal];
+    }
+}
+
+- (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest {
+    NSLog(@"upload failed: %@, error: %@", [objectRequest url], [objectRequest error]);
+    NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+    [alert setMessageText:@"HTTP Request Error"];
+    [alert setInformativeText:[NSString stringWithFormat:@"An error occured: %@", [objectRequest error]]];
+    [alert addButtonWithTitle:@"OK"];
+    [alert runModal];
+}
+
+- (void)uploadMissingHashesFinished:(ASIPithosObjectRequest *)objectRequest {
+    NSLog(@"upload of missing hashes completed: %@", [objectRequest url]);
+    if ((objectRequest.responseStatusCode == 201) || (objectRequest.responseStatusCode == 204)) {
+        NSArray *hashes = [objectRequest.userInfo objectForKey:@"hashes"];
+        ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] 
+                                                                                                     objectName:[objectRequest.userInfo objectForKey:@"objectName"] 
+                                                                                                    contentType:[objectRequest.userInfo objectForKey:@"contentType"] 
+                                                                                                      blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] 
+                                                                                                      blockHash:[objectRequest.userInfo objectForKey:@"blockHash"]
+                                                                                                        forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
+                                                                                                  checkIfExists:NO 
+                                                                                                         hashes:&hashes];
+        newObjectRequest.delegate = self;
+        newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
+        newObjectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:);
+        newObjectRequest.userInfo = objectRequest.userInfo;
+        [newObjectRequest startAsynchronous];
+    } else {
+        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+        [alert setMessageText:@"Unexpected Response Status"];
+        [alert setInformativeText:[NSString stringWithFormat:@"Unexpected response status %d - %@", objectRequest.responseStatusCode, objectRequest.responseStatusMessage]];
+        [alert addButtonWithTitle:@"OK"];
+        [alert runModal];
+    }
+}
+
+- (void)uploadMissingHashesFailed:(ASIPithosObjectRequest *)objectRequest {    
+    NSLog(@"upload of missing hashes failed: %@, error: %@", [objectRequest url], [objectRequest error]);
+    NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+    [alert setMessageText:@"HTTP Request Error"];
+    [alert setInformativeText:[NSString stringWithFormat:@"An error occured: %@", [objectRequest error]]];
+    [alert addButtonWithTitle:@"OK"];
+    [alert runModal];
+}
 
 #pragma mark -
-#pragma NSSplitViewDelegate
+#pragma mark NSSplitViewDelegate
 
 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
-    return 100;
+    return 120;
 }
 
 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
-    return 260;
+    return 220;
 }
 
 #pragma mark -
     }
 }
 
+#pragma mark -
+#pragma mark NSMenuDelegate
+
+- (void)menuNeedsUpdate:(NSMenu *)menu {
+    NSInteger column = [browser clickedColumn];
+    NSInteger row = [browser clickedRow];
+    [menu removeAllItems];
+    if ((column == -1) || (row == -1)) {
+        // General context menu has 0
+    } else {
+        // PithosNode menu has 1 items
+        // Get Info
+        NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""];
+        [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
+        [menu addItem:menuItem];
+    }
+}
+
+#pragma mark -
+#pragma mark Menu Actions
+
+- (void)getInfo:(NSMenuItem *)sender {
+    [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender];
+}
+
 @end
\ No newline at end of file