Refactor use of pithos objects to accommodate
[pithos-macos] / pithos-macos / PithosContainerNode.m
1 //
2 //  PithosContainerNode.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2013 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 "PithosContainerNode.h"
39 #import "PithosObjectNode.h"
40 #import "PithosSubdirNode.h"
41 #import "ASIPithos.h"
42 #import "ASIPithosContainerRequest.h"
43 #import "ASIPithosContainer.h"
44 #import "ASIPithosObject.h"
45 #import "ASIDownloadCache.h"
46 #import "PithosUtilities.h"
47 #import "PithosAccount.h"
48 #import "PithosContainerNodeInfoController.h"
49 #import "PithosActivityFacility.h"
50
51 static NSImage *sharedIcon = nil;
52
53 @implementation PithosContainerNode
54 @synthesize pithosContainer, containerRequest, prefix, applyMetadataContainerRequest, refreshMetadataContainerRequest;
55 @synthesize policyVersioning, policyQuota;
56
57 + (void)initialize {
58         if (self == [PithosContainerNode class])
59         sharedIcon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericHardDiskIcon)];
60 }
61
62 #pragma mark -
63 #pragma mark Object Lifecycle
64
65 - (id)initWithPithosAccountManager:(PithosAccount *)aPithosAccountManager pithosContainer:(ASIPithosContainer *)aPithosContainer icon:(NSImage *)anIcon {
66     if ((self = [super initWithPithosAccountManager:aPithosAccountManager])) {
67         self.pithosContainer = aPithosContainer;
68         self.icon = anIcon;
69     }
70     return self;
71 }
72
73 - (id)initWithPithosAccountManager:(PithosAccount *)aPithosAccountManager pithosContainer:(ASIPithosContainer *)aPithosContainer {
74     return [self initWithPithosAccountManager:aPithosAccountManager pithosContainer:aPithosContainer icon:nil];
75 }
76
77 - (id)initWithPithosAccountManager:(PithosAccount *)aPithosAccountManager containerName:(NSString *)aContainerName icon:(NSImage *)anIcon {
78     ASIPithosContainer *container = [ASIPithosContainer container];
79     container.name = aContainerName;
80     return [self initWithPithosAccountManager:aPithosAccountManager pithosContainer:container icon:anIcon];
81 }
82
83 - (id)initWithPithosAccountManager:(PithosAccount *)aPithosAccountManager containerName:(NSString *)aContainerName {
84     return [self initWithPithosAccountManager:aPithosAccountManager containerName:aContainerName icon:nil];
85 }
86
87 - (void)dealloc {
88     [containerRequest clearDelegatesAndCancel];
89     [refreshMetadataContainerRequest clearDelegatesAndCancel];
90     [applyMetadataContainerRequest clearDelegatesAndCancel];
91 }
92
93 #pragma mark -
94 #pragma mark Properties
95
96 - (NSString *)url {
97     return [NSString stringWithFormat:@"@container@%@/%@%@",
98             (sharingAccount ? sharingAccount : pithosAccountManager.pithos.authUser),
99             pithosContainer.name,
100             (shared ? @"?shared" : @"")];
101 }
102
103 - (NSArray *)children {
104     @synchronized(self) {
105         switch (freshness) {
106             case PithosNodeStateFresh:
107                 break;
108             case PithosNodeStateRefreshNeeded:
109                 freshness = PithosNodeStateRefreshing;
110                 self.containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithosAccountManager.pithos
111                                                                                   containerName:pithosContainer.name
112                                                                                           limit:0
113                                                                                          marker:nil
114                                                                                          prefix:prefix
115                                                                                       delimiter:@"/"
116                                                                                            path:nil
117                                                                                            meta:nil
118                                                                                          shared:shared
119                                                                                           until:nil];
120                 if (sharingAccount)
121                     [containerRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos];
122                 else if (!forcedRefresh)
123                     containerRequest.downloadCache = [ASIDownloadCache sharedCache];
124                 containerRequest.delegate = self;
125                 containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
126                 containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
127                 containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
128                                              [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
129                                              [NSNumber numberWithUnsignedInteger:10], @"retries", 
130                                              NSStringFromSelector(@selector(containerRequestFinished:)), @"didFinishSelector", 
131                                              NSStringFromSelector(@selector(containerRequestFailed:)), @"didFailSelector", 
132                                              nil];
133                 [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
134                 break;
135             case PithosNodeStateRefreshing:
136                 break;
137             case PithosNodeStateRefreshFinished:
138                 if (newChildren) {
139                     children = newChildren;
140                     newChildren = nil;
141                 }
142                 freshness = PithosNodeStateFresh;
143             default:
144                 break;
145         }
146         return children;
147     }
148 }
149
150 - (NSString *)displayName {
151     return [pithosContainer.name copy];
152 }
153
154 - (void)setDisplayName:(NSString *)aDisplayName {
155 }
156
157 - (NSImage *)icon {
158     if (icon == nil) {
159         if ([pithosContainer.name isEqualToString:@"pithos"])
160             icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
161         else if ([pithosContainer.name isEqualToString:@"trash"])
162             icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];
163         else
164             icon = sharedIcon;
165     }
166     return icon;
167 }
168
169 - (void)setPithosContainer:(ASIPithosContainer *)aPithosContainer {
170     if (![pithosContainer isEqualTo:aPithosContainer]) {
171         pithosContainer = aPithosContainer;
172     }
173     if (pithosContainer.policy) {
174         self.policyVersioning = [pithosContainer.policy objectForKey:@"versioning"];
175         self.policyQuota = [NSNumber numberWithLongLong:[[pithosContainer.policy objectForKey:@"quota"] longLongValue]];
176     } else {
177         self.policyVersioning = @"manual";
178         self.policyQuota = [NSNumber numberWithLongLong:0];
179     }
180 }
181
182 - (void)setLimitedPithosContainer:(ASIPithosContainer *)aPithosContainer {
183     if (![pithosContainer isEqualTo:aPithosContainer]) {
184         self.pithosContainer.name = aPithosContainer.name;
185         self.pithosContainer.count = aPithosContainer.count;
186         self.pithosContainer.bytes = aPithosContainer.bytes;
187         self.pithosContainer.lastModified = aPithosContainer.lastModified;
188         self.pithosContainer.untilTimestamp = aPithosContainer.untilTimestamp;
189         if (!pithosNodeInfoController) {
190             self.pithosContainer.policy = aPithosContainer.policy;
191             self.pithosContainer = pithosContainer;
192         }
193     }
194 }
195
196 #pragma mark -
197 #pragma mark ASIHTTPRequestDelegate
198
199 - (void)containerRequestFailed:(ASIPithosContainerRequest *)request {
200     @autoreleasepool {
201         NSString *message;
202         NSError *error = [containerRequest error];
203         if (error)
204             message = [NSString stringWithFormat:@"Container listing %@ failed: %@", containerRequest.url, [error localizedDescription]];
205         else
206             message = [NSString stringWithFormat:@"Container listing %@ failed: (%d) %@", 
207                        containerRequest.url, containerRequest.responseStatusCode, containerRequest.responseStatusMessage];
208         dispatch_async(dispatch_get_main_queue(), ^{
209             [[PithosActivityFacility defaultPithosActivityFacility] startAndEndActivityWithType:PithosActivityOther message:message];
210         });
211         NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
212         if (retries > 0) {
213             ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
214             [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
215             self.containerRequest = newContainerRequest;
216             [[PithosUtilities prepareRequest:containerRequest priority:[[containerRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
217         } else {
218             newChildren = nil;
219             self.containerRequest = nil;
220             forcedRefresh = NO;
221             @synchronized(self) {
222                 freshness = PithosNodeStateRefreshNeeded;
223             }
224         }
225     }
226 }
227
228 - (void)containerRequestFinished:(ASIPithosContainerRequest *)request {
229     @autoreleasepool {
230         DLog(@"List container finished: %@", [containerRequest url]);
231         DLog(@"Cached: %d", [containerRequest didUseCachedResponse]);
232         if (containerRequest.responseStatusCode == 200) {
233             if ((pithosContainer.blockHash == nil) || (pithosContainer.blockSize == 0)) {
234                 pithosContainer.blockHash = [containerRequest blockHash];
235                 pithosContainer.blockSize = [containerRequest blockSize];
236             }
237         
238             NSMutableArray *objects = [containerRequest.userInfo objectForKey:@"objects"];
239             NSArray *someObjects = [containerRequest objects];
240             if (objects == nil) {
241                 objects = [NSMutableArray arrayWithArray:someObjects];
242             } else {
243                 [objects addObjectsFromArray:someObjects];
244             }
245             if ([someObjects count] < 10000) {
246                 if (!containerRequest.didUseCachedResponse || ([objects count] != [someObjects count]) || !children) {
247                     // Save new children
248                     DLog(@"using newChildren");
249                     newChildren = [[NSMutableArray alloc] init];
250                     NSArray *objectNames = [objects valueForKey:@"name"];
251                     NSMutableIndexSet *keptNodes = [NSMutableIndexSet indexSet];
252                     BOOL isSubdirNode = ([self class] == [PithosSubdirNode class]);
253                     for (ASIPithosObject *object in objects) {
254                         if (!isSubdirNode || 
255                             ([object.name hasPrefix:[((PithosSubdirNode *)self).prefix stringByAppendingString:@"/"]] &&
256                              ([object.name length] > [((PithosSubdirNode *)self).prefix length] + 1))) {
257                             // The check above removes false objects due to trailing slash or same prefix
258                             if (object.subdir) {
259                                 NSUInteger sameNameObjectIndex = [objectNames indexOfObject:[object.name substringToIndex:([object.name length] - 1)]];
260                                 if ((sameNameObjectIndex == NSNotFound) || 
261                                     ![PithosUtilities isContentTypeDirectory:[[objects objectAtIndex:sameNameObjectIndex] contentType]]) {
262                                     PithosSubdirNode *node = [[PithosSubdirNode alloc] initWithPithosAccountManager:pithosAccountManager
263                                                                                                     pithosContainer:pithosContainer
264                                                                                                        pithosObject:object];
265                                     node.parent = self;
266                                     node.shared = shared;
267                                     node.sharingAccount = sharingAccount;
268                                     node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
269                                     if (children) {
270                                         NSUInteger oldIndex = [children indexOfObject:node];
271                                         if (oldIndex != NSNotFound) {
272                                             // Use the same pointer value, if possible
273                                             node = [children objectAtIndex:oldIndex];
274                                             node.pithosContainer = pithosContainer;
275 //                                          node.pithosObject = object;
276                                             [node setLimitedPithosObject:object];
277                                             [keptNodes addIndex:oldIndex];
278                                         }
279                                     }
280                                     if (sharingAccount)
281                                         node.pithosObject.allowedTo = @"read";
282                                     [newChildren addObject:node];
283                                 }
284                             } else if ([PithosUtilities isContentTypeDirectory:object.contentType]) {
285                                 PithosSubdirNode *node = [[PithosSubdirNode alloc] initWithPithosAccountManager:pithosAccountManager
286                                                                                                 pithosContainer:pithosContainer
287                                                                                                    pithosObject:object];
288                                 node.parent = self;
289                                 node.shared = shared;
290                                 node.sharingAccount = sharingAccount;
291                                 node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
292                                 if (children) {
293                                     NSUInteger oldIndex = [children indexOfObject:node];
294                                     if (oldIndex != NSNotFound) {
295                                         // Use the same pointer value, if possible
296                                         node = [children objectAtIndex:oldIndex];
297                                         node.pithosContainer = pithosContainer;
298 //                                      node.pithosObject = object;
299                                         [node setLimitedPithosObject:object];
300                                         [keptNodes addIndex:oldIndex];
301                                     }
302                                 }
303                                 [newChildren addObject:node];
304                             } else {
305                                 PithosObjectNode *node = [[PithosObjectNode alloc] initWithPithosAccountManager:pithosAccountManager
306                                                                                                 pithosContainer:pithosContainer
307                                                                                                    pithosObject:object];
308                                 node.parent = self;
309                                 node.shared = shared;
310                                 node.sharingAccount = sharingAccount;
311                                 node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
312                                 if (children) {
313                                     NSUInteger oldIndex = [children indexOfObject:node];
314                                     if (oldIndex != NSNotFound) {
315                                         // Use the same pointer value, if possible
316                                         node = [children objectAtIndex:oldIndex];
317                                         node.pithosContainer = pithosContainer;
318 //                                      node.pithosObject = object;
319                                         [node setLimitedPithosObject:object];
320                                         [keptNodes addIndex:oldIndex];
321                                     }
322                                 }
323                                 [newChildren addObject:node];                                
324                             }
325                         }
326                     }
327                     [[children objectsAtIndexes:
328                       [[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [children count])] indexesPassingTest:^(NSUInteger idx, BOOL *stop){
329                         if ([keptNodes containsIndex:idx])
330                             return NO;
331                         return YES;
332                     }]] makeObjectsPerformSelector:@selector(pithosNodeWillBeRemoved)];
333                 }
334                 // Else cache was used and all results were fetched during this request, so existing children can be reused
335                 self.containerRequest = nil;
336                 forcedRefresh = NO;
337                 @synchronized(self) {
338                     freshness = PithosNodeStateRefreshFinished;
339                 }
340                 [self postChildrenUpdatedNotificationName];
341             } else {
342                 // Do an additional request to fetch more objects
343                 self.containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithosAccountManager.pithos
344                                                                                   containerName:pithosContainer.name
345                                                                                           limit:0
346                                                                                          marker:[[someObjects lastObject] name]
347                                                                                          prefix:prefix
348                                                                                       delimiter:@"/"
349                                                                                            path:nil
350                                                                                            meta:nil
351                                                                                          shared:shared
352                                                                                           until:nil];
353                 if (sharingAccount)
354                     [containerRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos];
355                 else if (!forcedRefresh)
356                     containerRequest.downloadCache = [ASIDownloadCache sharedCache];
357                 containerRequest.delegate = self;
358                 containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
359                 containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
360                 containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
361                                              [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
362                                              [NSNumber numberWithUnsignedInteger:10], @"retries", 
363                                              NSStringFromSelector(@selector(containerRequestFinished:)), @"didFinishSelector", 
364                                              NSStringFromSelector(@selector(containerRequestFailed:)), @"didFailSelector",
365                                              objects, @"objects",
366                                              nil];
367                 [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
368             }
369         } else if (containerRequest.responseStatusCode == 304) {
370             // Container is not modified, so existing children can be reused
371             self.containerRequest = nil;
372             forcedRefresh = NO;
373             @synchronized(self) {
374                 freshness = PithosNodeStateRefreshFinished;
375             }
376             [self postChildrenUpdatedNotificationName];
377         } else {
378             [self containerRequestFailed:containerRequest];
379         }
380     }
381 }
382
383 - (void)containerMetadataRequestFinished:(ASIPithosContainerRequest *)request {
384     @autoreleasepool {
385         DLog(@"URL: %@", [request url]);
386         DLog(@"cached: %d", [request didUseCachedResponse]);
387         
388         if ([request isEqualTo:applyMetadataContainerRequest]) {
389             @synchronized(self) {
390                 self.applyMetadataContainerRequest = nil;
391             }
392             [self refreshInfo];
393         } else if ([request isEqualTo:refreshMetadataContainerRequest]) {
394             [[pithosNodeInfoController window] makeFirstResponder:nil];
395             self.pithosContainer = [refreshMetadataContainerRequest container];
396             @synchronized(self) {
397                 self.refreshMetadataContainerRequest = nil;
398             }
399         }
400     }
401 }
402
403 - (void)containerMetadataRequestFailed:(ASIPithosContainerRequest *)request {
404     @autoreleasepool {
405         NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
406         if (retries > 0) {
407             ASIPithosContainerRequest *newRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:request];
408             [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
409             if ([request isEqualTo:applyMetadataContainerRequest]) {
410                 @synchronized(self) {
411                     self.applyMetadataContainerRequest = newRequest;
412                 }
413             } else if ([request isEqualTo:refreshMetadataContainerRequest]) {
414                 @synchronized(self) {
415                     self.refreshMetadataContainerRequest = newRequest;
416                 }
417             }
418             [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
419         } else {
420             if ([request isEqualTo:applyMetadataContainerRequest]) {
421                 [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataContainerRequest];
422                 @synchronized(self) {
423                     self.applyMetadataContainerRequest = nil;
424                 }
425             } else if ([request isEqualTo:refreshMetadataContainerRequest]) {
426                 [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataContainerRequest];
427                 @synchronized(self) {
428                     self.refreshMetadataContainerRequest = nil;
429                 }
430             }
431         }
432     }
433 }
434
435 #pragma mark -
436 #pragma mark Info
437
438 - (void)applyInfo {
439     @synchronized(self) {
440         if (applyMetadataContainerRequest == nil) {
441             [[pithosNodeInfoController window] makeFirstResponder:nil];
442             self.applyMetadataContainerRequest = [ASIPithosContainerRequest updateContainerMetadataRequestWithPithos:pithosAccountManager.pithos
443                                                                                                        containerName:pithosContainer.name
444                                                                                                               policy:[NSDictionary dictionaryWithObjectsAndKeys:
445                                                                                                                       policyVersioning, @"versioning",
446                                                                                                                       [policyQuota stringValue], @"quota",
447                                                                                                                       nil]
448                                                                                                             metadata:pithosContainer.metadata
449                                                                                                               update:NO];
450             applyMetadataContainerRequest.delegate = self;
451             applyMetadataContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
452             applyMetadataContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
453             applyMetadataContainerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
454                                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
455                                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
456                                                       NSStringFromSelector(@selector(containerMetadataRequestFinished:)), @"didFinishSelector", 
457                                                       NSStringFromSelector(@selector(containerMetadataRequestFailed:)), @"didFailSelector", 
458                                                       nil];
459             [[PithosUtilities prepareRequest:applyMetadataContainerRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
460         }
461     }
462 }
463
464 - (void)refreshInfo {
465     @synchronized(self) {
466         if (refreshMetadataContainerRequest == nil) {
467             self.refreshMetadataContainerRequest = [ASIPithosContainerRequest containerMetadataRequestWithPithos:pithosAccountManager.pithos
468                                                                                                    containerName:pithosContainer.name];
469             refreshMetadataContainerRequest.delegate = self;
470             refreshMetadataContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
471             refreshMetadataContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
472             refreshMetadataContainerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
473                                                         [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
474                                                         [NSNumber numberWithUnsignedInteger:10], @"retries", 
475                                                         NSStringFromSelector(@selector(containerMetadataRequestFinished:)), @"didFinishSelector", 
476                                                         NSStringFromSelector(@selector(containerMetadataRequestFailed:)), @"didFailSelector", 
477                                                         nil];
478             if (!sharingAccount)
479                 refreshMetadataContainerRequest.downloadCache = [ASIDownloadCache sharedCache];
480             [[PithosUtilities prepareRequest:refreshMetadataContainerRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
481         }
482     }
483 }
484
485 #pragma mark -
486 #pragma mark Actions
487
488 - (void)showPithosNodeInfo:(id)sender {
489     if (!pithosNodeInfoController) {
490         pithosNodeInfoController = [[PithosContainerNodeInfoController alloc] initWithPithosNode:self];
491         [self refreshInfo];
492     }
493     [pithosNodeInfoController showWindow:sender];
494     [[pithosNodeInfoController window] makeKeyAndOrderFront:sender];
495     [NSApp activateIgnoringOtherApps:YES];
496 }
497
498 @end