973a414205cb3196edb103e59f7a11828c00a593
[pithos-macos] / pithos-macos / PithosObjectNode.m
1 //
2 //  PithosObjectNode.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 "PithosObjectNode.h"
39 #import "ASIPithosRequest.h"
40 #import "ASIPithos.h"
41 #import "ASIPithosObjectRequest.h"
42 #import "ASIPithosContainer.h"
43 #import "ASIPithosObject.h"
44 #import "ASIPithosSharingUser.h"
45 #import "ASIDownloadCache.h"
46 #import "PithosAccount.h"
47 #import "PithosUtilities.h"
48 #import "PithosObjectNodeInfoController.h"
49
50 @implementation PithosObjectNode
51 @synthesize pithos, pithosContainer, pithosObject, versions, applyMetadataObjectRequest, refreshMetadataObjectRequest, refreshVersionsObjectRequest;
52 @synthesize isPublic, translatedModifiedBy, translatedPermissions;
53
54 #pragma mark -
55 #pragma mark Object Lifecycle
56
57 - (id)initWithPithosAccountManager:(PithosAccount *)aPithosAccountManager
58                          andPithos:(ASIPithos *)aPithos
59                    pithosContainer:(ASIPithosContainer *)aPithosContainer
60                       pithosObject:(ASIPithosObject *)aPithosObject {
61     if ((self = [super initWithPithosAccountManager:aPithosAccountManager])) {
62         pithos = aPithos;
63         isLeafItem = YES;
64         self.pithosContainer = aPithosContainer;
65         self.pithosObject = aPithosObject;
66     }
67     return self;
68 }
69
70 - (void)dealloc {
71     [refreshVersionsObjectRequest clearDelegatesAndCancel];
72     [refreshMetadataObjectRequest clearDelegatesAndCancel];
73     [applyMetadataObjectRequest clearDelegatesAndCancel];
74 }
75
76 #pragma mark -
77 #pragma mark Internal
78
79 - (void)updateModifiedBy {
80     if (!pithosObject.modifiedBy) {
81         self.translatedModifiedBy = nil;
82     } else if (pithosAccountManager) {
83         NSString *displayname = [pithosAccountManager displaynameForUUID:pithosObject.modifiedBy safe:NO];
84         if (displayname) {
85             self.translatedModifiedBy = displayname;
86         } else {
87             [pithosAccountManager updateUserCatalogForDisplaynames:nil UUIDs:[NSArray arrayWithObject:pithosObject.modifiedBy]];
88             self.translatedModifiedBy = [pithosAccountManager displaynameForUUID:pithosObject.modifiedBy safe:YES];
89         }
90     } else {
91         self.translatedModifiedBy = [pithosObject.modifiedBy copy];
92     }
93 }
94
95 - (void)updatePermissions {
96     if (!pithosObject) {
97         self.translatedPermissions = [NSMutableArray array];
98     } else if (pithosAccountManager) {
99         NSMutableSet *UUIDs = [NSMutableSet set];
100         for (ASIPithosSharingUser *sharingUser in pithosObject.permissions) {
101             [UUIDs addObject:sharingUser.name];
102         }
103         [UUIDs removeObject:@""];
104         [UUIDs removeObject:@"*"];
105         if (UUIDs.count) {
106             [pithosAccountManager updateUserCatalogForDisplaynames:nil UUIDs:[UUIDs allObjects]];
107         }
108         
109         NSMutableArray *newTranslatedPermissions = [NSMutableArray arrayWithCapacity:pithosObject.permissions.count];
110         for (ASIPithosSharingUser *sharingUser in pithosObject.permissions) {
111             ASIPithosSharingUser *translatedSharingUser = [sharingUser copy];
112             translatedSharingUser.name = [pithosAccountManager displaynameForUUID:translatedSharingUser.name safe:YES];
113             [newTranslatedPermissions addObject:translatedSharingUser];
114         }
115         self.translatedPermissions = newTranslatedPermissions;
116     } else {
117         self.translatedPermissions = [NSMutableArray arrayWithArray:[pithosObject.permissions copy]];
118     }
119 }
120
121 #pragma mark -
122 #pragma mark Properties
123
124 - (void)setPithos:(ASIPithos *)aPithos {
125     if (aPithos && ![aPithos isEqualTo:pithos]) {
126         pithos = aPithos;
127         url = nil;
128     }
129 }
130
131 - (NSString *)url {
132     if (url == nil)
133         url = [[NSString alloc] initWithFormat:@"object %@/%@/%@%@", 
134                (sharingAccount ? [pithos storageURLWithAuthUser:sharingAccount] : pithos.storageURL), 
135                pithosContainer.name, 
136                pithosObject.name, 
137                (shared ? @"?shared" : @"")];
138     return url;
139 }
140
141 - (NSArray *)children {
142     return nil;
143 }
144
145 - (NSString *)displayName {
146     if (displayName == nil) {
147         displayName = [pithosObject.name lastPathComponent];
148         if([pithosObject.name hasSuffix:@"/"])
149             displayName = [displayName stringByAppendingString:@"/"];
150     }
151     return [displayName copy];
152 }
153
154 - (void)setDisplayName:(NSString *)aDisplayName {    
155 }
156
157 - (NSImage *)icon {
158     if (icon == nil)
159         icon = [[NSWorkspace sharedWorkspace] iconForFileType:[pithosObject.name pathExtension]];
160     return icon;
161 }
162
163 - (void)setPithosObject:(ASIPithosObject *)aPithosObject {
164     if (![pithosObject isEqualTo:aPithosObject]) {
165         pithosObject = aPithosObject;
166         [self updateModifiedBy];
167         [self updatePermissions];
168     }
169     self.isPublic = (pithosObject.publicURI != nil);
170     // Refresh browser if the object is in my shared and is no longer shared
171     if (shared && !pithosObject.sharing)
172         [[NSNotificationCenter defaultCenter] postNotificationName:@"PithosBrowserRefreshNeeded" object:self];
173 }
174
175 - (void)setLimitedPithosObject:(ASIPithosObject *)aPithosObject {
176     if (![pithosObject isEqualTo:aPithosObject]) {
177         self.pithosObject.subdir = aPithosObject.subdir;
178         self.pithosObject.name = aPithosObject.name;
179         self.pithosObject.hash = aPithosObject.hash;
180         self.pithosObject.objectHash = aPithosObject.objectHash;
181         self.pithosObject.UUID = aPithosObject.UUID;
182         self.pithosObject.bytes = aPithosObject.bytes;
183         self.pithosObject.contentType = aPithosObject.contentType;
184         self.pithosObject.lastModified = aPithosObject.lastModified;
185         self.pithosObject.version = aPithosObject.version;
186         self.pithosObject.versionTimestamp = aPithosObject.versionTimestamp;
187         self.pithosObject.modifiedBy = aPithosObject.modifiedBy;
188         self.pithosObject.sharedBy = aPithosObject.sharedBy;
189         self.pithosObject.allowedTo = aPithosObject.allowedTo;
190         if (!pithosNodeInfoController) {
191             self.pithosObject.sharing = aPithosObject.sharing;
192             self.pithosObject.publicURI = aPithosObject.publicURI;
193             self.pithosObject = pithosObject;
194             [self updatePermissions];
195         } else {
196             [self updateModifiedBy];
197         }
198     }
199 }
200
201 #pragma mark -
202 #pragma mark ASIHTTPRequestDelegate
203
204 - (void)objectRequestFinished:(ASIPithosObjectRequest *)request {
205     @autoreleasepool {
206         DLog(@"URL: %@", [request url]);
207         DLog(@"cached: %d", [request didUseCachedResponse]);
208         
209         if ([request isEqualTo:applyMetadataObjectRequest]) {
210             int responseStatusCode = applyMetadataObjectRequest.responseStatusCode;
211             if (responseStatusCode != 202)
212                 [PithosUtilities unexpectedResponseStatusAlertWithRequest:applyMetadataObjectRequest];
213             @synchronized(self) {
214                 self.applyMetadataObjectRequest = nil;
215             }
216             if (responseStatusCode == 202)
217                 [self refreshInfo];
218         } else if ([request isEqualTo:refreshMetadataObjectRequest]) {
219             [[pithosNodeInfoController window] makeFirstResponder:nil];
220             self.pithosObject = [refreshMetadataObjectRequest object];
221             @synchronized(self) {
222                 self.refreshMetadataObjectRequest = nil;
223             }
224         } else if ([request isEqualTo:refreshVersionsObjectRequest]) {
225             [[pithosNodeInfoController window] makeFirstResponder:nil];
226             self.versions = [refreshVersionsObjectRequest versions];
227             @synchronized(self) {
228                 self.refreshVersionsObjectRequest = nil;
229             }
230         }
231     }
232 }
233
234 - (void)objectRequestFailed:(ASIPithosObjectRequest *)request {
235     @autoreleasepool {
236         NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
237         if (retries > 0) {
238             ASIPithosObjectRequest *newRequest = (ASIPithosObjectRequest *)[PithosUtilities copyRequest:request];
239             [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
240             if ([request isEqualTo:applyMetadataObjectRequest]) {
241                 @synchronized(self) {
242                     self.applyMetadataObjectRequest = newRequest;
243                 }
244             } else if ([request isEqualTo:refreshMetadataObjectRequest]) {
245                 @synchronized(self) {
246                     self.refreshMetadataObjectRequest = newRequest;
247                 }
248             } else if ([request isEqualTo:refreshVersionsObjectRequest]) {
249                 @synchronized(self) {
250                     self.refreshVersionsObjectRequest = newRequest;
251                 }
252             }
253             [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
254         } else {
255             if ([request isEqualTo:applyMetadataObjectRequest]) {
256                 [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataObjectRequest];
257                 @synchronized(self) {
258                     self.applyMetadataObjectRequest = nil;
259                 }
260             } else if ([request isEqualTo:refreshMetadataObjectRequest]) {
261                 [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataObjectRequest];
262                 @synchronized(self) {
263                     self.refreshMetadataObjectRequest = nil;
264                 }
265             } else if ([request isEqualTo:refreshVersionsObjectRequest]) {
266                 [PithosUtilities httpRequestErrorAlertWithRequest:refreshVersionsObjectRequest];
267                 @synchronized(self) {
268                     self.refreshVersionsObjectRequest = nil;
269                 }
270             }
271         }
272     }
273 }
274
275 #pragma mark -
276 #pragma mark Info
277
278 - (void)applyInfo {
279     @synchronized(self) {
280         if (applyMetadataObjectRequest == nil) {
281             [[pithosNodeInfoController window] makeFirstResponder:nil];
282             if (sharingAccount) {
283                 self.applyMetadataObjectRequest = [ASIPithosObjectRequest updateObjectMetadataRequestWithPithos:pithos
284                                                                                                   containerName:pithosContainer.name
285                                                                                                      objectName:pithosObject.name
286                                                                                                 contentEncoding:nil
287                                                                                              contentDisposition:nil
288                                                                                                        manifest:nil
289                                                                                                         sharing:nil
290                                                                                                        isPublic:(isPublic ? ASIPithosObjectRequestPublicTrue : ASIPithosObjectRequestPublicFalse)
291                                                                                                        metadata:pithosObject.metadata
292                                                                                                          update:NO];
293                 [applyMetadataObjectRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
294             } else {
295                 NSMutableArray *permissions = [NSMutableArray array];
296                 if (translatedPermissions.count) {
297                     for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
298                         if (translatedsSharingUser.group.length &&
299                             [translatedsSharingUser.group rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" -_~,;"]].location != NSNotFound) {
300                             NSAlert *alert = [[NSAlert alloc] init];
301                             [alert setMessageText:@"Invalid Input"];
302                             [alert setInformativeText:@"Group names cannot contain ' ', '-', '_', '~', ',' or ';'."];
303                             [alert addButtonWithTitle:@"OK"];
304                             [alert runModal];
305                             return;
306                         }
307                     }
308                     if (pithosAccountManager) {
309                         NSMutableSet *allUsers = [NSMutableSet set];
310                         for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
311                             if (translatedsSharingUser.name.length) {
312                                 [allUsers addObject:translatedsSharingUser.name];
313                             }
314                         }
315                         [allUsers removeObject:@""];
316                         [allUsers removeObject:@"*"];
317                         if (allUsers.count) {
318                             ASIPithosRequest *userCatalogRequest = [pithosAccountManager updateUserCatalogForDisplaynames:[allUsers allObjects]
319                                                                                                                        UUIDs:nil];
320                             if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
321                                 return;
322                             } else if (userCatalogRequest.responseStatusCode == 200) {
323                                 // Check if all users exist.
324                                 NSDictionary *displaynameCatalog = [userCatalogRequest displaynameCatalog];
325                                 NSMutableArray *inexistentUsers = [NSMutableArray array];
326                                 for (NSString *user in allUsers) {
327                                     if (![displaynameCatalog objectForKey:user]) {
328                                         [inexistentUsers addObject:user];
329                                     }
330                                 }
331                                 if (!inexistentUsers.count) {
332                                     // Create permissions.
333                                     for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
334                                         if (translatedsSharingUser.name.length) {
335                                             ASIPithosSharingUser *sharingUser = [translatedsSharingUser copy];
336                                             if (![sharingUser.name isEqualToString:@"*"]) {
337                                                 sharingUser.name = [displaynameCatalog objectForKey:sharingUser.name];
338                                             }
339                                             if (!sharingUser.permission) {
340                                                 sharingUser.permission = @"read";
341                                             }
342                                             [permissions addObject:sharingUser];
343                                         }
344                                     }
345                                 } else {
346                                     NSAlert *alert = [[NSAlert alloc] init];
347                                     if (inexistentUsers.count == 1) {
348                                         [alert setMessageText:@"Invalid User"];
349                                         [alert setInformativeText:[NSString stringWithFormat:@"User '%@' doesn't exist.", [inexistentUsers objectAtIndex:0]]];
350                                     } else {
351                                         [alert setMessageText:@"Invalid Users"];
352                                         [alert setInformativeText:[NSString stringWithFormat:@"Users '%@' don't exist.", [inexistentUsers componentsJoinedByString:@"', '"]]];
353                                     }
354                                     [alert addButtonWithTitle:@"OK"];
355                                     [alert runModal];
356                                     return;
357                                 }
358                             } else {
359                                 // 404. Since we don't translate to UUIDs, check for invalid chars.
360                                 BOOL valid = YES;
361                                 // Create permissions.
362                                 for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
363                                     if (translatedsSharingUser.name.length &&
364                                         ([translatedsSharingUser.name rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ~,;:"]].location != NSNotFound)) {
365                                         valid = NO;
366                                         break;
367                                     }
368                                 }
369                                 if (valid) {
370                                     for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
371                                         if (translatedsSharingUser.name.length) {
372                                             ASIPithosSharingUser *sharingUser = [translatedsSharingUser copy];
373                                             if (!sharingUser.permission) {
374                                                 sharingUser.permission = @"read";
375                                             }                                            
376                                             [permissions addObject:sharingUser];
377                                         }
378                                     }
379                                 } else {
380                                     NSAlert *alert = [[NSAlert alloc] init];
381                                     [alert setMessageText:@"Invalid Input"];
382                                     [alert setInformativeText:@"Users cannot contain ' ', '~', ',', ';' or ':'."];
383                                     [alert addButtonWithTitle:@"OK"];
384                                     [alert runModal];
385                                     return;
386                                 }
387                             }
388                         } else {
389                             for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
390                                 if ([translatedsSharingUser.name isEqualToString:@"*"]) {
391                                     ASIPithosSharingUser *sharingUser = [translatedsSharingUser copy];
392                                     if (!sharingUser.permission) {
393                                         sharingUser.permission = @"read";
394                                     }
395                                     [permissions addObject:sharingUser];
396                                 }
397                             }
398                         }
399                     } else {
400                         for (ASIPithosSharingUser *translatedsSharingUser in translatedPermissions) {
401                             if (translatedsSharingUser.name.length) {
402                                 ASIPithosSharingUser *sharingUser = [translatedsSharingUser copy];
403                                 if (!sharingUser.permission) {
404                                     sharingUser.permission = @"read";
405                                 }
406                                 [permissions addObject:sharingUser];
407                             }
408                         }
409                     }
410                 }
411                 pithosObject.permissions = permissions;
412             
413                 self.applyMetadataObjectRequest = [ASIPithosObjectRequest updateObjectMetadataRequestWithPithos:pithos
414                                                                                                   containerName:pithosContainer.name
415                                                                                                      objectName:pithosObject.name
416                                                                                                 contentEncoding:pithosObject.contentEncoding
417                                                                                              contentDisposition:pithosObject.contentDisposition
418                                                                                                        manifest:pithosObject.manifest
419                                                                                                         sharing:(pithosObject.sharing ? pithosObject.sharing : @"")
420                                                                                                        isPublic:(isPublic ? ASIPithosObjectRequestPublicTrue : ASIPithosObjectRequestPublicFalse)
421                                                                                                        metadata:pithosObject.metadata
422                                                                                                          update:NO];
423             }
424             applyMetadataObjectRequest.delegate = self;
425             applyMetadataObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
426             applyMetadataObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
427             applyMetadataObjectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
428                                                    [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
429                                                    [NSNumber numberWithUnsignedInteger:10], @"retries", 
430                                                    NSStringFromSelector(@selector(objectRequestFinished:)), @"didFinishSelector", 
431                                                    NSStringFromSelector(@selector(objectRequestFailed:)), @"didFailSelector", 
432                                                    nil];
433             [[PithosUtilities prepareRequest:applyMetadataObjectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
434         }
435     }
436 }
437
438 - (void)refreshInfo {
439     @synchronized(self) {
440         if (refreshMetadataObjectRequest == nil) {
441             self.refreshMetadataObjectRequest = [ASIPithosObjectRequest objectMetadataRequestWithPithos:pithos
442                                                                                           containerName:pithosContainer.name
443                                                                                              objectName:pithosObject.name];
444             if (sharingAccount)
445                 [refreshMetadataObjectRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
446             refreshMetadataObjectRequest.delegate = self;
447             refreshMetadataObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
448             refreshMetadataObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
449             refreshMetadataObjectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
450                                                      [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
451                                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
452                                                      NSStringFromSelector(@selector(objectRequestFinished:)), @"didFinishSelector", 
453                                                      NSStringFromSelector(@selector(objectRequestFailed:)), @"didFailSelector", 
454                                                      nil];
455             if (!sharingAccount)
456                 refreshMetadataObjectRequest.downloadCache = [ASIDownloadCache sharedCache];
457             [[PithosUtilities prepareRequest:refreshMetadataObjectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
458         }
459     }
460     [self refreshVersions];
461 }
462
463 #pragma mark -
464 #pragma mark Versions
465
466 - (void)refreshVersions {
467     @synchronized(self) {
468         if (refreshVersionsObjectRequest == nil) {
469             self.refreshVersionsObjectRequest = [ASIPithosObjectRequest objectVersionsRequestWithPithos:pithos
470                                                                                           containerName:pithosContainer.name
471                                                                                              objectName:pithosObject.name];
472             if (sharingAccount)
473                 [refreshVersionsObjectRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
474             refreshVersionsObjectRequest.delegate = self;
475             refreshVersionsObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
476             refreshVersionsObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
477             refreshVersionsObjectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
478                                                      [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
479                                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
480                                                      NSStringFromSelector(@selector(objectRequestFinished:)), @"didFinishSelector", 
481                                                      NSStringFromSelector(@selector(objectRequestFailed:)), @"didFailSelector", 
482                                                      nil];
483             if (!sharingAccount)
484                 refreshVersionsObjectRequest.downloadCache = [ASIDownloadCache sharedCache];
485             [[PithosUtilities prepareRequest:refreshVersionsObjectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
486         }
487     }
488 }
489
490 #pragma mark -
491 #pragma mark Actions
492
493 - (void)showPithosNodeInfo:(id)sender {
494     if (!pithosNodeInfoController) {
495         pithosNodeInfoController = [[PithosObjectNodeInfoController alloc] initWithPithosNode:self];
496         [self refreshInfo];
497     }
498     [pithosNodeInfoController showWindow:sender];
499     [[pithosNodeInfoController window] makeKeyAndOrderFront:sender];
500     [NSApp activateIgnoringOtherApps:YES];
501 }
502
503 @end