X-Git-Url: https://code.grnet.gr/git/pithos-macos/blobdiff_plain/6a9b9f40837b5718a5b496190050fd5a95e33561..e8e3d71e3b001f4783889dcab46bf356f488b279:/pithos-macos/PithosBrowserController.m diff --git a/pithos-macos/PithosBrowserController.m b/pithos-macos/PithosBrowserController.m index 55a1c9f..e8f0743 100644 --- a/pithos-macos/PithosBrowserController.m +++ b/pithos-macos/PithosBrowserController.m @@ -2,7 +2,7 @@ // PithosBrowserController.m // pithos-macos // -// Copyright 2011 GRNET S.A. All rights reserved. +// Copyright 2011-2012 GRNET S.A. All rights reserved. // // Redistribution and use in source and binary forms, with or // without modification, are permitted provided that the following @@ -41,15 +41,22 @@ #import "PithosContainerNode.h" #import "PithosSubdirNode.h" #import "PithosObjectNode.h" +#import "PithosSharingAccountsNode.h" #import "PithosEmptyNode.h" #import "ImageAndTextCell.h" #import "FileSystemBrowserCell.h" +#import "ASINetworkQueue.h" #import "ASIPithosRequest.h" +#import "ASIPithos.h" #import "ASIPithosContainerRequest.h" #import "ASIPithosObjectRequest.h" +#import "ASIPithosAccount.h" #import "ASIPithosContainer.h" #import "ASIPithosObject.h" -#import "PithosFileUtilities.h" +#import "PithosUtilities.h" +#import "UsingSizeTransformer.h" + +#define REFRESH_TIMER_INTERVAL 5 @interface PithosBrowserCell : FileSystemBrowserCell {} @end @@ -59,6 +66,7 @@ - (id)init { if ((self = [super init])) { [self setLineBreakMode:NSLineBreakByTruncatingMiddle]; + [self setEditable:YES]; } return self; } @@ -93,19 +101,17 @@ @end -@interface PithosBrowserController (Private) {} -- (void)resetContainers:(NSNotification *)notification; -- (void)getInfo:(NSMenuItem *)sender; -- (void)downloadObjectFinished:(ASIPithosObjectRequest *)objectRequest; -- (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest; -- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest; -- (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest; -- (void)uploadMissingBlockFinished:(ASIPithosObjectRequest *)objectRequest; -- (void)uploadMissingBlockFailed:(ASIPithosObjectRequest *)objectRequest; +@interface PithosBrowserController (Private) +- (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode; +- (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode; +- (BOOL)cpyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode; @end @implementation PithosBrowserController -@synthesize outlineViewDataSourceArray, splitView, outlineView, browser; +@synthesize pithos; +@synthesize pithosAccountManager, accountNode; +@synthesize draggedNodes, draggedParentNode; +@synthesize clipboardNodes, clipboardParentNode, clipboardCopy; #pragma mark - #pragma Object Lifecycle @@ -114,181 +120,421 @@ return [super initWithWindowNibName:@"PithosBrowserController"]; } -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [browserMenu release]; - [sharedPreviewController release]; - [outlineViewDataSourceArray release]; - [accountNode release]; - [rootNode release]; - [super dealloc]; +- (void)windowDidLoad { + [super windowDidLoad]; + if (browser && !browserInitialized) { + browserInitialized = YES; + [self initBrowser]; + } } -- (void)awakeFromNib { - [super awakeFromNib]; - - [browser registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; - [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES]; +- (void)initBrowser { + [browser registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]]; + [browser setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES]; [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO]; + [outlineView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSFilesPromisePboardType, nil]]; + [outlineView setDraggingSourceOperationMask:(NSDragOperationCopy|NSDragOperationMove) forLocal:YES]; + [outlineView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO]; + [browser setCellClass:[PithosBrowserCell class]]; + [browser setAllowsBranchSelection:YES]; + [browser setAllowsMultipleSelection:YES]; + [browser setAllowsEmptySelection:YES]; + [browser setAllowsTypeSelect:YES]; + [browser setDoubleAction:@selector(browserDoubleAction:)]; - browserMenu = [[NSMenu alloc] init]; - [browserMenu setDelegate:self]; - [browser setMenu:browserMenu]; -} - -- (void)resetContainers:(NSNotification *)notification { - rootNode = nil; - [browser loadColumnZero]; - self.outlineViewDataSourceArray = nil; + moveNetworkQueue = [[ASINetworkQueue alloc] init]; + moveNetworkQueue.shouldCancelAllRequestsOnFailure = NO; +// moveNetworkQueue.maxConcurrentOperationCount = 1; + copyNetworkQueue = [[ASINetworkQueue alloc] init]; + copyNetworkQueue.shouldCancelAllRequestsOnFailure = NO; +// copyNetworkQueue.maxConcurrentOperationCount = 1; + deleteNetworkQueue = [[ASINetworkQueue alloc] init]; + deleteNetworkQueue.shouldCancelAllRequestsOnFailure = NO; +// deleteNetworkQueue.maxConcurrentOperationCount = 1; + uploadNetworkQueue = [[ASINetworkQueue alloc] init]; + uploadNetworkQueue.showAccurateProgress = YES; + uploadNetworkQueue.shouldCancelAllRequestsOnFailure = NO; +// uploadNetworkQueue.maxConcurrentOperationCount = 1; + downloadNetworkQueue = [[ASINetworkQueue alloc] init]; + downloadNetworkQueue.showAccurateProgress = YES; + downloadNetworkQueue.shouldCancelAllRequestsOnFailure = NO; +// downloadNetworkQueue.maxConcurrentOperationCount = 1; - // Create the outlineView tree - // CONTAINERS - NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject: - [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]]; - - // SHARED - NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject: - [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]]; - // SHARED/my shared - [[sharedTreeNode mutableChildNodes] addObject: - [NSTreeNode treeNodeWithRepresentedObject: - [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" - icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)] - ] autorelease]]]; - // SHARED/others shared - [[sharedTreeNode mutableChildNodes] addObject: - [NSTreeNode treeNodeWithRepresentedObject: - [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared" - icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)] - ] autorelease]]]; + moveQueue = [[NSOperationQueue alloc] init]; + [moveQueue setSuspended:YES]; + moveQueue.name = @"gr.grnet.pithos.MoveQueue"; +// moveQueue.maxConcurrentOperationCount = 1; + copyQueue = [[NSOperationQueue alloc] init]; + [copyQueue setSuspended:YES]; + copyQueue.name = @"gr.grnet.pithos.CopyQueue"; +// copyQueue.maxConcurrentOperationCount = 1; + deleteQueue = [[NSOperationQueue alloc] init]; + [deleteQueue setSuspended:YES]; + deleteQueue.name = @"gr.grnet.pithos.DeleteQueue"; +// deleteQueue.maxConcurrentOperationCount = 1; + uploadQueue = [[NSOperationQueue alloc] init]; + [uploadQueue setSuspended:YES]; + uploadQueue.name = @"gr.grnet.pithos.UploadQueue"; +// uploadQueue.maxConcurrentOperationCount = 1; + downloadQueue = [[NSOperationQueue alloc] init]; + [downloadQueue setSuspended:YES]; + downloadQueue.name = @"gr.grnet.pithos.DownloadQueue"; +// downloadQueue.maxConcurrentOperationCount = 1; - self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil]; + moveCallbackQueue = [[NSOperationQueue alloc] init]; + [moveCallbackQueue setSuspended:YES]; + moveCallbackQueue.name = @"gr.grnet.pithos.MoveCallbackQueue"; +// moveCallbackQueue.maxConcurrentOperationCount = 1; + copyCallbackQueue = [[NSOperationQueue alloc] init]; + [copyCallbackQueue setSuspended:YES]; + copyCallbackQueue.name = @"gr.grnet.pithos.CopyCallbackQueue"; +// copyCallbackQueue.maxConcurrentOperationCount = 1; + deleteCallbackQueue = [[NSOperationQueue alloc] init]; + [deleteCallbackQueue setSuspended:YES]; + deleteCallbackQueue.name = @"gr.grnet.pithos.DeleteCallbackQueue"; +// deleteCallbackQueue.maxConcurrentOperationCount = 1; + uploadCallbackQueue = [[NSOperationQueue alloc] init]; + [uploadCallbackQueue setSuspended:YES]; + uploadCallbackQueue.name = @"gr.grnet.pithos.UploadCallbackQueue"; +// uploadCallbackQueue.maxConcurrentOperationCount = 1; + downloadCallbackQueue = [[NSOperationQueue alloc] init]; + [downloadCallbackQueue setSuspended:YES]; + downloadCallbackQueue.name = @"gr.grnet.pithos.DownloadCallbackQueue"; +// downloadCallbackQueue.maxConcurrentOperationCount = 1; - // Expand the folder outline view - [outlineView expandItem:nil expandChildren:YES]; - [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO]; + [activityProgressIndicator setUsesThreadedAnimation:YES]; + [activityProgressIndicator setMinValue:0.0]; + [activityProgressIndicator setMaxValue:1.0]; + activityFacility = [PithosActivityFacility defaultPithosActivityFacility]; - // Create accountNode and trigger a refresh - accountNode = [[PithosAccountNode alloc] init]; - accountNode.children; -} - -- (void)windowDidLoad { - [super windowDidLoad]; + self.accountNode = [[PithosAccountNode alloc] initWithPithosAccountManager:pithosAccountManager andPithos:pithos]; + containersNode = [[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil]; + containersNodeChildren = [[NSMutableArray alloc] init]; + sharedNode = [[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil]; + mySharedNode = [[PithosAccountNode alloc] initWithPithosAccountManager:pithosAccountManager andPithos:pithos]; + mySharedNode.displayName = @"shared by me"; + mySharedNode.shared = YES; + mySharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]; + othersSharedNode = [[PithosSharingAccountsNode alloc] initWithPithosAccountManager:pithosAccountManager andPithos:pithos]; + othersSharedNode.displayName = @"shared with me"; + othersSharedNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]; - [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]]; + [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[PithosOutlineViewCell alloc] init]]; // Register for updates + // PithosAccountNode accountNode updates outlineView container nodes [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(pithosNodeChildrenUpdated:) - name:@"PithosContainerNodeChildrenUpdated" - object:nil]; + selector:@selector(pithosAccountNodeChildrenUpdated:) + name:@"PithosNodeChildrenUpdated" + object:accountNode]; + // PithosNode updates browser nodes [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pithosNodeChildrenUpdated:) - name:@"PithosSubdirNodeChildrenUpdated" - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(pithosAccountNodeChildrenUpdated:) - name:@"PithosAccountNodeChildrenUpdated" + name:@"PithosNodeChildrenUpdated" object:nil]; + // Request for browser refresh [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(resetContainers:) - name:@"PithosAuthenticationCredentialsUpdated" + selector:@selector(pithosBrowserRefreshNeeded:) + name:@"PithosBrowserRefreshNeeded" object:nil]; } +- (void)resetBrowser { + @synchronized(self) { + if (!browserActive) + return; + } + + [refreshTimer invalidate]; + + [moveNetworkQueue reset]; + [copyNetworkQueue reset]; + [deleteNetworkQueue reset]; + [uploadNetworkQueue reset]; + [downloadNetworkQueue reset]; + + [moveQueue cancelAllOperations]; + [moveQueue setSuspended:YES]; + [copyQueue cancelAllOperations]; + [copyQueue setSuspended:YES]; + [deleteQueue cancelAllOperations]; + [deleteQueue setSuspended:YES]; + [uploadQueue cancelAllOperations]; + [uploadQueue setSuspended:YES]; + [downloadQueue cancelAllOperations]; + [downloadQueue setSuspended:YES]; + + [moveCallbackQueue cancelAllOperations]; + [moveCallbackQueue setSuspended:YES]; + [copyCallbackQueue cancelAllOperations]; + [copyCallbackQueue setSuspended:YES]; + [deleteCallbackQueue cancelAllOperations]; + [deleteCallbackQueue setSuspended:YES]; + [uploadCallbackQueue cancelAllOperations]; + [uploadCallbackQueue setSuspended:YES]; + [downloadCallbackQueue cancelAllOperations]; + [downloadCallbackQueue setSuspended:YES]; + + [accountNode pithosNodeWillBeRemoved]; + [mySharedNode pithosNodeWillBeRemoved]; + [othersSharedNode pithosNodeWillBeRemoved]; + + rootNode = nil; + [browser loadColumnZero]; + [containersNodeChildren removeAllObjects]; + [outlineView reloadData]; + // Expand the folder outline view + [outlineView expandItem:nil expandChildren:YES]; + [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO]; + + activityFacility.delegate = nil; + [activityProgressIndicator setDoubleValue:1.0]; + [activityProgressIndicator stopAnimation:self]; + + @synchronized(self) { + browserActive = NO; + } +} + +- (void)startBrowser { + @synchronized(self) { + if (browserActive) + return; + } + + // In the improbable case of leftover operations + [moveNetworkQueue reset]; + [copyNetworkQueue reset]; + [deleteNetworkQueue reset]; + [uploadNetworkQueue reset]; + [downloadNetworkQueue reset]; + [moveQueue cancelAllOperations]; + [copyQueue cancelAllOperations]; + [deleteQueue cancelAllOperations]; + [uploadQueue cancelAllOperations]; + [downloadQueue cancelAllOperations]; + [moveCallbackQueue cancelAllOperations]; + [copyCallbackQueue cancelAllOperations]; + [deleteCallbackQueue cancelAllOperations]; + [uploadCallbackQueue cancelAllOperations]; + [downloadCallbackQueue cancelAllOperations]; + + [moveNetworkQueue go]; + [copyNetworkQueue go]; + [deleteNetworkQueue go]; + [uploadNetworkQueue go]; + [downloadNetworkQueue go]; + [moveQueue setSuspended:NO]; + [copyQueue setSuspended:NO]; + [deleteQueue setSuspended:NO]; + [uploadQueue setSuspended:NO]; + [downloadQueue setSuspended:NO]; + [moveCallbackQueue setSuspended:NO]; + [copyCallbackQueue setSuspended:NO]; + [deleteCallbackQueue setSuspended:NO]; + [uploadCallbackQueue setSuspended:NO]; + [downloadCallbackQueue setSuspended:NO]; + + accountNode.pithos = pithos; + accountNode.pithosAccountManager = pithosAccountManager; + [accountNode forceRefresh]; + mySharedNode.pithos = pithos; + mySharedNode.pithosAccountManager = pithosAccountManager; + [mySharedNode forceRefresh]; + othersSharedNode.pithos = pithos; + othersSharedNode.pithosAccountManager = pithosAccountManager; + [othersSharedNode forceRefresh]; + +// [activityFacility reset]; + activityFacility.delegate = self; + + refreshTimer = [NSTimer scheduledTimerWithTimeInterval:REFRESH_TIMER_INTERVAL + target:self + selector:@selector(forceRefresh:) + userInfo:self + repeats:YES]; + @synchronized(self) { + browserActive = YES; + } +} + +- (BOOL)operationsPending { + return ([moveNetworkQueue operationCount] || + [copyNetworkQueue operationCount] || + [deleteNetworkQueue operationCount] || + [uploadNetworkQueue operationCount] || + [downloadNetworkQueue operationCount] || + [moveQueue operationCount] || + [copyQueue operationCount] || + [deleteQueue operationCount] || + [uploadQueue operationCount] || + [downloadQueue operationCount] || + [moveCallbackQueue operationCount] || + [copyCallbackQueue operationCount] || + [deleteCallbackQueue operationCount] || + [uploadCallbackQueue operationCount] || + [downloadCallbackQueue operationCount]); +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self resetBrowser]; +} + +- (void)setPithos:(ASIPithos *)aPithos { + if (aPithos) { + if (![aPithos.authUser isEqualToString:pithos.authUser] || + ![aPithos.authToken isEqualToString:pithos.authToken] || + ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix] || + ![aPithos.publicURLPrefix isEqualToString:pithos.publicURLPrefix]) { + [self resetBrowser]; + pithos = aPithos; + [self startBrowser]; + } else { + [self startBrowser]; + } + } +} + + #pragma mark - -#pragma Observers +#pragma mark Observers - (void)pithosNodeChildrenUpdated:(NSNotification *)notification { + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(pithosNodeChildrenUpdated:) withObject:notification waitUntilDone:NO]; + return; + } PithosNode *node = (PithosNode *)[notification object]; - NSLog(@"pithosNodeChildrenUpdated:%@", node.url); + if ((node == accountNode) || ![node.pithos isEqualTo:pithos]) + return; + DLog(@"pithosNodeChildrenUpdated:%@", node.url); NSInteger lastColumn = [browser lastColumn]; for (NSInteger column = lastColumn; column >= 0; column--) { if ([[browser parentForItemsInColumn:column] isEqualTo:node]) { [browser reloadColumn:column]; - // The following code is unnecessary since the pithosObject is set in the PithosNode in each refresh - // Furthermore it caused problems on delete, because a non-existing parent was asked for - //if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) { - // // This reloads the preview column - // [browser setLastColumn:column]; - // [browser addColumn]; - //} return; } } } - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification { + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(pithosAccountNodeChildrenUpdated:) withObject:notification waitUntilDone:NO]; + return; + } BOOL containerPithosFound = NO; BOOL containerTrashFound = NO; - NSMutableArray *containersTreeNodeChildren = [NSMutableArray array]; + NSMutableIndexSet *removedContainersNodeChildren = [NSMutableIndexSet indexSet]; + for (NSUInteger i = 0 ; i < [containersNodeChildren count] ; i++) { + if (![accountNode.children containsObject:[containersNodeChildren objectAtIndex:i]]) + [removedContainersNodeChildren addIndex:i]; + } + [containersNodeChildren removeObjectsAtIndexes:removedContainersNodeChildren]; 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]; + if (![containersNodeChildren containsObject:containerNode]) + [containersNodeChildren insertObject: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]; + if (![containersNodeChildren containsObject:containerNode]) + [containersNodeChildren insertObject:containerNode atIndex:insertIndex]; containerTrashFound = YES; - } else { - [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]]; + } else if (![containersNodeChildren containsObject:containerNode]) { + [containersNodeChildren addObject:containerNode]; } } BOOL refreshAccountNode = NO; if (!containerPithosFound) { - // create pithos - ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"]; - [containerRequest startSynchronous]; + // Create pithos node + ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithPithos:pithos + containerName:@"pithos"]; + [PithosUtilities startAndWaitForRequest:containerRequest]; if ([containerRequest error]) { - [PithosFileUtilities httpRequestErrorAlertWithRequest:containerRequest]; + [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest]; } else { refreshAccountNode = YES; } } if (!containerTrashFound) { - // create trash - ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"]; - [containerRequest startSynchronous]; + // Create trash node + ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithPithos:pithos + containerName:@"trash"]; + [PithosUtilities startAndWaitForRequest:containerRequest]; if ([containerRequest error]) { - [PithosFileUtilities httpRequestErrorAlertWithRequest:containerRequest]; + [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest]; } else { refreshAccountNode = YES; } } - if (refreshAccountNode) { - [accountNode invalidateChildren]; - accountNode.children; - } else { - [[[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]; + + if (refreshAccountNode) + [accountNode refresh]; + + [outlineView reloadData]; + + // Expand the folder outline view + [outlineView expandItem:nil expandChildren:YES]; + + if (((rootNode == nil) || (rootNode == containersNode) || (rootNode == sharedNode)) && [containersNodeChildren count]) { + rootNode = [containersNodeChildren objectAtIndex:0]; + [browser loadColumnZero]; } + + if (notification) + [self refresh:nil]; +} + +- (void)pithosBrowserRefreshNeeded:(NSNotification *)notification { + [self refresh:nil]; } #pragma mark - -#pragma Actions +#pragma mark Actions -- (IBAction)refresh:(id)sender { +- (IBAction)forceRefresh:(id)sender { + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(forceRefresh:) withObject:sender waitUntilDone:NO]; + return; + } + if (editingItem) + return; + if (sender) + [accountNode forceRefresh]; for (NSInteger column = [browser lastColumn]; column >= 0; column--) { - [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren]; + PithosNode *node = (PithosNode *)[browser parentForItemsInColumn:column]; + node.forcedRefresh = YES; + [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren]; } [browser validateVisibleColumns]; } +- (IBAction)refresh:(id)sender { + if (![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(refresh:) withObject:sender waitUntilDone:NO]; + return; + } + if (editingItem) + return; + if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) { + [self forceRefresh:sender]; + } else { + if (sender) + [accountNode refresh]; + for (NSInteger column = [browser lastColumn]; column >= 0; column--) { + [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren]; + } + [browser validateVisibleColumns]; + } +} + #pragma mark - -#pragma NSBrowserDelegate +#pragma mark NSBrowserDelegate - (id)rootItemForBrowser:(NSBrowser *)browser { return rootNode; @@ -334,65 +580,228 @@ return NO; } +#pragma mark Editing + +- (BOOL)browser:(NSBrowser *)browser shouldEditItem:(id)item { + PithosNode *node = (PithosNode *)item; + if (node.shared || node.sharingAccount || + ([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class])) + return NO; + editingItem = YES; + return YES; +} + +- (void)browser:(NSBrowser *)browser setObjectValue:(id)object forItem:(id)item { + editingItem = NO; + PithosNode *node = (PithosNode *)item; + NSString *newName = (NSString *)object; + NSUInteger newNameLength = [newName length]; + NSRange firstSlashRange = [newName rangeOfString:@"/"]; + if ((newNameLength == 0) || + ((firstSlashRange.length == 1) && (firstSlashRange.location != (newNameLength - 1))) || + ([newName isEqualToString:node.displayName])) { + return; + } + if (([node class] == [PithosObjectNode class]) || + (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Rename (move) an object or subdir/ node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName]; + if ([newName hasSuffix:@"/"]) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + NSError *error = nil; + BOOL isDirectory; + if ([PithosUtilities objectExistsAtPithos:pithos + containerName:node.pithosContainer.name + objectName:destinationObjectName + error:&error + isDirectory:&isDirectory + sharingAccount:nil]) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Name Taken"]; + [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + }); + return; + } else if (error) { + return; + } + if (operation.isCancelled) + return; + ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:node.pithosContainer.name + destinationObjectName:destinationObjectName + checkIfExists:NO]; + if (!operation.isCancelled && objectRequest) { + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + }]; + [moveQueue addOperation:operation]; + } else if ([node class] == [PithosSubdirNode class]) { + if (firstSlashRange.length == 1) + return; + // Operation: Rename (move) a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName = [[node.pithosObject.name stringByDeletingLastPathComponent] stringByAppendingPathComponent:newName]; + NSError *error = nil; + BOOL isDirectory; + if ([PithosUtilities objectExistsAtPithos:pithos + containerName:node.pithosContainer.name + objectName:destinationObjectName + error:&error + isDirectory:&isDirectory + sharingAccount:nil]) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Name Taken"]; + [alert setInformativeText:[NSString stringWithFormat:@"The name '%@' is already taken. Please choose a different name", newName]]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + }); + return; + } else if (error) { + return; + } + if (operation.isCancelled) + return; + if (node.pithosObject.subdir) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:node.pithosContainer.name + destinationObjectName:destinationObjectName + checkIfExists:NO]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + }]; + [moveQueue addOperation:operation]; + } +} + #pragma mark Drag and Drop source +- (BOOL)browser:(NSBrowser *)aBrowser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column + withEvent:(NSEvent *)event { + NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; + __block BOOL result = YES; + [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){ + PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]]; + if (([node class] == [PithosContainerNode class]) || ([node class] == [PithosAccountNode class])) { + result = NO; + *stop = YES; + } + }]; + return result; +} + - (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]]; + NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:[rowIndexes count]]; + NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; + [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){ + PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:idx]]; [propertyList addObject:[node.pithosObject.name pathExtension]]; - } + [nodes addObject:node]; + }]; [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self]; [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType]; - + self.draggedNodes = nodes; + self.draggedParentNode = [browser parentForItemsInColumn:column]; 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]]; - + NSMutableArray *names = [NSMutableArray arrayWithCapacity:[draggedNodes count]]; + for (PithosNode *node in draggedNodes) { + [names addObject:node.displayName]; // 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]; + NSAlert *alert = [[NSAlert alloc] init]; [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]; - } - } - } + if (choice == NSAlertFirstButtonReturn) + [self downloadNode:node toDirectory:[dropDestination path] withNewFileName:nil version:nil checkIfExists:YES]; } 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]; - } + [self downloadNode:node toDirectory:[dropDestination path] withNewFileName:nil version:nil checkIfExists:YES]; } } return names; @@ -406,23 +815,68 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { column:(NSInteger *)column dropOperation:(NSBrowserDropOperation *)dropOperation { NSDragOperation result = NSDragOperationNone; - // Files from the finder are accepted if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) { - // For a between drop, we let the user drop "on" the parent item + // For a drop above, the drop is redirected to the parent item if (*dropOperation == NSBrowserDropAbove) *row = -1; // Only allow dropping in folders if (*column != -1) { + PithosNode *dropNode; if (*row != -1) { - PithosNode *node = [browser itemAtRow:*row inColumn:*column]; - if ([node class] != [PithosSubdirNode class]) + // Check if the node is not a folder and if so redirect to the parent item + dropNode = [browser itemAtRow:*row inColumn:*column]; + if ([dropNode class] == [PithosObjectNode class]) *row = -1; } - *dropOperation = NSBrowserDropOn; - result = NSDragOperationCopy; + if (*row == -1) + dropNode = [browser parentForItemsInColumn:*column]; + + if (!dropNode.shared && + (!dropNode.sharingAccount || + ([dropNode class] == [PithosSubdirNode class]) || + ([dropNode class] == [PithosContainerNode class]))) { + *dropOperation = NSBrowserDropOn; + result = NSDragOperationCopy; + } + } + } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) { + // For a drop above, the drop is redirected to the parent item + if (*dropOperation == NSBrowserDropAbove) + *row = -1; + // Only allow dropping in folders + if (*column != -1) { + PithosNode *dropNode; + if (*row != -1) { + // Check if the node is not a folder and if so redirect to the parent item + dropNode = [browser itemAtRow:*row inColumn:*column]; + if ([dropNode class] == [PithosObjectNode class]) + *row = -1; + } + if (*row == -1) + dropNode = [browser parentForItemsInColumn:*column]; + + if (!dropNode.shared && !dropNode.sharingAccount) { + if ([info draggingSourceOperationMask] & NSDragOperationMove) { + // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove + if ((([dropNode class] == [PithosContainerNode class]) || + dropNode.pithosObject.subdir || + ![dropNode.pithosObject.name hasSuffix:@"/"]) && + ![dropNode isEqualTo:draggedParentNode]) { + // ![dropNode isEqualTo:draggedParentNode] && + // ![draggedNodes containsObject:dropNode]) { + result = NSDragOperationMove; + } + } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) { + // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy + if (([dropNode class] == [PithosContainerNode class]) || + dropNode.pithosObject.subdir || + ![dropNode.pithosObject.name hasSuffix:@"/"]) { + result = NSDragOperationCopy; + } + } + } } } - // XXX else local file promises return result; } @@ -431,276 +885,1211 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { 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])) + if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) { + NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType]; + DLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames); + if ((column != -1) && (filenames != nil)) { + PithosNode *node; + if (row != -1) + node = [browser itemAtRow:row inColumn:column]; + else + node = [browser parentForItemsInColumn:column]; + DLog(@"drag in node: %@", node.url); + return [self uploadFiles:filenames toNode:node]; + } + } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) { + DLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes); + if ((column != -1) && (draggedNodes != nil)) { + PithosNode *node; + if (row != -1) + node = [browser itemAtRow:row inColumn:column]; + else + node = [browser parentForItemsInColumn:column]; + DLog(@"drag local node: %@", node.url); + if ([info draggingSourceOperationMask] & NSDragOperationMove) + return [self moveNodes:draggedNodes toNode:node]; + else if ([info draggingSourceOperationMask] & NSDragOperationCopy) + return [self cpyNodes:draggedNodes toNode:node]; + } + } + return NO; +} + +#pragma mark - +#pragma mark NSBrowser Actions + +- (void)browserDoubleAction:(id)sender { + NSInteger column = [browser clickedColumn]; + NSInteger row = [browser clickedRow]; + if ((column == -1) || (row == -1)) + return; + NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row]; + NSArray *menuNodesIndexPaths = [browser selectionIndexPaths]; + NSMutableArray *menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]]; + if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) { + for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) { + [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]]; + } + } else { + [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]]; + } + NSMenuItem *menuItem = [[NSMenuItem alloc] init]; + menuItem.representedObject = menuNodes; + [self menuDownload:menuItem]; +} + +#pragma mark - +#pragma mark Drag and Drop methods + +- (void)downloadNode:(PithosNode *)node toDirectory:(NSString *)dirPath withNewFileName:(NSString *)newFileName + version:(NSString *)version checkIfExists:(BOOL)checkIfExists { + if ([node class] == [PithosSubdirNode class]) { + // XXX newFilename and version are ignored in the case of a subdir node for now + // Operation: Download a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSArray *objectRequests = [PithosUtilities objectDataRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + toDirectory:dirPath + checkIfExists:checkIfExists + sharingAccount:node.sharingAccount]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (__block ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload + message:[messagePrefix stringByAppendingString:@" (0%)"] + totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] + currentBytes:0]; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility updateActivity:activity withMessage:activity.message]; + }); + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + downloadNetworkQueue, @"networkQueue", + @"download", @"operationType", + nil]]; + [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]]; + } + } + } + }]; + [downloadQueue addOperation:operation]; + } else if ([node class] == [PithosObjectNode class]) { + // Operation: Download an object node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectDataRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + version:version + toDirectory:dirPath + withNewFileName:newFileName + checkIfExists:checkIfExists + sharingAccount:node.sharingAccount]; + if (!operation.isCancelled && objectRequest) { + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Downloading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload + message:[messagePrefix stringByAppendingString:@" (0%)"] + totalBytes:node.pithosObject.bytes + currentBytes:0]; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility updateActivity:activity withMessage:activity.message]; + }); + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(downloadObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + downloadNetworkQueue, @"networkQueue", + @"download", @"operationType", + nil]]; + [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[messagePrefix stringByAppendingFormat:@" (%.0f%%)", (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; + [downloadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]]; + } + } + }]; + [downloadQueue addOperation:operation]; + } +} + +- (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode { + if (([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) + return NO; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name]; + NSString *objectNamePrefix; + if ([destinationNode class] == [PithosSubdirNode class]) + objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name]; + else + objectNamePrefix = [NSString string]; + if ((destinationNode.pithosContainer.blockHash == nil) || (destinationNode.pithosContainer.blockSize == 0)) { + ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest containerMetadataRequestWithPithos:pithos + containerName:containerName]; + [PithosUtilities startAndWaitForRequest:containerRequest]; + if ([containerRequest error]) { + [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest]; 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, ^{ + } else if (containerRequest.responseStatusCode != 204) { + [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest]; + return NO; + } + destinationNode.pithosContainer.blockHash = [containerRequest blockHash]; + destinationNode.pithosContainer.blockSize = [containerRequest blockSize]; + } + NSUInteger blockSize = destinationNode.pithosContainer.blockSize; + NSString *blockHash = destinationNode.pithosContainer.blockHash; + + for (NSString *filePath in filenames) { + BOOL isDirectory; + if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { + if (!isDirectory) { + // Upload file + NSString *objectName = [[objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]] + precomposedStringWithCanonicalMapping]; + // Operation: Upload a local file + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; NSError *error = nil; - NSString *contentType = [PithosFileUtilities contentTypeOfFile:filePath error:&error]; + NSString *contentType = [PithosUtilities contentTypeOfFile:filePath error:&error]; if (contentType == nil) contentType = @"application/octet-stream"; + #if DEBUG_PITHOS if (error) - NSLog(@"contentType detection error: %@", error); + DLog(@"contentType detection error: %@", error); + #endif NSArray *hashes = nil; - ASIPithosObjectRequest *objectRequest = [PithosFileUtilities writeObjectDataRequestWithContainerName:containerName - objectName:objectName - contentType:contentType - blockSize:blockSize - blockHash:blockHash - forFile:filePath - checkIfExists:YES - hashes:&hashes]; - if (objectRequest) { + if (operation.isCancelled) + return; + ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos + containerName:containerName + objectName:objectName + contentType:contentType + blockSize:blockSize + blockHash:blockHash + forFile:filePath + checkIfExists:YES + hashes:&hashes + sharingAccount:destinationNode.sharingAccount]; + if (!operation.isCancelled && objectRequest) { 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", - node, @"node", - [NSNumber numberWithUnsignedInteger:10], @"iteration", - nil]; - [objectRequest startAsynchronous]; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload + message:[messagePrefix stringByAppendingString:@" (0%)"] + totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] + currentBytes:0]; + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + containerName, @"containerName", + objectName, @"objectName", + contentType, @"contentType", + [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", + blockHash, @"blockHash", + filePath, @"filePath", + hashes, @"hashes", + [NSArray arrayWithObject:destinationNode], @"refreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + [NSNumber numberWithUnsignedInteger:10], @"iteration", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + uploadNetworkQueue, @"networkQueue", + @"upload", @"operationType", + nil]]; + if (destinationNode.sharingAccount) + [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]]; } - }); - } else { - // Upload directory, confirm first - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert setMessageText:@"Upload directory"]; - [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]]; - [alert addButtonWithTitle:@"OK"]; - [alert addButtonWithTitle:@"Cancel"]; - NSInteger choice = [alert runModal]; - if (choice == NSAlertFirstButtonReturn) { - NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]]; - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(queue, ^{ + } + }]; + [uploadQueue addOperation:operation]; + } else { + // Upload directory, confirm first + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Upload directory"]; + [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to upload it and its contents?", filePath]]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Cancel"]; + NSInteger choice = [alert runModal]; + if (choice == NSAlertFirstButtonReturn) { + NSString *objectName = [[objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]] + precomposedStringWithCanonicalMapping]; + // Operation: Upload a local directory and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; NSMutableArray *objectNames = nil; NSMutableArray *contentTypes = nil; NSMutableArray *filePaths = nil; NSMutableArray *hashesArrays = nil; - NSArray *objectRequests = [PithosFileUtilities writeObjectDataRequestsWithContainerName:containerName - objectName:objectName - blockSize:blockSize - blockHash:blockHash - forDirectory:filePath - checkIfExists:YES - objectNames:&objectNames - contentTypes:&contentTypes - filePaths:&filePaths - hashesArrays:&hashesArrays]; - if (objectRequests) { + NSMutableArray *directoryObjectRequests = nil; + NSArray *objectRequests = [PithosUtilities writeObjectDataRequestsWithPithos:pithos + containerName:containerName + objectName:objectName + blockSize:blockSize + blockHash:blockHash + forDirectory:filePath + checkIfExists:YES + objectNames:&objectNames + contentTypes:&contentTypes + filePaths:&filePaths + hashesArrays:&hashesArrays + directoryObjectRequests:&directoryObjectRequests + sharingAccount:destinationNode.sharingAccount]; + if (operation.isCancelled) + return; + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in directoryObjectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory + message:messagePrefix]; + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + uploadNetworkQueue, @"networkQueue", + @"upload", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]]; + } + if (!operation.isCancelled && objectRequests) { for (NSUInteger i = 0 ; i < [objectRequests count] ; i++) { + if (operation.isCancelled) + return; ASIPithosObjectRequest *objectRequest = [objectRequests objectAtIndex:i]; - // XXX if dir creation requests differentiate objectRequest.delegate = self; - objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:); - objectRequest.didFailSelector = @selector(uploadObjectUsingHashMapFailed:); - objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: - containerName, @"containerName", - [objectNames objectAtIndex:i], @"objectName", - [contentTypes objectAtIndex:i], @"contentType", - [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", - blockHash, @"blockHash", - [filePaths objectAtIndex:i], @"filePath", - [hashesArrays objectAtIndex:i], @"hashes", - [NSNull null], @"node", - [NSNumber numberWithUnsignedInteger:10], @"iteration", - nil]; - [objectRequest startAsynchronous]; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Uploading '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload + message:[messagePrefix stringByAppendingString:@" (0%)"] + totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] + currentBytes:0]; + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + containerName, @"containerName", + [objectNames objectAtIndex:i], @"objectName", + [contentTypes objectAtIndex:i], @"contentType", + [NSNumber numberWithUnsignedInteger:blockSize], @"blockSize", + blockHash, @"blockHash", + [filePaths objectAtIndex:i], @"filePath", + [hashesArrays objectAtIndex:i], @"hashes", + [NSNumber numberWithBool:YES], @"refresh", + [NSNumber numberWithUnsignedInteger:10], @"iteration", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (100%)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + uploadNetworkQueue, @"networkQueue", + @"upload", @"operationType", + nil]]; + if (destinationNode.sharingAccount) + [(NSMutableDictionary *)objectRequest.userInfo setObject:destinationNode.sharingAccount forKey:@"sharingAccount"]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest]]; } } - }); - } + } + }]; + [uploadQueue addOperation:operation]; } } - } - return YES; } + return YES; +} - return NO; +- (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode { + if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) || + (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) + return NO; + NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name]; + NSString *objectNamePrefix; + if ([destinationNode class] == [PithosSubdirNode class]) + objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name]; + else + objectNamePrefix = [NSString string]; + + for (PithosNode *node in nodes) { + if (([node class] == [PithosObjectNode class]) || + (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Move an object or subdir/ node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName]; + if ([node.pithosObject.name hasSuffix:@"/"]) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:containerName + destinationObjectName:destinationObjectName + checkIfExists:YES]; + if (!operation.isCancelled && objectRequest) { + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + }]; + [moveQueue addOperation:operation]; + } else if ([node class] == [PithosSubdirNode class]) { + // Operation: Move a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName]; + if (node.pithosObject.subdir) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:containerName + destinationObjectName:destinationObjectName + checkIfExists:YES]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects:node.parent, destinationNode, nil], @"forceRefreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + }]; + [moveQueue addOperation:operation]; + } + } + return YES; +} + +- (BOOL)cpyNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode { + if ((([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) || + (([destinationNode class] == [PithosSubdirNode class]) && !destinationNode.pithosObject.subdir && [destinationNode.pithosObject.name hasSuffix:@"/"])) + return NO; + NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name]; + NSString *objectNamePrefix; + if ([destinationNode class] == [PithosSubdirNode class]) + objectNamePrefix = [NSString stringWithString:destinationNode.pithosObject.name]; + else + objectNamePrefix = [NSString string]; + + for (PithosNode *node in nodes) { + if (([node class] == [PithosObjectNode class]) || + (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Copy an object or subdir/ node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName; + if (![destinationNode isEqualTo:node.parent]) { + destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName]; + if ([node.pithosObject.name hasSuffix:@"/"]) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + } else { + destinationObjectName = [PithosUtilities safeObjectNameForPithos:pithos + containerName:containerName + objectName:node.pithosObject.name]; + } + if (operation.isCancelled) + return; + ASIPithosObjectRequest *objectRequest = [PithosUtilities cpyObjectRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:containerName + destinationObjectName:destinationObjectName + checkIfExists:YES + sharingAccount:node.sharingAccount]; + if (!operation.isCancelled && objectRequest) { + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + copyNetworkQueue, @"networkQueue", + @"copy", @"operationType", + nil]]; + [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + }]; + [copyQueue addOperation:operation]; + } else if ([node class] == [PithosSubdirNode class]) { + // Operation: Copy a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *destinationObjectName; + if (![destinationNode isEqualTo:node.parent]) { + destinationObjectName = [objectNamePrefix stringByAppendingPathComponent:node.displayName]; + if (node.pithosObject.subdir) + destinationObjectName = [destinationObjectName stringByAppendingString:@"/"]; + } else { + destinationObjectName = [PithosUtilities safeSubdirNameForPithos:pithos + containerName:containerName + subdirName:node.pithosObject.name]; + } + if (operation.isCancelled) + return; + NSArray *objectRequests = [PithosUtilities cpyObjectRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:containerName + destinationObjectName:destinationObjectName + checkIfExists:YES + sharingAccount:node.sharingAccount]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Copying '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCopy + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:destinationNode], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(copyFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + copyNetworkQueue, @"networkQueue", + @"copy", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [copyNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + }]; + [copyQueue addOperation:operation]; + } + } + return YES; } #pragma mark - #pragma mark ASIHTTPRequestDelegate +- (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request { + NSOperationQueue *callbackQueue; + NSString *operationType = [request.userInfo objectForKey:@"operationType"]; + if ([operationType isEqualToString:@"move"]) + callbackQueue = moveCallbackQueue; + else if ([operationType isEqualToString:@"copy"]) + callbackQueue = copyCallbackQueue; + else if ([operationType isEqualToString:@"delete"]) + callbackQueue = deleteCallbackQueue; + else if ([operationType isEqualToString:@"upload"]) + callbackQueue = uploadCallbackQueue; + else if ([operationType isEqualToString:@"download"]) + callbackQueue = downloadCallbackQueue; + else { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + return; + } + // Add an operation to the callbackQueue with a completionBlock for the case of cancellation + NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self + selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) + object:request]; + operation.completionBlock = ^{ + @autoreleasepool { + if ([[request.userInfo objectForKey:@"operation"] isCancelled]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + } + } + }; + [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"]; + [callbackQueue addOperation:operation]; +} + +- (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request { + if (request.isCancelled) { + // Request has been cancelled + // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway + [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) + withObject:request]; + } else { + NSOperationQueue *callbackQueue; + NSString *operationType = [request.userInfo objectForKey:@"operationType"]; + if ([operationType isEqualToString:@"move"]) + callbackQueue = moveCallbackQueue; + else if ([operationType isEqualToString:@"copy"]) + callbackQueue = copyCallbackQueue; + else if ([operationType isEqualToString:@"delete"]) + callbackQueue = deleteCallbackQueue; + else if ([operationType isEqualToString:@"upload"]) + callbackQueue = uploadCallbackQueue; + else if ([operationType isEqualToString:@"download"]) + callbackQueue = downloadCallbackQueue; + else { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + return; + } + // Add an operation to the callbackQueue with a completionBlock for the case of cancellation + NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self + selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) + object:request]; + operation.completionBlock = ^{ + @autoreleasepool { + if ([[request.userInfo objectForKey:@"operation"] isCancelled]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + } + } + }; + [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"]; + [callbackQueue addOperation:operation]; + } +} + +- (void)requestFailed:(ASIPithosRequest *)request { + @autoreleasepool { + NSOperation *operation = [request.userInfo objectForKey:@"operation"]; + DLog(@"Request failed: %@", request.url); + if (operation.isCancelled) + return; + if (request.isCancelled) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + return; + } + NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue]; + if (retries > 0) { + ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request]; + [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; + [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithBool:NO] forKey:@"unexpectedResponseStatus"]; + [[newRequest.userInfo objectForKey:@"networkQueue"] addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]]; + }); + if ([[request.userInfo objectForKey:@"unexpectedResponseStatus"] boolValue]) + [PithosUtilities unexpectedResponseStatusAlertWithRequest:request]; + else + [PithosUtilities httpRequestErrorAlertWithRequest:request]; + } + } +} + - (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:@"Create File Error"]; - [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]]; - [alert addButtonWithTitle:@"OK"]; - [alert runModal]; + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Download finished: %@", objectRequest.url); + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 200) { + NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"]; + PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; + NSUInteger totalBytes = activity.totalBytes; + + // XXX change contentLength to objectContentLength if it is fixed in the server + if ([objectRequest contentLength] == 0) { + // The check above was: + // if (([objectRequest contentLength] == 0) && ![PithosUtilities isContentTypeDirectory:[objectRequest contentType]]) { + // I checked for directory content types in order not to create a file in place of a directory, + // but this callback method is not called in the case of a directory download. + // It maybe the case though, when downloading an old version of an object, is of a directory content type. + // In this case, a file should be created. This is actually a feature that allows you to hide data in a directory object. + DLog(@"Downloaded 0 bytes"); + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:filePath]) { + if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Create File Error"]; + [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + }); + } + } } + + NSUInteger currentBytes = [objectRequest objectContentLength]; + if (currentBytes == 0) + currentBytes = totalBytes; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] + totalBytes:totalBytes + currentBytes:currentBytes]; + }); + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; } } } -- (void)downloadObjectFailed:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Download failed"); - [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest]; +- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest { + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Upload directory object finished: %@", objectRequest.url); + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 201) { + DLog(@"Directory object created: %@", objectRequest.url); + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) { + [node forceRefresh]; + } + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) { + [node refresh]; + } + if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue]) + [self forceRefresh:self]; + else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue]) + [self refresh:self]; + }); + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; + } + } } - (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"]; - if (node != (id)[NSNull null]) { - [node invalidateChildren]; - node.children; - } // XXX else total refresh? - } else if (objectRequest.responseStatusCode == 409) { - NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue] - 1; - if (iteration == 0) { - NSLog(@"Upload iteration limit reached: %@", [objectRequest url]); - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert setMessageText:@"Upload Timeout"]; - [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", - [objectRequest.userInfo objectForKey:@"objectName"], [objectRequest.userInfo objectForKey:@"containerName"]]]; - [alert addButtonWithTitle:@"OK"]; - [alert runModal]; - return; + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Upload using hashmap finished: %@", objectRequest.url); + NSString *fileName = [objectRequest.userInfo objectForKey:@"fileName"]; + PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; + NSUInteger totalBytes = activity.totalBytes; + NSUInteger currentBytes = activity.currentBytes; + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 201) { + DLog(@"Object created: %@", objectRequest.url); + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] + totalBytes:totalBytes + currentBytes:totalBytes]; + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) { + [node forceRefresh]; + } + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) { + [node refresh]; + } + if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue]) + [self forceRefresh:self]; + else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue]) + [self refresh:self]; + }); + } else if (objectRequest.responseStatusCode == 409) { + NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue]; + if (iteration == 0) { + DLog(@"Upload iteration limit reached: %@", objectRequest.url); + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Upload Timeout"]; + [alert setInformativeText:[NSString stringWithFormat:@"Upload iteration limit reached for object with path '%@'", + [objectRequest.userInfo objectForKey:@"objectName"]]]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + }); + return; + } + DLog(@"object is missing hashes: %@", objectRequest.url); + NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"] + withMissingHashes:[objectRequest hashes]]; + NSUInteger blockSize = [[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]; + if (totalBytes >= [missingBlocks count]*blockSize) + currentBytes = totalBytes - [missingBlocks count]*blockSize; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (totalBytes ? (100*(currentBytes + 0.0)/(totalBytes + 0.0)) : 100)] + totalBytes:totalBytes + currentBytes:currentBytes]; + }); + NSUInteger missingBlockIndex = [missingBlocks firstIndex]; + __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos + containerName:[objectRequest.userInfo objectForKey:@"containerName"] + blockSize:blockSize + forFile:[objectRequest.userInfo objectForKey:@"filePath"] + missingBlockIndex:missingBlockIndex + sharingAccount:[objectRequest.userInfo objectForKey:@"sharingAccount"]]; + newContainerRequest.delegate = self; + newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + newContainerRequest.userInfo = objectRequest.userInfo; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"]; + [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; } - NSLog(@"object is missing hashes: %@", [objectRequest url]); - NSIndexSet *missingBlocks = [PithosFileUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"] - withMissingHashesResponse:[objectRequest responseString]]; - NSUInteger missingBlockIndex = [missingBlocks firstIndex]; - ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] - objectName:@".upload" - blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] - forFile:[objectRequest.userInfo objectForKey:@"filePath"] - missingBlockIndex:missingBlockIndex]; - newObjectRequest.delegate = self; - newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:); - newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:); - newObjectRequest.userInfo = objectRequest.userInfo; - [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:iteration] forKey:@"iteration"]; - [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; - [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; - [newObjectRequest startAsynchronous]; - } else { - [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest]; - } -} - -- (void)uploadObjectUsingHashMapFailed:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Upload using hashmap failed"); - [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest]; -} - -- (void)uploadMissingBlockFinished:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Upload of missing block completed: %@", [objectRequest url]); - if (objectRequest.responseStatusCode == 201) { - NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"]; - NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; - missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; - if (missingBlockIndex == NSNotFound) { - 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; - [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"]; - [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"]; - [newObjectRequest startAsynchronous]; + } +} + +- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest { + @autoreleasepool { + NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; + DLog(@"Upload of missing block finished: %@", containerRequest.url); + NSUInteger blockSize = [[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue]; + NSString *fileName = [containerRequest.userInfo objectForKey:@"fileName"]; + PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"]; + if (operation.isCancelled) { + [self requestFailed:containerRequest]; + } else if (containerRequest.responseStatusCode == 202) { + NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"]; + NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; + missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; + if (missingBlockIndex == NSNotFound) { + NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"]; + ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos + containerName:[containerRequest.userInfo objectForKey:@"containerName"] + objectName:[containerRequest.userInfo objectForKey:@"objectName"] + contentType:[containerRequest.userInfo objectForKey:@"contentType"] + blockSize:blockSize + blockHash:[containerRequest.userInfo objectForKey:@"blockHash"] + forFile:[containerRequest.userInfo objectForKey:@"filePath"] + checkIfExists:NO + hashes:&hashes + sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]]; + newObjectRequest.delegate = self; + newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + newObjectRequest.userInfo = containerRequest.userInfo; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } else { + __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos + containerName:[containerRequest.userInfo objectForKey:@"containerName"] + blockSize:[[containerRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] + forFile:[containerRequest.userInfo objectForKey:@"filePath"] + missingBlockIndex:missingBlockIndex + sharingAccount:[containerRequest.userInfo objectForKey:@"sharingAccount"]]; + newContainerRequest.delegate = self; + newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + newContainerRequest.userInfo = containerRequest.userInfo; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", fileName, (activity.totalBytes ? (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0)) : 100)] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } } else { - ASIPithosObjectRequest *newObjectRequest = [PithosFileUtilities updateObjectDataRequestWithContainerName:[objectRequest.userInfo objectForKey:@"containerName"] - objectName:@".upload" - blockSize:[[objectRequest.userInfo objectForKey:@"blockSize"] unsignedIntegerValue] - forFile:[objectRequest.userInfo objectForKey:@"filePath"] - missingBlockIndex:missingBlockIndex]; - newObjectRequest.delegate = self; - newObjectRequest.didFinishSelector = @selector(uploadMissingBlockFinished:); - newObjectRequest.didFailSelector = @selector(uploadMissingBlockFailed:); - newObjectRequest.userInfo = objectRequest.userInfo; - [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; - [newObjectRequest startAsynchronous]; + [(NSMutableDictionary *)(containerRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:containerRequest]; + } + } +} + +- (void)moveFinished:(ASIPithosObjectRequest *)objectRequest { + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Move object finished: %@", objectRequest.url); + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 201) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) { + [node forceRefresh]; + } + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) { + [node refresh]; + } + if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue]) + [self forceRefresh:self]; + else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue]) + [self refresh:self]; + }); + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; + } + } +} + +- (void)copyFinished:(ASIPithosObjectRequest *)objectRequest { + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Copy object finished: %@", objectRequest.url); + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 201) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) { + [node forceRefresh]; + } + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) { + [node refresh]; + } + if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue]) + [self forceRefresh:self]; + else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue]) + [self refresh:self]; + }); + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; } - } else { - [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest]; } } -- (void)uploadMissingBlockFailed:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Upload of missing block failed"); - [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest]; +- (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest { + @autoreleasepool { + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; + DLog(@"Delete object finished: %@", objectRequest.url); + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 204) { + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"forceRefreshNodes"]) { + [node forceRefresh]; + } + for (PithosNode *node in [objectRequest.userInfo objectForKey:@"refreshNodes"]) { + [node refresh]; + } + if ([[objectRequest.userInfo objectForKey:@"forceRefresh"] boolValue]) + [self forceRefresh:self]; + else if ([[objectRequest.userInfo objectForKey:@"refresh"] boolValue]) + [self refresh:self]; + }); + } else { + [(NSMutableDictionary *)(objectRequest.userInfo)setObject:[NSNumber numberWithBool:YES] forKey:@"unexpectedResponseStatus"]; + [self requestFailed:objectRequest]; + } + } } #pragma mark - #pragma mark NSSplitViewDelegate - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex { - return 120; + if (splitView == verticalSplitView) + return 140; + else + return ([horizontalSplitView bounds].size.height - 142); } - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex { - return 220; + if (splitView == verticalSplitView) + return 220; + else + return ([horizontalSplitView bounds].size.height - 108); +} + +- (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view { + if (((splitView == verticalSplitView) && (view == leftView)) || + ((splitView == horizontalSplitView) && (view == leftBottomView))) { + return NO; + } + return YES; +} + +#pragma mark - +#pragma mark NSOutlineViewDataSource + +- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { + if (!browserInitialized) + return 0; + if (item == nil) + return 2; + if (item == containersNode) + return containersNodeChildren.count; + if (item == sharedNode) + return 2; + return 0; +} + +- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { + if (!browserInitialized) + return nil; + if (item == nil) + return (!index ? containersNode : sharedNode); + if (item == sharedNode) + return (!index ? mySharedNode : othersSharedNode); + return [containersNodeChildren objectAtIndex:index]; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { + if ((item == containersNode) || (item == sharedNode)) + return YES; + return NO; +} + +- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { + PithosNode *node = (PithosNode *)item; + return node; +} + +#pragma mark Drag and Drop destination + +- (NSDragOperation)outlineView:(NSOutlineView *)anOutlineView + validateDrop:(id)info + proposedItem:(id)item + proposedChildIndex:(NSInteger)index { + NSDragOperation result = NSDragOperationNone; + if ((item == nil) || (index != NSOutlineViewDropOnItemIndex)) + return result; + PithosNode *dropNode = (PithosNode *)item; + if ([dropNode class] != [PithosContainerNode class]) + return result; + if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) { + result = NSDragOperationCopy; + } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) { + if (![[draggedNodes objectAtIndex:0] shared] && ![[draggedNodes objectAtIndex:0] sharingAccount] && + ([info draggingSourceOperationMask] & NSDragOperationMove)) { + // NSDragOperationCopy | NSDragOperationMove -> NSDragOperationMove + if (![dropNode isEqualTo:draggedParentNode]) + result = NSDragOperationMove; + } else if ([info draggingSourceOperationMask] & NSDragOperationCopy) { + // NSDragOperationCopy (Option modifier) -> NSDragOperationCopy + result = NSDragOperationCopy; + } + } + return result; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id)info item:(id)item childIndex:(NSInteger)index { + if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) { + NSArray *filenames = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType]; + DLog(@"drag in operation: %lu filenames: %@", [info draggingSourceOperationMask], filenames); + if (item && (index == NSOutlineViewDropOnItemIndex) && (filenames != nil)) { + PithosNode *node = (PithosNode *)item; + DLog(@"drag in node: %@", node.url); + return [self uploadFiles:filenames toNode:node]; + } + } else if ([[[info draggingPasteboard] types] containsObject:NSFilesPromisePboardType]) { + DLog(@"drag local operation: %lu nodes: %@", [info draggingSourceOperationMask], draggedNodes); + if (item && (index == NSOutlineViewDropOnItemIndex) && (draggedNodes != nil)) { + PithosNode *node = (PithosNode *)item; + DLog(@"drag local node: %@", node.url); + if (![[draggedNodes objectAtIndex:0] shared] && ![[draggedNodes objectAtIndex:0] sharingAccount] && + ([info draggingSourceOperationMask] & NSDragOperationMove)) + return [self moveNodes:draggedNodes toNode:node]; + else if ([info draggingSourceOperationMask] & NSDragOperationCopy) + return [self cpyNodes:draggedNodes toNode:node]; + } + } + return NO; } #pragma mark - #pragma mark NSOutlineViewDelegate - (BOOL)outlineView:outlineView shouldSelectItem:(id)item { - return ([[item representedObject] isLeaf]); + if ((item == containersNode) || (item == sharedNode)) + return NO; + return YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { - return (![[item representedObject] isLeaf]); + if ((item == containersNode) || (item == sharedNode)) + return YES; + return NO; } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { - PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject]; + PithosNode *node = (PithosNode *)[outlineView itemAtRow:[outlineView selectedRow]]; if (node) { rootNode = node; [browser loadColumnZero]; - [self refresh:nil]; + [self refresh:nil]; } } @@ -708,142 +2097,647 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { #pragma mark NSMenuDelegate - (void)menuNeedsUpdate:(NSMenu *)menu { - NSInteger column = [browser clickedColumn]; - NSInteger row = [browser clickedRow]; [menu removeAllItems]; NSMenuItem *menuItem; - if ((column == -1) || (row == -1)) { + NSString *menuItemTitle; + BOOL nodeContextMenu = NO; + PithosNode *menuNode = nil; + NSMutableArray *menuNodes; + if (menu == browserMenu) { + NSInteger column = [browser clickedColumn]; + NSInteger row = [browser clickedRow]; + if ((column == -1) || (row == -1)) { + if (column == -1) { + // General context menu + NSArray *menuNodesIndexPaths = [browser selectionIndexPaths]; + if ([menuNodesIndexPaths count] == 0) { + menuNode = [browser parentForItemsInColumn:0]; + } else if (([menuNodesIndexPaths count] != 1) || + ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) { + menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)]; + } else { + menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]]; + } + } else { + menuNode = [browser parentForItemsInColumn:column]; + if ([menuNode class] == [PithosObjectNode class]) { + // Node context menu + menuNodes = [NSMutableArray arrayWithObject:menuNode]; + nodeContextMenu = YES; + } + // else + // General context menu + } + } else { + // Node context menu + NSIndexPath *clickedNodeIndexPath = [[browser indexPathForColumn:column] indexPathByAddingIndex:row]; + NSArray *menuNodesIndexPaths = [browser selectionIndexPaths]; + menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]]; + if ([menuNodesIndexPaths containsObject:clickedNodeIndexPath]) { + for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) { + [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]]; + } + } else { + [menuNodes addObject:[browser itemAtIndexPath:clickedNodeIndexPath]]; + } + nodeContextMenu = YES; + } + } else if (menu == outlineViewMenu) { + NSInteger row = [outlineView clickedRow]; + if (row == -1) + row = [outlineView selectedRow]; + if (row == -1) + return; + menuNode = [outlineView itemAtRow:row]; + } + + if (!nodeContextMenu) { // General context menu + if (([menuNode class] == [PithosAccountNode class]) || + ([menuNode class] == [PithosSharingAccountsNode class]) || + ([menuNode class] == [PithosEmptyNode class])) + return; + // New Folder + if (!menuNode.shared && !menuNode.sharingAccount) { + menuItem = [[NSMenuItem alloc] initWithTitle:@"New Folder" action:@selector(menuNewFolder:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNode]; + [menu addItem:menuItem]; + [menu addItem:[NSMenuItem separatorItem]]; + } + // Refresh + menuItem = [[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""]; + [menu addItem:menuItem]; + [menu addItem:[NSMenuItem separatorItem]]; + // Get Info + menuItem = [[NSMenuItem alloc] initWithTitle:(([menuNode class] == [PithosContainerNode class]) ? @"Info" : @"Info and Sharing") + action:@selector(menuGetInfo:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:[NSArray arrayWithObject:menuNode]]; + [menu addItem:menuItem]; + // Paste + if (clipboardNodes && !menuNode.shared && !menuNode.sharingAccount && + (([menuNode class] == [PithosContainerNode class]) || + (([menuNode class] == [PithosSubdirNode class]) && + (menuNode.pithosObject.subdir || ![menuNode.pithosObject.name hasSuffix:@"/"])))) { + NSUInteger clipboardNodesCount = [clipboardNodes count]; + if (clipboardNodesCount == 0) { + self.clipboardNodes = nil; + } else if (clipboardCopy || ![menuNode isEqualTo:clipboardParentNode]) { + if (clipboardNodesCount == 1) + menuItemTitle = @"Paste Item"; + else + menuItemTitle = @"Paste Items"; + [menu addItem:[NSMenuItem separatorItem]]; + menuItem = [[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNode]; + [menu addItem:menuItem]; + } + } } else { + // Node context menu + NSUInteger menuNodesCount = [menuNodes count]; + PithosNode *firstMenuNode = [menuNodes objectAtIndex:0]; + // Download + if (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class])) { + menuItem = [[NSMenuItem alloc] initWithTitle:@"Download" action:@selector(menuDownload:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; + [menu addItem:menuItem]; + [menu addItem:[NSMenuItem separatorItem]]; + } // Move to Trash (pithos container only) // Delete - if ([rootNode class] == [PithosContainerNode class]) { + if (!firstMenuNode.shared && !firstMenuNode.sharingAccount && ([rootNode class] == [PithosContainerNode class])) { if ([rootNode.pithosContainer.name isEqualToString:@"pithos"]) { - menuItem = [[[NSMenuItem alloc] initWithTitle:@"Move to Trash" action:@selector(moveToTrash:) keyEquivalent:@""] autorelease]; - [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]]; + menuItem = [[NSMenuItem alloc] initWithTitle:@"Move to Trash" + action:@selector(menuMoveToTrash:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; [menu addItem:menuItem]; } - menuItem = [[[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteObject:) keyEquivalent:@""] autorelease]; - [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]]; + menuItem = [[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(menuDelete:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; [menu addItem:menuItem]; [menu addItem:[NSMenuItem separatorItem]]; } - // Get Info - menuItem = [[[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""] autorelease]; - [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]]; + // Refresh + menuItem = [[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refresh:) keyEquivalent:@""]; [menu addItem:menuItem]; + // Get Info + if (!firstMenuNode.sharingAccount || ([firstMenuNode class] != [PithosAccountNode class])) { + [menu addItem:[NSMenuItem separatorItem]]; + menuItem = [[NSMenuItem alloc] initWithTitle:(([firstMenuNode class] == [PithosContainerNode class]) ? @"Info" : @"Info and Sharing") + action:@selector(menuGetInfo:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; + [menu addItem:menuItem]; + + if ((!firstMenuNode.shared && !firstMenuNode.sharingAccount) || ([firstMenuNode class] != [PithosContainerNode class])) + [menu addItem:[NSMenuItem separatorItem]]; + } + // Cut + if (!firstMenuNode.shared && !firstMenuNode.sharingAccount) { + if (menuNodesCount == 1) + menuItemTitle = [NSString stringWithFormat:@"Cut \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName]; + else + menuItemTitle = [NSString stringWithFormat:@"Cut %lu Items", menuNodesCount]; + menuItem = [[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCut:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; + [menu addItem:menuItem]; + } + // Copy + if ((!firstMenuNode.shared && !firstMenuNode.sharingAccount) || + (([firstMenuNode class] != [PithosContainerNode class]) && ([firstMenuNode class] != [PithosAccountNode class]))) { + if (menuNodesCount == 1) + menuItemTitle = [NSString stringWithFormat:@"Copy \"%@\"", ((PithosNode *)[menuNodes objectAtIndex:0]).displayName]; + else + menuItemTitle = [NSString stringWithFormat:@"Copy %lu Items", menuNodesCount]; + menuItem = [[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuCopy:) keyEquivalent:@""]; + [menuItem setRepresentedObject:menuNodes]; + [menu addItem:menuItem]; + } + // Paste + if (clipboardNodes && !firstMenuNode.shared && !firstMenuNode.sharingAccount && (menuNodesCount == 1) && + ([firstMenuNode class] == [PithosSubdirNode class]) && + (firstMenuNode.pithosObject.subdir || ![firstMenuNode.pithosObject.name hasSuffix:@"/"])) { + NSUInteger clipboardNodesCount = [clipboardNodes count]; + if (clipboardNodesCount == 0) { + self.clipboardNodes = nil; + } else if (clipboardCopy || ![firstMenuNode isEqualTo:clipboardParentNode]) { + if (clipboardNodesCount == 1) + menuItemTitle = @"Paste Item"; + else + menuItemTitle = @"Paste Items"; + menuItem = [[NSMenuItem alloc] initWithTitle:menuItemTitle action:@selector(menuPaste:) keyEquivalent:@""]; + [menuItem setRepresentedObject:firstMenuNode]; + [menu addItem:menuItem]; + } + } } } #pragma mark - -#pragma mark Menu Actions - -- (void)getInfo:(NSMenuItem *)sender { - [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender]; -} +#pragma mark NSMenuValidation -- (void)deleteObject:(NSMenuItem *)sender { - PithosNode *node = (PithosNode *)[sender representedObject]; - if (([node class] == [PithosObjectNode class]) || - (([node class] == [PithosSubdirNode class]) && - !node.pithosObject.subdir && - [node.pithosObject.name hasSuffix:@"/"])) { - ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name - objectName:node.pithosObject.name]; - objectRequest.delegate = self; - objectRequest.didFinishSelector = @selector(deleteObjectFinished:); - objectRequest.didFailSelector = @selector(deleteObjectFailed:); - [objectRequest startAsynchronous]; - } else if ([node class] == [PithosSubdirNode class]) { - NSArray *objectRequests = [PithosFileUtilities deleteObjectRequestsForSubdirWithContainerName:node.pithosContainer.name - objectName:node.pithosObject.name]; - if (objectRequests) { - for (ASIPithosObjectRequest *objectRequest in objectRequests) { - objectRequest.delegate = self; - objectRequest.didFinishSelector = @selector(deleteObjectFinished:); - objectRequest.didFailSelector = @selector(deleteObjectFailed:); - [objectRequest startAsynchronous]; - } +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if ((menuItem.action == @selector(cut:)) || (menuItem.action == @selector(copy:)) || (menuItem.action == @selector(delete:))) { + NSArray *menuNodesIndexPaths = [browser selectionIndexPaths]; + if ([menuNodesIndexPaths count] == 0) + return NO; + + PithosNode *firstMenuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]]; + if (((menuItem.action == @selector(cut:)) && (firstMenuNode.shared || firstMenuNode.sharingAccount)) || + ((menuItem.action == @selector(copy:)) && (firstMenuNode.shared || firstMenuNode.sharingAccount) && + (([firstMenuNode class] == [PithosContainerNode class]) || ([firstMenuNode class] == [PithosAccountNode class]))) || + ((menuItem.action == @selector(delete:)) && + (firstMenuNode.shared || firstMenuNode.sharingAccount || ([rootNode class] != [PithosContainerNode class]) || + ((menuItem.tag == 0) && ![rootNode.pithosContainer.name isEqualToString:@"pithos"])))) + return NO; + + NSMutableArray *menuNodes = [NSMutableArray arrayWithCapacity:[menuNodesIndexPaths count]]; + for (NSIndexPath *nodeIndexPath in menuNodesIndexPaths) { + [menuNodes addObject:[browser itemAtIndexPath:nodeIndexPath]]; } + menuItem.representedObject = menuNodes; + } else if (menuItem.action == @selector(paste:)) { + if (!clipboardNodes || ![clipboardNodes count]) + return NO; - } + NSArray *menuNodesIndexPaths = [browser selectionIndexPaths]; + PithosNode *menuNode; + if ([menuNodesIndexPaths count] == 0) + menuNode = [browser parentForItemsInColumn:0]; + else if (([menuNodesIndexPaths count] != 1) || + ([[browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]] class] == [PithosObjectNode class])) + menuNode = [browser parentForItemsInColumn:([[menuNodesIndexPaths objectAtIndex:0] length] - 1)]; + else + menuNode = [browser itemAtIndexPath:[menuNodesIndexPaths objectAtIndex:0]]; + + if (menuNode.shared || menuNode.sharingAccount || + (([menuNode class] != [PithosContainerNode class]) && + (([menuNode class] != [PithosSubdirNode class]) || + (!menuNode.pithosObject.subdir && [menuNode.pithosObject.name hasSuffix:@"/"]))) || + (!clipboardCopy && [menuNode isEqualTo:clipboardParentNode])) + return NO; + + menuItem.representedObject = menuNode; + } + return YES; +} + +- (void)cut:(NSMenuItem *)sender { + [self menuCut:sender]; } -- (void)moveToTrash:(NSMenuItem *)sender { +- (void)copy:(NSMenuItem *)sender { + [self menuCopy:sender]; +} + +- (void)paste:(NSMenuItem *)sender { + [self menuPaste:sender]; +} + +- (void)delete:(NSMenuItem *)sender { + if (sender.tag == 0) + [self menuMoveToTrash:sender]; + else + [self menuDelete:sender]; +} + +#pragma mark - +#pragma mark Menu Actions + +- (void)menuNewFolder:(NSMenuItem *)sender { PithosNode *node = (PithosNode *)[sender representedObject]; - if (([node class] == [PithosObjectNode class]) || - (([node class] == [PithosSubdirNode class]) && - !node.pithosObject.subdir && - [node.pithosObject.name hasSuffix:@"/"])) { - NSString *safeObjectName = [PithosFileUtilities safeObjectNameForContainerName:@"trash" - objectName:node.pithosObject.name]; - if (safeObjectName) { - ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest moveObjectDataRequestWithContainerName:node.pithosContainer.name - objectName:node.pithosObject.name - contentType:nil - contentEncoding:nil - contentDisposition:nil - manifest:nil - sharing:nil - isPublic:ASIPithosObjectRequestPublicIgnore - metadata:nil - destinationContainerName:@"trash" - destinationObjectName:safeObjectName]; - objectRequest.delegate = self; - objectRequest.didFinishSelector = @selector(moveToTrashFinished:); - objectRequest.didFailSelector = @selector(moveToTrashFailed:); - [objectRequest startAsynchronous]; - } - } else if ([node class] == [PithosSubdirNode class]) { - NSString *safeObjectName = [PithosFileUtilities safeSubdirNameForContainerName:@"trash" - subdirName:node.pithosObject.name]; - if (safeObjectName) { - NSArray *objectRequests = [PithosFileUtilities moveObjectRequestsForSubdirWithContainerName:node.pithosContainer.name - objectName:node.pithosObject.name - destinationContainerName:@"trash" - destinationObjectName:safeObjectName]; - if (objectRequests) { - for (ASIPithosObjectRequest *objectRequest in objectRequests) { - objectRequest.delegate = self; - objectRequest.didFinishSelector = @selector(moveToTrashFinished:); - objectRequest.didFailSelector = @selector(moveToTrashFailed:); - [objectRequest startAsynchronous]; - } + if ([node class] == [PithosContainerNode class]) { + // Operation: Create (upload) a new root application/directory object + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos + containerName:node.pithosContainer.name + subdirName:@"untitled folder"]; + NSString *fileName = [safeObjectName lastPathComponent]; + if (operation.isCancelled) + return; + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:safeObjectName + eTag:nil + contentType:@"application/directory" + contentEncoding:nil + contentDisposition:nil + manifest:nil + sharing:nil + isPublic:ASIPithosObjectRequestPublicIgnore + metadata:nil + data:[NSData data]]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory + message:messagePrefix]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + fileName, @"fileName", + [NSArray arrayWithObject:node], @"refreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + uploadNetworkQueue, @"networkQueue", + @"upload", @"operationType", + nil]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + }]; + [uploadQueue addOperation:operation]; + } else if (([node class] == [PithosSubdirNode class]) && + (node.pithosObject.subdir || ![node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Create (upload) a new aplication/directory object + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos + containerName:node.pithosContainer.name + subdirName:[node.pithosObject.name stringByAppendingPathComponent:@"untitled folder"]]; + NSString *fileName = [safeObjectName lastPathComponent]; + if (operation.isCancelled) + return; + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:safeObjectName + eTag:nil + contentType:@"application/directory" + contentEncoding:nil + contentDisposition:nil + manifest:nil + sharing:nil + isPublic:ASIPithosObjectRequestPublicIgnore + metadata:nil + data:[NSData data]]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Creating directory '%@'", fileName]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory + message:messagePrefix]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + fileName, @"fileName", + [NSArray arrayWithObject:node], @"refreshNodes", + [NSNumber numberWithBool:YES], @"refresh", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + uploadNetworkQueue, @"networkQueue", + @"upload", @"operationType", + nil]; + [uploadNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; } - } + }]; + [uploadQueue addOperation:operation]; } } -#pragma mark - -#pragma mark Menu Actions ASIHTTPRequestDelegate +- (void)menuGetInfo:(NSMenuItem *)sender { + for (PithosNode *node in ((NSArray *)[sender representedObject])) { + [node showPithosNodeInfo:sender]; + } +} -- (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest { - if (objectRequest.responseStatusCode == 204) { - NSLog(@"Object deleted: %@", [objectRequest url]); - [self refresh:nil]; +- (void)menuDownload:(NSMenuItem *)sender { + NSArray *nodes = (NSArray *)[sender representedObject]; + PithosNode *firstNode = [nodes objectAtIndex:0]; + if (([nodes count] == 1) && ([firstNode class] == [PithosObjectNode class])) { + NSSavePanel *save = [NSSavePanel savePanel]; + save.nameFieldStringValue = firstNode.displayName; + NSInteger result = [save runModal]; + if (result == NSOKButton) { + NSString *destinationPath = save.URL.path; + NSString *directoryPath = [destinationPath stringByDeletingLastPathComponent]; + NSString *newFileName = [destinationPath lastPathComponent]; + if ([destinationPath hasSuffix:@"/"]) + newFileName = [newFileName stringByAppendingString:@"/"]; + if ([firstNode.displayName isEqualToString:newFileName]) + newFileName = nil; + [self downloadNode:firstNode toDirectory:directoryPath withNewFileName:newFileName version:nil checkIfExists:NO]; + } } else { - [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest]; + NSOpenPanel *open = [NSOpenPanel openPanel]; + open.canChooseFiles = NO; + open.canChooseDirectories = YES; + open.canCreateDirectories = YES; + NSInteger result = [open runModal]; + if (result == NSOKButton) { + NSString *directoryPath = open.URL.path; + for (PithosNode *node in nodes) { + [self downloadNode:node toDirectory:directoryPath withNewFileName:nil version:nil checkIfExists:YES]; + } + } } } -- (void)deleteObjectFailed:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Delete of object failed"); - [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest]; +- (void)menuDelete:(NSMenuItem *)sender { + for (PithosNode *node in ((NSArray *)[sender representedObject])) { + if (([node class] == [PithosObjectNode class]) || + (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Delete an object or subdir/ node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *fileName = [node.pithosObject.name lastPathComponent]; + if ([node.pithosObject.name hasSuffix:@"/"]) + fileName = [fileName stringByAppendingString:@"/"]; + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name]; + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", fileName]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete + message:messagePrefix]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + fileName, @"fileName", + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + deleteNetworkQueue, @"networkQueue", + @"delete", @"operationType", + nil]; + [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + }]; + [deleteQueue addOperation:operation]; + } else if ([node class] == [PithosSubdirNode class]) { + // Operation: Delete a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSArray *objectRequests = [PithosUtilities deleteObjectRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Deleting '%@'", [objectRequest.userInfo objectForKey:@"fileName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + deleteNetworkQueue, @"networkQueue", + @"delete", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [deleteNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + }]; + [deleteQueue addOperation:operation]; + } + } } -- (void)moveToTrashFinished:(ASIPithosObjectRequest *)objectRequest { - if (objectRequest.responseStatusCode == 201) { - NSLog(@"Object moved: %@", [objectRequest url]); - [self refresh:nil]; - } else { - [PithosFileUtilities unexpectedResponseStatusAlertWithRequest:objectRequest]; +- (void)menuMoveToTrash:(NSMenuItem *)sender { + for (PithosNode *node in ((NSArray *)[sender representedObject])) { + if (([node class] == [PithosObjectNode class]) || + (([node class] == [PithosSubdirNode class]) && + !node.pithosObject.subdir && + [node.pithosObject.name hasSuffix:@"/"])) { + // Operation: Move to trash an object or subdir/ node + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *safeObjectName = [PithosUtilities safeObjectNameForPithos:pithos + containerName:@"trash" + objectName:node.pithosObject.name]; + if (!operation.isCancelled && safeObjectName) { + ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:@"trash" + destinationObjectName:safeObjectName + checkIfExists:NO]; + if (!operation.isCancelled && objectRequest) { + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + }]; + [moveQueue addOperation:operation]; + } else if ([node class] == [PithosSubdirNode class]) { + // Operation: Move to trash a subdir node and its descendants + // The resulting ASIPithosObjectRequests are chained through dependencies + __block NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { + if (operation.isCancelled) + return; + NSString *safeObjectName = [PithosUtilities safeSubdirNameForPithos:pithos + containerName:@"trash" + subdirName:node.pithosObject.name]; + if (!operation.isCancelled && safeObjectName) { + NSArray *objectRequests = [PithosUtilities moveObjectRequestsForSubdirWithPithos:pithos + containerName:node.pithosContainer.name + objectName:node.pithosObject.name + destinationContainerName:@"trash" + destinationObjectName:safeObjectName + checkIfExists:NO]; + if (!operation.isCancelled && objectRequests) { + ASIPithosObjectRequest *previousObjectRequest = nil; + for (ASIPithosObjectRequest *objectRequest in objectRequests) { + if (operation.isCancelled) + return; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); + objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); + NSString *messagePrefix = [NSString stringWithFormat:@"Moving '%@/%@' to '%@/%@'", + [objectRequest.userInfo objectForKey:@"sourceContainerName"], + [objectRequest.userInfo objectForKey:@"sourceObjectName"], + [objectRequest.userInfo objectForKey:@"destinationContainerName"], + [objectRequest.userInfo objectForKey:@"destinationObjectName"]]; + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityMove + message:messagePrefix]; + [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + activity, @"activity", + [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", + [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", + [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + NSStringFromSelector(@selector(moveFinished:)), @"didFinishSelector", + NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", + moveNetworkQueue, @"networkQueue", + @"move", @"operationType", + nil]]; + if (previousObjectRequest) + [objectRequest addDependency:previousObjectRequest]; + previousObjectRequest = objectRequest; + [moveNetworkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } + } + }]; + [moveQueue addOperation:operation]; + } } } -- (void)moveToTrashFailed:(ASIPithosObjectRequest *)objectRequest { - NSLog(@"Move of object failed"); - [PithosFileUtilities httpRequestErrorAlertWithRequest:objectRequest]; +- (void)menuCut:(NSMenuItem *)sender { + self.clipboardNodes = (NSArray *)[sender representedObject]; + self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent; + self.clipboardCopy = NO; } +- (void)menuCopy:(NSMenuItem *)sender { + self.clipboardNodes = (NSArray *)[sender representedObject]; + self.clipboardParentNode = ((PithosNode *)[clipboardNodes objectAtIndex:0]).parent; + self.clipboardCopy = YES; +} + +- (void)menuPaste:(NSMenuItem *)sender { + if (!clipboardNodes || ![clipboardNodes count]) + return; + PithosNode *dropNode = (PithosNode *)[sender representedObject]; + NSArray *localClipboardNodes = [NSArray arrayWithArray:clipboardNodes]; + if (clipboardCopy) { + [self cpyNodes:localClipboardNodes toNode:dropNode]; + } else if (![dropNode isEqualTo:clipboardParentNode]) { + self.clipboardNodes = nil; + self.clipboardParentNode = nil; + [self moveNodes:localClipboardNodes toNode:dropNode]; + } +} + +#pragma mark - +#pragma mark PithosActivityFacilityDelegate + +- (void)activityUpdate:(NSDictionary *)info { + NSString *message = [info objectForKey:@"message"]; + NSUInteger runningActivitiesCount = [[info objectForKey:@"runningActivitiesCount"] unsignedIntegerValue]; +// NSUInteger endingActivitiesCount = [[info objectForKey:@"endingActivitiesCount"] unsignedIntegerValue]; + NSUInteger totalUploadBytes = [[info objectForKey:@"totalUploadBytes"] unsignedIntegerValue]; + NSUInteger currentUploadBytes = [[info objectForKey:@"currentUploadBytes"] unsignedIntegerValue]; + NSUInteger totalDownloadBytes = [[info objectForKey:@"totalDownloadBytes"] unsignedIntegerValue]; + NSUInteger currentDownloadBytes = [[info objectForKey:@"currentDownloadBytes"] unsignedIntegerValue]; + NSUInteger totalBytes = totalUploadBytes + totalDownloadBytes; + if (runningActivitiesCount && totalBytes) { + [activityProgressIndicator setDoubleValue:((currentUploadBytes + currentDownloadBytes + 0.0)/(totalBytes + 0.0))]; + [activityProgressIndicator startAnimation:self]; + } else { + [activityProgressIndicator setDoubleValue:1.0]; + [activityProgressIndicator stopAnimation:self]; + } + + if (!message) + message = [[[UsingSizeTransformer alloc] init] transformedValue:accountNode.pithosAccount]; + [activityTextField setStringValue:message]; +} @end \ No newline at end of file