Support multiple accounts. Bug fixes. Improve concurrency.
[pithos-macos] / pithos-macos / pithos_macosAppDelegate.m
index efa7a72..e52fe17 100644 (file)
@@ -2,7 +2,7 @@
 //  pithos_macosAppDelegate.m
 //  pithos-macos
 //
-// Copyright 2011 GRNET S.A. All rights reserved.
+// Copyright 2011-2012 GRNET S.A. All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or
 // without modification, are permitted provided that the following
 // or implied, of GRNET S.A.
 
 #import "pithos_macosAppDelegate.h"
+#import "PithosAccount.h"
 #import "PithosBrowserController.h"
 #import "PithosPreferencesController.h"
 #import "PithosSyncDaemon.h"
 #import "ASIPithosRequest.h"
+#import "ASIPithos.h"
 #import "ASIDownloadCache.h"
+#import "LastCompletedSyncTransformer.h"
 
 @implementation pithos_macosAppDelegate
-@synthesize pithosBrowserController, pithosSyncDaemon, alwaysNo;
+@synthesize pithosBrowserController, alwaysNo, aboutVersion, activated, currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount;
 
 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
-    userDefaults = [[NSUserDefaults standardUserDefaults] retain];
-    NSString *stringURL;
-    NSURL *testURL;
+    [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self 
+                                                       andSelector:@selector(handleAppleEvent:withReplyEvent:) 
+                                                     forEventClass:kInternetEventClass 
+                                                        andEventID:kAEGetURL];
     
-    stringURL = [userDefaults stringForKey:@"storageURLPrefix"];
-    testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
-    if (!testURL || !testURL.scheme || !testURL.host)
-        [userDefaults setObject:@"https://plus.pithos.grnet.gr/v1" forKey:@"storageURLPrefix"];
-
-    stringURL = [userDefaults stringForKey:@"publicURLPrefix"];
-    testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
-    if (!testURL || !testURL.scheme || !testURL.host)
-        [userDefaults setObject:@"https://plus.pithos.grnet.gr" forKey:@"publicURLPrefix"];
-        
-    stringURL = [userDefaults stringForKey:@"loginURLPrefix"];
-    testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
-    if (!testURL || !testURL.scheme || !testURL.host)
-        [userDefaults setObject:@"https://plus.pithos.grnet.gr/login" forKey:@"loginURLPrefix"];
-
-    stringURL = [userDefaults stringForKey:@"aboutURL"];
-    testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
-    if (!testURL || !testURL.scheme || !testURL.host)
-        [userDefaults setObject:@"https://pithos.dev.grnet.gr/docs" forKey:@"aboutURL"];
+    userDefaults = [[NSUserDefaults standardUserDefaults] retain];
+
+    NSString *stringURL = [userDefaults stringForKey:@"aboutURL"];
+    NSURL *testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
+    if (!testURL || !testURL.scheme || !testURL.host) {
+        [userDefaults setObject:@"https://pithos.dev.grnet.gr/docs/pithos" forKey:@"aboutURL"];
+        [userDefaults synchronize];
+    }
     
