// // PithosAccountNode.m // pithos-macos // // Copyright 2011-2013 GRNET S.A. All rights reserved. // // Redistribution and use in source and binary forms, with or // without modification, are permitted provided that the following // conditions are met: // // 1. Redistributions of source code must retain the above // copyright notice, this list of conditions and the following // disclaimer. // // 2. Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials // provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // The views and conclusions contained in the software and // documentation are those of the authors and should not be // interpreted as representing official policies, either expressed // or implied, of GRNET S.A. #import "PithosAccountNode.h" #import "PithosContainerNode.h" #import "ASIPithos.h" #import "ASIPithosAccountRequest.h" #import "ASIPithosAccount.h" #import "ASIPithosContainer.h" #import "ASIDownloadCache.h" #import "PithosAccount.h" #import "PithosUtilities.h" #import "PithosActivityFacility.h" static NSImage *sharedIcon = nil; @implementation PithosAccountNode @synthesize pithosAccount, accountRequest, applyMetadataAccountRequest, refreshMetadataAccountRequest, translatedGroups; + (void)initialize { if (self == [PithosAccountNode class]) sharedIcon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]; } #pragma mark - #pragma mark Object Lifecycle - (void)dealloc { [accountRequest clearDelegatesAndCancel]; [refreshMetadataAccountRequest clearDelegatesAndCancel]; [applyMetadataAccountRequest clearDelegatesAndCancel]; } #pragma mark - #pragma mark Internal - (void)updateGroups { if (!pithosAccount) { self.translatedGroups = [NSMutableDictionary dictionary]; } else if (pithosAccountManager) { NSMutableSet *UUIDs = [NSMutableSet set]; for (NSString *groupName in pithosAccount.groups) { [UUIDs addObjectsFromArray:[pithosAccount.groups objectForKey:groupName]]; } [UUIDs removeObject:@""]; [UUIDs removeObject:@"*"]; if (UUIDs.count) { [pithosAccountManager updateUserCatalogForDisplaynames:nil UUIDs:[UUIDs allObjects]]; } NSMutableDictionary *newTranslatedGroups = [NSMutableDictionary dictionaryWithCapacity:pithosAccount.groups.count]; for (NSString *groupName in pithosAccount.groups) { NSMutableArray *groupUsers = [NSMutableArray array]; for (NSString *UUID in [pithosAccount.groups objectForKey:groupName]) { [groupUsers addObject:[pithosAccountManager displaynameForUUID:UUID safe:YES]]; } [newTranslatedGroups setObject:groupUsers forKey:groupName]; } self.translatedGroups = newTranslatedGroups; } else { self.translatedGroups = [pithosAccount.groups copy]; } } #pragma mark - #pragma mark Actions - (void)reset { [accountRequest clearDelegatesAndCancel]; self.accountRequest = nil; [refreshMetadataAccountRequest clearDelegatesAndCancel]; self.refreshMetadataAccountRequest = nil; [applyMetadataAccountRequest clearDelegatesAndCancel]; self.applyMetadataAccountRequest = nil; children = nil; newChildren = nil; self.pithosAccount = nil; freshness = PithosNodeStateRefreshNeeded; forcedRefresh = YES; [self postChildrenUpdatedNotificationName]; [self children]; } #pragma mark - #pragma mark Properties - (void)setPithosAccount:(ASIPithosAccount *)aPithosAccount { if (![pithosAccount isEqualTo:aPithosAccount]) { pithosAccount = aPithosAccount; [self updateGroups]; } } - (NSString *)url { return [NSString stringWithFormat:@"@account@%@%@", (sharingAccount ? sharingAccount : pithosAccountManager.pithos.authUser), (shared ? @"?shared" : @"")]; } - (NSArray *)children { @synchronized(self) { switch (freshness) { case PithosNodeStateFresh: break; case PithosNodeStateRefreshNeeded: freshness = PithosNodeStateRefreshing; self.accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithosAccountManager.pithos limit:0 marker:nil shared:shared until:nil]; if (sharingAccount) [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos]; else if (!forcedRefresh) accountRequest.downloadCache = [ASIDownloadCache sharedCache]; accountRequest.delegate = self; accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", nil]; [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; break; case PithosNodeStateRefreshing: break; case PithosNodeStateRefreshFinished: if (newChildren) { children = newChildren; newChildren = nil; } freshness = PithosNodeStateFresh; default: break; } return children; } } - (NSString *)displayName { if (displayName == nil) { if (!sharingAccount) { return @"account"; } else if (pithosAccountManager) { return [pithosAccountManager displaynameForUUID:sharingAccount safe:YES]; } else { return [sharingAccount copy]; } } return [displayName copy]; } - (NSImage *)icon { if (icon == nil) icon = sharedIcon; return icon; } #pragma mark - #pragma mark ASIHTTPRequestDelegate - (void)accountRequestFailed:(ASIPithosAccountRequest *)request { @autoreleasepool { NSString *message; NSError *error = [accountRequest error]; if (error) message = [NSString stringWithFormat:@"Account listing %@ failed: %@", accountRequest.url, [error localizedDescription]]; else message = [NSString stringWithFormat:@"Account listing %@ failed: (%d) %@", accountRequest.url, accountRequest.responseStatusCode, accountRequest.responseStatusMessage]; dispatch_async(dispatch_get_main_queue(), ^{ [[PithosActivityFacility defaultPithosActivityFacility] startAndEndActivityWithType:PithosActivityOther message:message]; }); NSUInteger retries = [[accountRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue]; if (retries > 0) { ASIPithosAccountRequest *newAccountRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:accountRequest]; [(NSMutableDictionary *)(newAccountRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; self.accountRequest = newAccountRequest; [[PithosUtilities prepareRequest:accountRequest priority:[[accountRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; } else { newChildren = nil; self.accountRequest = nil; forcedRefresh = NO; @synchronized(self) { freshness = PithosNodeStateRefreshNeeded; } } } } - (void)accountRequestFinished:(ASIPithosAccountRequest *)request { @autoreleasepool { DLog(@"List account finished: %@", [accountRequest url]); DLog(@"Cached: %d", [accountRequest didUseCachedResponse]); if (accountRequest.responseStatusCode == 200) { self.pithosAccount = [accountRequest account]; NSMutableArray *containers = [accountRequest.userInfo objectForKey:@"containers"]; NSArray *someContainers = [accountRequest containers]; if (containers == nil) { containers = [NSMutableArray arrayWithArray:someContainers]; } else { [containers addObjectsFromArray:someContainers]; } if ([someContainers count] < 10000) { if (!accountRequest.didUseCachedResponse || ([containers count] != [someContainers count]) || !children) { // Save new children DLog(@"using newChildren"); newChildren = [[NSMutableArray alloc] init]; NSMutableIndexSet *keptNodes = [NSMutableIndexSet indexSet]; for (ASIPithosContainer *container in containers) { PithosContainerNode *node = [[PithosContainerNode alloc] initWithPithosAccountManager:pithosAccountManager pithosContainer:container]; node.parent = self; node.shared = shared; node.sharingAccount = sharingAccount; node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName; if (children) { NSUInteger oldIndex = [children indexOfObject:node]; if (oldIndex != NSNotFound) { // Use the same pointer value, if possible node = [children objectAtIndex:oldIndex]; // node.pithosContainer = container; [node setLimitedPithosContainer:container]; [keptNodes addIndex:oldIndex]; } } [newChildren addObject:node]; } [[children objectsAtIndexes: [[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [children count])] indexesPassingTest:^(NSUInteger idx, BOOL *stop){ if ([keptNodes containsIndex:idx]) return NO; return YES; }]] makeObjectsPerformSelector:@selector(pithosNodeWillBeRemoved)]; } // Else cache was used and all results were fetched during this request, so existing children can be reused self.accountRequest = nil; forcedRefresh = NO; @synchronized(self) { freshness = PithosNodeStateRefreshFinished; } [self postChildrenUpdatedNotificationName]; } else { // Do an additional request to fetch more objects self.accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithosAccountManager.pithos limit:0 marker:[[someContainers lastObject] name] shared:shared until:nil]; if (sharingAccount) [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos]; else if (!forcedRefresh) accountRequest.downloadCache = [ASIDownloadCache sharedCache]; accountRequest.delegate = self; accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", containers, @"containers", nil]; [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; } } else if (accountRequest.responseStatusCode == 304) { // Account is not modified, so existing children can be reused self.accountRequest = nil; forcedRefresh = NO; @synchronized(self) { freshness = PithosNodeStateRefreshFinished; } [self postChildrenUpdatedNotificationName]; } else { [self accountRequestFailed:accountRequest]; } } } - (void)accountMetadataRequestFinished:(ASIPithosAccountRequest *)request { @autoreleasepool { DLog(@"URL: %@", [request url]); DLog(@"cached: %d", [request didUseCachedResponse]); if ([request isEqualTo:applyMetadataAccountRequest]) { @synchronized(self) { self.applyMetadataAccountRequest = nil; } [self refreshInfo]; } else if ([request isEqualTo:refreshMetadataAccountRequest]) { self.pithosAccount = [refreshMetadataAccountRequest account]; @synchronized(self) { self.refreshMetadataAccountRequest = nil; } } } } - (void)accountMetadataRequestFailed:(ASIPithosAccountRequest *)request { @autoreleasepool { NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue]; if (retries > 0) { ASIPithosAccountRequest *newRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:request]; [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; if ([request isEqualTo:applyMetadataAccountRequest]) { @synchronized(self) { self.applyMetadataAccountRequest = newRequest; } } else if ([request isEqualTo:refreshMetadataAccountRequest]) { @synchronized(self) { self.refreshMetadataAccountRequest = newRequest; } } [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; } else { if ([request isEqualTo:applyMetadataAccountRequest]) { [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataAccountRequest]; @synchronized(self) { self.applyMetadataAccountRequest = nil; } } else if ([request isEqualTo:refreshMetadataAccountRequest]) { [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataAccountRequest]; @synchronized(self) { self.refreshMetadataAccountRequest = nil; } } } } } #pragma mark - #pragma mark Info - (void)applyInfo { @synchronized(self) { if (applyMetadataAccountRequest == nil) { NSMutableDictionary *groups = [NSMutableDictionary dictionary]; if (translatedGroups.count) { for (NSString *groupName in translatedGroups) { if (!groupName.length || [groupName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" -_~,;"]].location != NSNotFound) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Invalid Input"]; [alert setInformativeText:@"Group names cannot be empty or contain ' ', '-', '_', '~', ',' or ';'."]; [alert addButtonWithTitle:@"OK"]; [alert runModal]; return; } } if (pithosAccountManager) { NSMutableSet *allGroupUsers = [NSMutableSet set]; for (NSMutableArray *groupUsers in [translatedGroups objectEnumerator]) { [allGroupUsers addObjectsFromArray:groupUsers]; } [allGroupUsers removeObject:@""]; [allGroupUsers removeObject:@"*"]; if (allGroupUsers.count) { ASIPithosRequest *userCatalogRequest = [pithosAccountManager updateUserCatalogForDisplaynames:[allGroupUsers allObjects] UUIDs:nil]; if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) { return; } else if (userCatalogRequest.responseStatusCode == 200) { // Check if all users exist. NSDictionary *displaynameCatalog = [userCatalogRequest displaynameCatalog]; NSMutableArray *inexistentGroupUsers = [NSMutableArray array]; for (NSString *groupUser in allGroupUsers) { if (![displaynameCatalog objectForKey:groupUser]) { [inexistentGroupUsers addObject:groupUser]; } } if (!inexistentGroupUsers.count) { // Create groups. for (NSString *groupName in translatedGroups) { NSMutableArray *groupUsers = [NSMutableArray array]; for (NSString *groupUser in [translatedGroups objectForKey:groupName]) { [groupUsers addObject:([groupUser isEqualToString:@"*"] ? @"*" : [displaynameCatalog objectForKey:groupUser])]; } [groups setObject:groupUsers forKey:groupName]; } } else { NSAlert *alert = [[NSAlert alloc] init]; if (inexistentGroupUsers.count == 1) { [alert setMessageText:@"Invalid User"]; [alert setInformativeText:[NSString stringWithFormat:@"User '%@' doesn't exist.", [inexistentGroupUsers objectAtIndex:0]]]; } else { [alert setMessageText:@"Invalid Users"]; [alert setInformativeText:[NSString stringWithFormat:@"Users '%@' don't exist.", [inexistentGroupUsers componentsJoinedByString:@"', '"]]]; } [alert addButtonWithTitle:@"OK"]; [alert runModal]; return; } } else { // 404. Since we don't translate to UUIDs, check for invalid chars. BOOL valid = YES; for (NSString *groupUser in allGroupUsers) { if ([groupUser rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ~,;:"]].location != NSNotFound) { valid = NO; break; } } if (valid) { [groups addEntriesFromDictionary:translatedGroups]; } else { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Invalid Input"]; [alert setInformativeText:@"Users cannot contain ' ', '~', ',', ';' or ':'."]; [alert addButtonWithTitle:@"OK"]; [alert runModal]; return; } } } else { for (NSString *groupName in translatedGroups) { if ([[translatedGroups objectForKey:groupName] containsObject:@"*"]) { [groups setObject:[NSMutableArray arrayWithObject:@"*"] forKey:groupName]; } } if (!groups.count) { [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"]; } } } else { [groups addEntriesFromDictionary:translatedGroups]; } } else { [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"]; } self.applyMetadataAccountRequest = [ASIPithosAccountRequest updateAccountMetadataRequestWithPithos:pithosAccountManager.pithos groups:groups metadata:pithosAccount.metadata update:NO]; applyMetadataAccountRequest.delegate = self; applyMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); applyMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); applyMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", nil]; [[PithosUtilities prepareRequest:applyMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; } } } - (void)refreshInfo { @synchronized(self) { if (refreshMetadataAccountRequest == nil) { self.refreshMetadataAccountRequest = [ASIPithosAccountRequest accountMetadataRequestWithPithos:pithosAccountManager.pithos]; refreshMetadataAccountRequest.delegate = self; refreshMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); refreshMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); refreshMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", nil]; if (!sharingAccount) refreshMetadataAccountRequest.downloadCache = [ASIDownloadCache sharedCache]; [[PithosUtilities prepareRequest:refreshMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; } } } @end