51b5c6134681f368fd2326a7f30d65f2340d6972
[pithos-macos] / pithos-macos / PithosAccountNode.m
1 //
2 //  PithosAccountNode.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosAccountNode.h"
39 #import "PithosContainerNode.h"
40 #import "ASIPithos.h"
41 #import "ASIPithosAccountRequest.h"
42 #import "ASIPithosAccount.h"
43 #import "ASIPithosContainer.h"
44 #import "ASIDownloadCache.h"
45 #import "PithosAccount.h"
46 #import "PithosUtilities.h"
47 #import "PithosActivityFacility.h"
48
49 static NSImage *sharedIcon = nil;
50
51 @implementation PithosAccountNode
52 @synthesize pithos, pithosAccount, translatedGroups;
53
54 + (void)initialize {
55         if (self == [PithosAccountNode class])
56         sharedIcon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)];
57 }
58
59 #pragma mark -
60 #pragma mark Object Lifecycle
61
62 - (id)initWithPithos:(ASIPithos *)aPithos {
63     if ((self = [super init])) {
64         pithos = aPithos;
65     }
66     return self;
67 }
68
69 - (void)dealloc {
70     [accountRequest clearDelegatesAndCancel];
71     [refreshMetadataAccountRequest clearDelegatesAndCancel];
72     [applyMetadataAccountRequest clearDelegatesAndCancel];
73 }
74
75 #pragma mark -
76 #pragma mark Properties
77
78 - (void)setPithos:(ASIPithos *)aPithos {
79     if (aPithos && ![aPithos isEqualTo:pithos]) {
80         pithos = aPithos;
81         url = nil;
82         [accountRequest clearDelegatesAndCancel];
83         accountRequest = nil;
84         [refreshMetadataAccountRequest clearDelegatesAndCancel];
85         refreshMetadataAccountRequest = nil;
86         [applyMetadataAccountRequest clearDelegatesAndCancel];
87         applyMetadataAccountRequest = nil;
88         reset = YES;
89     }
90 }
91
92 - (void)setPithosAccount:(ASIPithosAccount *)aPithosAccount {
93     if (!aPithosAccount) {
94         pithosAccount = aPithosAccount;
95         self.translatedGroups = [NSMutableDictionary dictionary];
96     } else if (![aPithosAccount isEqualTo:pithosAccount]) {
97         pithosAccount = aPithosAccount;
98         if (pithosAccountManager) {
99             NSMutableArray *UUIDs = [NSMutableArray array];
100             for (NSString *groupName in pithosAccount.groups) {
101                 [UUIDs addObjectsFromArray:[pithosAccount.groups objectForKey:groupName]];
102             }
103             if (UUIDs.count) {
104                 [pithosAccountManager updateUserCatalogForForDisplaynames:nil UUIDs:UUIDs];
105             }
106             
107             NSMutableDictionary *newTranslatedGroups = [NSMutableDictionary dictionaryWithCapacity:pithosAccount.groups.count];
108             for (NSString *groupName in pithosAccount.groups) {
109                 NSMutableArray *groupUsers = [NSMutableArray array];
110                 for (NSString *UUID in [pithosAccount.groups objectForKey:groupName]) {
111                     [groupUsers addObject:[pithosAccountManager displaynameForUUID:UUID safe:YES]];
112                 }
113                 [newTranslatedGroups setObject:groupUsers forKey:groupName];
114             }
115             self.translatedGroups = newTranslatedGroups;
116         } else {
117             self.translatedGroups = [pithosAccount.groups copy];
118         }
119     }
120 }
121
122 - (NSString *)url {
123     if (url == nil)
124         url = [[NSString alloc] initWithFormat:@"%@%@", 
125                (sharingAccount ? [pithos storageURLWithAuthUser:sharingAccount] : pithos.storageURL), 
126                (shared ? @"?shared" : @"")];
127     return url;
128 }
129
130 - (NSArray *)children {
131     @synchronized(self) {
132         if (reset) {
133             [accountRequest clearDelegatesAndCancel];
134             accountRequest = nil;
135             [refreshMetadataAccountRequest clearDelegatesAndCancel];
136             refreshMetadataAccountRequest = nil;
137             [applyMetadataAccountRequest clearDelegatesAndCancel];
138             applyMetadataAccountRequest = nil;
139             children = nil;
140             newChildren = nil;
141             self.pithosAccount = nil;
142             freshness = PithosNodeStateRefreshNeeded;
143             forcedRefresh = YES;
144             reset = NO;
145             [self postChildrenUpdatedNotificationName];
146         }
147         switch (freshness) {
148             case PithosNodeStateFresh:
149                 break;
150             case PithosNodeStateRefreshNeeded:
151                 freshness = PithosNodeStateRefreshing;
152                 accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithos 
153                                                                                      limit:0 
154                                                                                     marker:nil 
155                                                                                     shared:shared 
156                                                                                      until:nil];
157                 if (sharingAccount)
158                     [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
159                 else if (!forcedRefresh)
160                     accountRequest.downloadCache = [ASIDownloadCache sharedCache];
161                 accountRequest.delegate = self;
162                 accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
163                 accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
164                 accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
165                                            [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
166                                            [NSNumber numberWithUnsignedInteger:10], @"retries", 
167                                            NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
168                                            NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", 
169                                            nil];
170                 [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
171                 break;
172             case PithosNodeStateRefreshing:
173                 break;
174             case PithosNodeStateRefreshFinished:
175                 if (newChildren) {
176                     children = newChildren;
177                     newChildren = nil;
178                 }
179                 freshness = PithosNodeStateFresh;
180             default:
181                 break;
182         }
183         return children;
184     }
185 }
186
187 - (NSString *)displayName {
188     if (displayName == nil)
189         return [NSString stringWithString:(sharingAccount ? sharingAccount : @"account")];
190     return [displayName copy];
191 }
192
193 - (NSImage *)icon {
194     if (icon == nil)
195         icon = sharedIcon;
196     return icon;
197 }
198
199 #pragma mark -
200 #pragma mark ASIHTTPRequestDelegate
201
202 - (void)accountRequestFailed:(ASIPithosAccountRequest *)request {
203     @autoreleasepool {
204         NSString *message;
205         NSError *error = [accountRequest error];
206         if (error)
207             message = [NSString stringWithFormat:@"Account listing %@ failed: %@", accountRequest.url, [error localizedDescription]];
208         else
209             message = [NSString stringWithFormat:@"Account listing %@ failed: (%d) %@", 
210                        accountRequest.url, accountRequest.responseStatusCode, accountRequest.responseStatusMessage];
211         dispatch_async(dispatch_get_main_queue(), ^{
212             [[PithosActivityFacility defaultPithosActivityFacility] startAndEndActivityWithType:PithosActivityOther message:message];
213         });
214         NSUInteger retries = [[accountRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
215         if (retries > 0) {
216             ASIPithosAccountRequest *newAccountRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:accountRequest];
217             [(NSMutableDictionary *)(newAccountRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
218             accountRequest = newAccountRequest;
219             [[PithosUtilities prepareRequest:accountRequest priority:[[accountRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
220         } else {
221             newChildren = nil;
222             accountRequest = nil;
223             containers = nil;
224             forcedRefresh = NO;
225             @synchronized(self) {
226                 freshness = PithosNodeStateRefreshNeeded;
227             }
228         }
229     }
230 }
231
232 - (void)accountRequestFinished:(ASIPithosAccountRequest *)request {
233     @autoreleasepool {
234         DLog(@"List account finished: %@", [accountRequest url]);
235         DLog(@"Cached: %d", [accountRequest didUseCachedResponse]);
236         if (accountRequest.responseStatusCode == 200) {
237             self.pithosAccount = [accountRequest account];
238             
239             NSArray *someContainers = [accountRequest containers];
240             if (containers == nil) {
241                 containers = [[NSMutableArray alloc] initWithArray:someContainers];
242             } else {
243                 [containers addObjectsFromArray:someContainers];
244             }
245             if ([someContainers count] < 10000) {
246                 if (!accountRequest.didUseCachedResponse || ([containers count] != [someContainers count]) || !children) {
247                     // Save new children
248                     DLog(@"using newChildren");
249                     newChildren = [[NSMutableArray alloc] init];
250                     NSMutableIndexSet *keptNodes = [NSMutableIndexSet indexSet];
251                     for (ASIPithosContainer *container in containers) {
252                         PithosContainerNode *node = [[PithosContainerNode alloc] initWithPithos:pithos pithosContainer:container];
253                         node.parent = self;
254                         node.shared = shared;
255                         node.sharingAccount = sharingAccount;
256                         node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
257                         if (children) {
258                             NSUInteger oldIndex = [children indexOfObject:node];
259                             if (oldIndex != NSNotFound) {
260                                 // Use the same pointer value, if possible
261                                 node = [children objectAtIndex:oldIndex];
262     //                            node.pithosContainer = container;
263                                 [node setLimitedPithosContainer:container];
264                                 [keptNodes addIndex:oldIndex];
265                             }
266                         }
267                         [newChildren addObject:node];
268                     }
269                     [[children objectsAtIndexes:
270                       [[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [children count])] indexesPassingTest:^(NSUInteger idx, BOOL *stop){
271                         if ([keptNodes containsIndex:idx])
272                             return NO;
273                         return YES;
274                     }]] makeObjectsPerformSelector:@selector(pithosNodeWillBeRemoved)];
275                 }
276                 // Else cache was used and all results were fetched during this request, so existing children can be reused
277                 accountRequest = nil;
278                 containers = nil;
279                 forcedRefresh = NO;
280                 @synchronized(self) {
281                     freshness = PithosNodeStateRefreshFinished;
282                 }
283                 [self postChildrenUpdatedNotificationName];
284             } else {
285                 // Do an additional request to fetch more objects
286                 accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithos 
287                                                                                      limit:0 
288                                                                                     marker:[[someContainers lastObject] name] 
289                                                                                     shared:shared 
290                                                                                      until:nil];
291                 if (sharingAccount)
292                     [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
293                 else if (!forcedRefresh)
294                     accountRequest.downloadCache = [ASIDownloadCache sharedCache];
295                 accountRequest.delegate = self;
296                 accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
297                 accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
298                 accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
299                                            [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
300                                            [NSNumber numberWithUnsignedInteger:10], @"retries", 
301                                            NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
302                                            NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", 
303                                            nil];
304                 [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
305             }
306         } else if (accountRequest.responseStatusCode == 304) {
307             // Account is not modified, so existing children can be reused
308             accountRequest = nil;
309             containers = nil;
310             forcedRefresh = NO;
311             @synchronized(self) {
312                 freshness = PithosNodeStateRefreshFinished;
313             }
314             [self postChildrenUpdatedNotificationName];
315         } else {
316             [self accountRequestFailed:accountRequest];
317         }
318     }
319 }
320
321 - (void)accountMetadataRequestFinished:(ASIPithosAccountRequest *)request {
322     @autoreleasepool {
323         DLog(@"URL: %@", [request url]);
324         DLog(@"cached: %d", [request didUseCachedResponse]);
325         
326         if ([request isEqualTo:applyMetadataAccountRequest]) {
327             @synchronized(self) {
328                 applyMetadataAccountRequest = nil;
329             }
330             [self refreshInfo];
331         } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
332             self.pithosAccount = [refreshMetadataAccountRequest account];
333             @synchronized(self) {
334                 refreshMetadataAccountRequest = nil;
335             }
336         }
337     }
338 }
339
340 - (void)accountMetadataRequestFailed:(ASIPithosAccountRequest *)request {
341     @autoreleasepool {
342         NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
343         if (retries > 0) {
344             ASIPithosAccountRequest *newRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:request];
345             [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
346             if ([request isEqualTo:applyMetadataAccountRequest]) {
347                 @synchronized(self) {
348                     applyMetadataAccountRequest = newRequest;
349                 }
350             } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
351                 @synchronized(self) {
352                     refreshMetadataAccountRequest = newRequest;
353                 }
354             }
355             [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
356         } else {
357             if ([request isEqualTo:applyMetadataAccountRequest]) {
358                 [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataAccountRequest];
359                 @synchronized(self) {
360                     applyMetadataAccountRequest = nil;
361                 }
362             } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
363                 [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataAccountRequest];
364                 @synchronized(self) {
365                     refreshMetadataAccountRequest = nil;
366                 }
367             }
368         }
369     }
370 }
371
372 #pragma mark -
373 #pragma mark Info
374
375 - (void)applyInfo {
376     @synchronized(self) {
377         if (applyMetadataAccountRequest == nil) {
378             NSMutableDictionary *groups = [NSMutableDictionary dictionary];
379             if (translatedGroups.count) {
380                 for (NSString *groupName in translatedGroups) {
381                     if (!groupName.length ||
382                         [groupName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" -_~,;"]].location != NSNotFound) {
383                         NSAlert *alert = [[NSAlert alloc] init];
384                         [alert setMessageText:@"Invalid Input"];
385                         [alert setInformativeText:@"Group names cannot be empty or contain ' ', '-', '_', '~', ',' or ';'."];
386                         [alert addButtonWithTitle:@"OK"];
387                         [alert runModal];
388                         return;
389                     }
390                 }
391                 if (pithosAccountManager) {
392                     NSMutableSet *allGroupUsers = [NSMutableSet set];
393                     for (NSMutableArray *groupUsers in [translatedGroups objectEnumerator]) {
394                         [allGroupUsers addObjectsFromArray:groupUsers];
395                     }
396                     [allGroupUsers removeObject:@""];
397                     [allGroupUsers removeObject:@"*"];
398                     if (allGroupUsers.count) {
399                         ASIPithosRequest *userCatalogRequest = [pithosAccountManager updateUserCatalogForForDisplaynames:[allGroupUsers allObjects]
400                                                                                                                         UUIDs:nil];
401                         if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
402                             return;
403                         } else if (userCatalogRequest.responseStatusCode == 200) {
404                             // Check if all users exist.
405                             NSDictionary *displaynameCatalog = [userCatalogRequest displaynameCatalog];
406                             NSMutableArray *inexistentGroupUsers = [NSMutableArray array];
407                             for (NSString *groupUser in allGroupUsers) {
408                                 if (![displaynameCatalog objectForKey:groupUser]) {
409                                     [inexistentGroupUsers addObject:groupUser];
410                                 }
411                             }
412                             if (!inexistentGroupUsers.count) {
413                                 // create groups
414                                 for (NSString *groupName in translatedGroups) {
415                                     NSMutableArray *groupUsers = [NSMutableArray array];
416                                     for (NSString *groupUser in [translatedGroups objectForKey:groupName]) {
417                                         [groupUsers addObject:([groupUser isEqualToString:@"*"] ?
418                                                                @"*" : [displaynameCatalog objectForKey:groupUser])];
419                                     }
420                                     [groups setObject:groupUsers forKey:groupName];
421                                 }
422                             } else {
423                                 NSAlert *alert = [[NSAlert alloc] init];
424                                 if (inexistentGroupUsers.count == 1) {
425                                     [alert setMessageText:@"Invalid User"];
426                                     [alert setInformativeText:[NSString stringWithFormat:@"User '%@' doesn't exist.", [inexistentGroupUsers objectAtIndex:0]]];
427                                 } else {
428                                     [alert setMessageText:@"Invalid Users"];
429                                     [alert setInformativeText:[NSString stringWithFormat:@"Users '%@' don't exist.", [inexistentGroupUsers componentsJoinedByString:@"', '"]]];
430                                 }
431                                 [alert addButtonWithTitle:@"OK"];
432                                 [alert runModal];
433                                 return;
434                             }
435                         } else {
436                             // 404. Since we don't translate to UUIDs, check for invalid chars.
437                             BOOL valid = YES;
438                             for (NSString *groupUser in allGroupUsers) {
439                                 if ([groupUser rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ~,;:"]].location != NSNotFound) {
440                                     valid = NO;
441                                     break;
442                                 }
443                             }
444                             if (valid) {
445                                 [groups addEntriesFromDictionary:translatedGroups];
446                             } else {
447                                 NSAlert *alert = [[NSAlert alloc] init];
448                                 [alert setMessageText:@"Invalid Input"];
449                                 [alert setInformativeText:@"Users cannot contain ' ', '~', ',', ';' or ':'."];
450                                 [alert addButtonWithTitle:@"OK"];
451                                 [alert runModal];
452                                 return;
453                             }
454                         }
455                     } else {
456                         [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
457                     }
458                 } else {
459                     [groups addEntriesFromDictionary:translatedGroups];
460                 }
461             } else {
462                 [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
463             }
464             
465             applyMetadataAccountRequest = [ASIPithosAccountRequest updateAccountMetadataRequestWithPithos:pithos
466                                                                                                     groups:groups 
467                                                                                                   metadata:pithosAccount.metadata 
468                                                                                                     update:NO];
469             applyMetadataAccountRequest.delegate = self;
470             applyMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
471             applyMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
472             applyMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
473                                                     [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
474                                                     [NSNumber numberWithUnsignedInteger:10], @"retries", 
475                                                     NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
476                                                     NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
477                                                     nil];
478             [[PithosUtilities prepareRequest:applyMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
479         }
480     }
481 }
482
483 - (void)refreshInfo {
484     @synchronized(self) {
485         if (refreshMetadataAccountRequest == nil) {
486             refreshMetadataAccountRequest = [ASIPithosAccountRequest accountMetadataRequestWithPithos:pithos];
487             refreshMetadataAccountRequest.delegate = self;
488             refreshMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
489             refreshMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
490             refreshMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
491                                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
492                                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
493                                                       NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
494                                                       NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
495                                                       nil];
496             if (!sharingAccount)
497                 refreshMetadataAccountRequest.downloadCache = [ASIDownloadCache sharedCache];
498             [[PithosUtilities prepareRequest:refreshMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
499         }
500     }
501 }
502
503 @end