-    NSString *syncDirectoryPath = [userDefaults stringForKey:@"syncDirectoryPath"];
-    if (!syncDirectoryPath || ![syncDirectoryPath length]) {
-        syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Pithos"];
-    } else {
-        NSFileManager *fileManager = [NSFileManager defaultManager];
-        BOOL isDirectory;
-        BOOL fileExists = [fileManager fileExistsAtPath:syncDirectoryPath isDirectory:&isDirectory];
-        NSError *error = nil;
-        if ((fileExists && !isDirectory) || 
-            (!fileExists && (![fileManager createDirectoryAtPath:syncDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || error))) {
-            syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Pithos"];
-        }   
+    syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
+    if (syncTimeInterval <= 0.0) {
+        syncTimeInterval = 180.0;
+        [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
+        [userDefaults synchronize];
     }
-    [userDefaults setObject:syncDirectoryPath forKey:@"syncDirectoryPath"];
     
-    NSString *syncContainerName = [userDefaults stringForKey:@"syncContainerName"];
-    if (!syncContainerName || ![syncContainerName length] || [syncContainerName isEqualToString:@"trash"])
-        [userDefaults setObject:@"pithos" forKey:@"syncContainerName"];
-
-    double syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInteral"];
-    if (syncTimeInterval <= 0)
-        [userDefaults setDouble:180.0 forKey:@"syncTimeInteral"];
+    NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
+    NSArray *tmpArray;
+    if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
+        self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
+    else
+        self.pithosAccounts = [NSMutableArray array];
     
-    [userDefaults synchronize];
+    if (![pithosAccounts count]) {
+        [pithosAccounts addObject:[PithosAccount pithosAccount]];
+        self.pithosAccounts = self.pithosAccounts;
+    } else {
+        self.activated = YES;
+    }
     
-    [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self 
-                                                       andSelector:@selector(handleAppleEvent:withReplyEvent:) 
-                                                     forEventClass:kInternetEventClass 
-                                                        andEventID:kAEGetURL];
-    [self showPithosBrowser:self];
+    pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
+    for (PithosAccount *pithosAccount in pithosAccounts) {
+        [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
+        if (!currentPithosAccount && pithosAccount.active)
+            currentPithosAccount = [pithosAccount retain];
+    }
+    if (!currentPithosAccount)
+        self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
     
-    [self authenticate];
+    if (currentPithosAccount.active) {
+        [self savePithosAccounts:self];
+        [self showPithosBrowser:self];
+        pithosBrowserController.pithos = currentPithosAccount.pithos;
+    } else {
+        // XXX maybe call specifically to go to new account tab
+        [self showPithosPreferences:self];
+    }
+
+    syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval 
+                                                  target:self 
+                                                selector:@selector(sync) 
+                                                userInfo:nil 
+                                                 repeats:YES] retain];
+    [syncTimer fire];
 }
 
 // Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
     NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
     NSString *host = [url host];
        NSString *query = [url query];
-    NSLog(@"host : '%@', query: '%@'", host, query);
+    PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
-    if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && query) {
+    if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && 
+        pithosAccount && query) {
         // user=
         NSString *authUser;
         NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
         
         NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
         if ([authUser length] && [authToken length]) {
-            [userDefaults setObject:authUser forKey:@"authUser"];
-            [userDefaults setObject:authToken forKey:@"authToken"];
-            [userDefaults synchronize];
-
-            [self authenticate];
+            [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
+            [self savePithosAccounts:self];
+            if (pithosPreferencesController && [pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
+                pithosPreferencesController.authUser = pithosAccount.authUser;
+                pithosPreferencesController.authToken = pithosAccount.authToken;
+            }
+            self.activated = YES;
+            if ([pithosAccount isEqualTo:currentPithosAccount]) {
+                [self showPithosBrowser:self];
+                pithosBrowserController.pithos = pithosAccount.pithos;
+            }
         }
         // XXX else maybe show an error message?
     }
     // XXX else maybe show an error message?
 }
 
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
+    [self savePithosAccounts:self];
+    if ([self.pithosBrowserController operationsPending]) {
+        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+        [alert setMessageText:@"Pending Operations"];
+        [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
+        [alert addButtonWithTitle:@"OK"];
+        [alert addButtonWithTitle:@"Cancel"];
+        NSInteger choice = [alert runModal];
+        if (choice == NSAlertSecondButtonReturn) 
+            return NSTerminateCancel;
+    }
+    return NSTerminateNow;
+}
+
 #pragma mark -
-#pragma Actions
+#pragma mark Properties
+
+- (NSString *)aboutVersion {
+    return [NSString stringWithFormat:@"About Pithos+ %@ (%@)", 
+            [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 
+            [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
+}
+
+#pragma mark -
+#pragma mark NSMenuDelegate
+
+- (void)menuNeedsUpdate:(NSMenu *)menu {
+    NSMenuItem *menuItem;
+    [menu removeAllItems];
+    if ([menu isEqualTo:accountsMenu]) {
+        [menu setAutoenablesItems:NO];
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name 
+                                                   action:@selector(menuChangePithosAccount:) 
+                                            keyEquivalent:@""] autorelease];
+            [menuItem setRepresentedObject:pithosAccount];
+            [menuItem setEnabled:pithosAccount.active];
+            [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
+            [menu addItem:menuItem];
+        }
+    } else if ([menu isEqualTo:lastSyncMenu]) {
+        NSString *menuItemTitle;
+        [menu setAutoenablesItems:NO];
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            menuItemTitle = [NSString stringWithFormat:@"%@: %@", 
+                             pithosAccount.name, 
+                             [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
+            if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
+                menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
+            menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle 
+                                                   action:@selector(menuChangeSyncActive:) 
+                                            keyEquivalent:@""] autorelease];
+            [menuItem setRepresentedObject:pithosAccount];
+            [menuItem setEnabled:pithosAccount.active];
+            [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
+            [menu addItem:menuItem];
+        }
+        [menu addItem:[NSMenuItem separatorItem]];
+        [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync" 
+                                                  action:@selector(sync) 
+                                           keyEquivalent:@""] autorelease]];
+    }
+}
+
+#pragma mark -
+#pragma mark Actions
 
 - (IBAction)showPithosBrowser:(id)sender {
+    if (!activated)
+        return;
     [pithosBrowserController showWindow:sender];
     [[pithosBrowserController window] makeKeyAndOrderFront:sender];
     [NSApp activateIgnoringOtherApps:YES];
     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[userDefaults stringForKey:@"aboutURL"]]];
 }
 
-- (IBAction)syncNow:(id)sender {
-    [pithosSyncDaemon sync];
+- (void)sync {
+    if (!activated || ![pithosAccounts count])
+        return;
+    NSUInteger syncIndex;
+    BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
+    if (syncPithosAccountFound)
+         syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
+    
+    PithosAccount *singleSyncPithosAccount = nil;
+    for (PithosAccount *pithosAccount in pithosAccounts) {
+        if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+            singleSyncPithosAccount = pithosAccount;
+        } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+            singleSyncPithosAccount = nil;
+            break;
+        }
+    }
+    
+    if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
+        // An active syncDaemon was previously syncing
+        if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
+            // It's the only one, sync again
+            [syncPithosAccount.syncDaemon startDaemon];
+            [syncPithosAccount.syncDaemon sync];
+            return;
+        } else if ([syncPithosAccount.syncDaemon isSyncing]) {
+            // It's still syncing, mark it as late and return
+            [syncPithosAccount.syncDaemon syncLate];
+            return;
+        }
+    }
+    PithosAccount *newSyncPithosAccount = nil;
+    if (syncPithosAccountFound) {
+        for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
+            if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                newSyncPithosAccount = pithosAccount;
+                break;
+            }
+        }
+        if (!newSyncPithosAccount) {
+            for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
+                if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                    newSyncPithosAccount = pithosAccount;
+                    break;
+                }
+            }
+        }
+    } else {
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                newSyncPithosAccount = pithosAccount;
+                break;
+            }
+        }
+    }        
+    if (newSyncPithosAccount) {
+        // A different syncDaemon is found, sync it
+        self.syncPithosAccount = newSyncPithosAccount;
+        [syncPithosAccount.syncDaemon startDaemon];
+        [syncPithosAccount.syncDaemon sync];
+    } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
+        [syncPithosAccount.syncDaemon startDaemon];
+        [syncPithosAccount.syncDaemon sync];
+    } else {
+        self.syncPithosAccount = nil;
+    }
+}
+
+- (void)savePithosAccounts:(id)sender {
+    [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
+    [userDefaults synchronize];
+}
+
+- (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
+    if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            if (pithosAccount.active) {
+                self.currentPithosAccount = pithosAccount;
+                pithosBrowserController.pithos = currentPithosAccount.pithos;
+                break;
+            }
+        }
+        if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
+            self.activated = NO;
+            [pithosBrowserController.window close];
+            [pithosBrowserController resetBrowser];
+            self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
+        }
+    }
+    if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
+        self.syncPithosAccount = nil;
 }
 
 #pragma mark -
-#pragma Authentication
+#pragma mark Menu Actions
 
-- (void)login {
-    NSProcessInfo *processInfo = [NSProcessInfo processInfo];
-    NSString *loginURL = [NSString stringWithFormat:@"%@?next=pithos://%@_%d", 
-                          [userDefaults stringForKey:@"loginURLPrefix"], [processInfo processName], [processInfo processIdentifier]];
-    NSLog(@"loginURL: %@", loginURL);
-    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:loginURL]];
+- (void)menuChangePithosAccount:(NSMenuItem *)sender {
+    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
+    if (!pithosAccount.active)
+        return;
+    if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
+        if ([self.pithosBrowserController operationsPending]) {
+            NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+            [alert setMessageText:@"Pending Operations"];
+            [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
+            [alert addButtonWithTitle:@"OK"];
+            [alert addButtonWithTitle:@"Cancel"];
+            NSInteger choice = [alert runModal];
+            if (choice == NSAlertSecondButtonReturn) 
+                return;
+        }
+        self.currentPithosAccount = pithosAccount;
+        [self showPithosBrowser:self];
+        pithosBrowserController.pithos = currentPithosAccount.pithos;
+    }
 }
 
-- (void)authenticate {
-    NSString *authUser = [userDefaults stringForKey:@"authUser"];
-    NSString *authToken = [userDefaults stringForKey:@"authToken"];
-    NSString *storageURLPrefix = [userDefaults stringForKey:@"storageURLPrefix"];
-    NSString *publicURLPrefix = [userDefaults stringForKey:@"publicURLPrefix"];
-    NSString *syncDirectoryPath = [userDefaults stringForKey:@"syncDirectoryPath"];
-    NSString *syncContainerName = [userDefaults stringForKey:@"syncContainerName"];
-    double syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
-    NSLog(@"Authentication - storageURLPrefix:%@, authUser:%@, authToken:%@", storageURLPrefix, authUser, authToken);
-    if (([authUser length] == 0) || ([authToken length] == 0)) {
-        [self showPithosPreferences:self];
-    } else if ([authUser length] && [authToken length] && 
-               (![[ASIPithosRequest authUser] isEqualToString:authUser] || ![[ASIPithosRequest authToken] isEqualToString:authToken])) {
-        [userDefaults setObject:authUser forKey:@"previousAuthUser"];
-        [userDefaults setObject:authToken forKey:@"previousAuthToken"];
-        
-        [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy];
-        [[ASIPithosRequest sharedQueue] cancelAllOperations];
-        
-        [ASIPithosRequest setAuthURL:storageURLPrefix];
-        [ASIPithosRequest setStorageURLPrefix:storageURLPrefix];
-        [ASIPithosRequest setAuthUser:authUser];
-        [ASIPithosRequest setAuthToken:authToken];
-        [ASIPithosRequest setPublicURLPrefix:publicURLPrefix];
-        
-        self.pithosSyncDaemon = [[[PithosSyncDaemon alloc] initWithDirectoryPath:syncDirectoryPath 
-                                                                   containerName:syncContainerName 
-                                                                    timeInterval:syncTimeInterval 
-                                                                 resetLocalState:NO] autorelease];
-        
-        [[NSNotificationCenter defaultCenter] postNotificationName:@"PithosAuthenticationCredentialsUpdated" object:self];
-    } else if (![pithosSyncDaemon.directoryPath isEqualToString:syncDirectoryPath]) {
-        self.pithosSyncDaemon = [[[PithosSyncDaemon alloc] initWithDirectoryPath:syncDirectoryPath 
-                                                                   containerName:syncContainerName 
-                                                                    timeInterval:syncTimeInterval 
-                                                                 resetLocalState:YES] autorelease];
-    }
+- (void)menuChangeSyncActive:(NSMenuItem *)sender {
+    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
+    if (!pithosAccount.active)
+        return;
+    pithosAccount.syncActive = !pithosAccount.syncActive;
+    if (pithosPreferencesController && [pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
+        pithosPreferencesController.syncActive = pithosAccount.syncActive;
+    [self savePithosAccounts:self];
 }
 
 @end