Fix bugs
[pithos-macos] / pithos-macos / PithosAccount.m
1 //
2 //  PithosAccount.m
3 //  pithos-macos
4 //
5 // Copyright 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 "PithosAccount.h"
39 #import "PithosSyncDaemon.h"
40 #import "ASIPithos.h"
41 #import "ASIPithosRequest.h"
42 #import "PithosAccountNode.h"
43 #import "PithosSharingAccountsNode.h"
44 #import "PithosUtilities.h"
45 #import "pithos_macosAppDelegate.h"
46
47 @interface PithosAccount (Internal)
48 - (BOOL)urlIsValid:(NSString *)urlString;
49 @end
50
51 @implementation PithosAccount
52 @synthesize uniqueName, active, name, clientVersion;
53 @synthesize syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, syncDaemon;
54 @synthesize serverURL, versionResource, loginResource, publicResource, userCatalogResource;
55 @synthesize authUser, authToken, storageURLPrefix, authURL, loginURLPrefix, publicURLPrefix, userCatalogURL, userCatalog;
56 @synthesize pithos, accountNode, sharingAccountsNode;
57
58 #pragma mark -
59 #pragma mark Object Lifecycle
60
61 + (id)pithosAccount {
62     PithosAccount *pithosAccount = [[self alloc] init];
63     pithosAccount.uniqueName = [NSString stringWithFormat:@"pithosAccount-%f", [NSDate timeIntervalSinceReferenceDate]];
64     pithosAccount.clientVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
65     pithosAccount.syncSkipHidden = YES;
66     return pithosAccount;
67 }
68
69
70 - (NSString *)description {
71     return [NSString stringWithFormat:@"uniqueName: %@, active: %d, name: %@, clientVersion: %@, syncActive: %d, syncDirectoryPath: %@, syncAccountsDictionary: %@, syncSkipHidden: %d, syncLastCompleted: %@, serverURL: %@, versionResource: %@, loginResource: %@, publicResource: %@, userCatalogResource: %@, authUser: %@, authToken: %@, storageURLPrefix: %@, authURL: %@, loginURLPrefix: %@, publicURLPrefix: %@, userCatalogResource: %@",
72             uniqueName, active, name, clientVersion, syncActive, syncDirectoryPath, syncAccountsDictionary, syncSkipHidden, syncLastCompleted, serverURL, versionResource, loginResource, publicResource, userCatalogResource, authUser, authToken, storageURLPrefix, authURL, loginURLPrefix, publicURLPrefix, userCatalogResource];
73 }
74
75 #pragma mark -
76 #pragma mark Internal
77
78 - (BOOL)urlIsValid:(NSString *)urlString {
79     if (urlString) {
80         NSURL *url = [NSURL URLWithString:urlString];
81         if (url && url.scheme && url.host)
82             return YES;
83     }
84     return NO;
85 }
86
87 #pragma mark -
88 #pragma mark Properties
89
90 - (NSString *)name {
91     if (![name length]) {
92         NSDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
93         NSString *namePrefix = @"okeanos";
94         NSUInteger nameSuffix = 1;
95         name = @"okeanos";
96         NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
97         NSFileManager *fileManager = [NSFileManager defaultManager];
98         while ([pithosAccountsDictionary objectForKey:name] || 
99                [fileManager fileExistsAtPath:[documentsDirectoryPath stringByAppendingPathComponent:name]]) {
100             name = [NSString stringWithFormat:@"%@%ld", namePrefix, ++nameSuffix];
101         }
102     }
103     return name;
104 }
105
106 - (void)setName:(NSString *)aName {
107     NSMutableDictionary *pithosAccountsDictionary = [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] pithosAccountsDictionary];
108     if (![self.name isEqualToString:aName] && [aName length] && ![pithosAccountsDictionary objectForKey:aName]) {
109         [pithosAccountsDictionary setObject:self forKey:aName];
110         [pithosAccountsDictionary removeObjectForKey:name];
111         name = aName;
112     }
113 }
114
115 - (BOOL)syncActive {
116     if (active)
117         return syncActive;
118     else
119         return NO;
120 }
121
122 - (void)setSyncActive:(BOOL)aSyncActive {
123     syncActive = aSyncActive;
124     if (syncDaemon && !self.syncActive)
125         [syncDaemon resetDaemon];
126 }
127
128 - (NSString *)syncDirectoryPath {
129     if (![syncDirectoryPath length]) {
130         syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
131                               stringByAppendingPathComponent:self.name];
132     }
133     return syncDirectoryPath;
134 }
135
136 - (void)setSyncDirectoryPath:(NSString *)aSyncDirectoryPath {
137     if (![self.syncDirectoryPath isEqualToString:aSyncDirectoryPath] && [aSyncDirectoryPath length]) {
138         BOOL isDirectory;
139         if (![[NSFileManager defaultManager] fileExistsAtPath:aSyncDirectoryPath isDirectory:&isDirectory] || isDirectory) {
140             syncDirectoryPath = aSyncDirectoryPath;
141         } else {
142             return;
143         }
144
145         @synchronized(self) {
146             resetSyncDaemonLocalState = YES;
147             syncLastCompleted = nil;
148         }
149     }
150 }
151
152 - (NSMutableDictionary *)syncAccountsDictionary {
153     if (!syncAccountsDictionary) {
154         syncAccountsDictionary = [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionaryWithObject:[NSMutableSet set] 
155                                                                                                               forKey:@"pithos"]
156                                                                     forKey:@""];
157     }        
158     return syncAccountsDictionary;
159 }
160
161 - (void)setSyncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary {
162     if (aSyncAccountsDictionary && ![self.syncAccountsDictionary isEqualToDictionary:aSyncAccountsDictionary]) {
163         syncAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncAccountsDictionary count]];
164         for (NSString *accountName in aSyncAccountsDictionary) {
165             NSDictionary *aSyncContainersDictionary = [aSyncAccountsDictionary objectForKey:accountName];
166             NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
167             for (NSString *containerName in aSyncContainersDictionary) {
168                 if (![accountName isEqualToString:@""] || ![[containerName lowercaseString] isEqualToString:@"shared with me"])
169                     [syncContainersDictionary setObject:[NSMutableSet setWithSet:[aSyncContainersDictionary objectForKey:containerName]] 
170                                                  forKey:containerName];
171             }
172             if ([syncContainersDictionary count])
173                 [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
174         }
175         
176         @synchronized(self) {
177             resetSyncDaemonLocalState = YES;
178             syncLastCompleted = nil;
179         }
180     }
181 }
182
183 - (NSDate *)syncLastCompleted {
184     if (self.syncDaemon.lastCompletedSync && ![self.syncDaemon.lastCompletedSync isEqualToDate:syncLastCompleted]) {
185         syncLastCompleted = [self.syncDaemon.lastCompletedSync copy];
186     }
187     return syncLastCompleted;
188 }
189
190 - (PithosSyncDaemon *)syncDaemon {
191     @synchronized(self) {
192         if (self.syncActive && !syncDaemon)
193             syncDaemon = [[PithosSyncDaemon alloc] initWithDirectoryPath:self.syncDirectoryPath 
194                                                            pithosAccount:self 
195                                                       accountsDictionary:self.syncAccountsDictionary 
196                                                               skipHidden:self.syncSkipHidden 
197                                                          resetLocalState:resetSyncDaemonLocalState];
198         resetSyncDaemonLocalState = NO;
199     }
200     return syncDaemon;
201 }
202
203 - (NSString *)serverURL {
204     if (![self urlIsValid:serverURL]) {
205         serverURL = @"https://pithos.okeanos.grnet.gr";
206     }
207     return serverURL;
208 }
209
210 - (void)setServerURL:(NSString *)aServerURL {
211     if (![self.serverURL isEqualToString:aServerURL] && [self urlIsValid:aServerURL]) {
212         serverURL = aServerURL;
213         storageURLPrefix = nil;
214         authURL = nil;
215         publicURLPrefix = nil;
216         loginURLPrefix = nil;
217         userCatalogURL = nil;
218
219         @synchronized(self) {
220             updatePithos = YES;
221             resetSyncDaemonLocalState = YES;
222             syncLastCompleted = nil;
223         }
224     }
225 }
226
227 - (NSString *)versionResource {
228     if (!versionResource) {
229         versionResource = @"v1";
230     }
231     return versionResource;
232 }
233
234 - (NSString *)loginResource {
235     if (!loginResource) {
236         loginResource = @"login";
237     }
238     return loginResource;
239 }
240
241 - (NSString *)userCatalogResource {
242     if (!userCatalogResource) {
243         userCatalogResource = @"user_catalogs";
244     }
245     return userCatalogResource;
246 }
247
248 - (void)setAuthUser:(NSString *)anAuthUser {
249     if ([anAuthUser length] && ![anAuthUser isEqualToString:authUser]) {
250         authUser = anAuthUser;
251         
252         @synchronized(self) {
253             updatePithos = YES;
254             resetSyncDaemonLocalState = YES;
255             syncLastCompleted = nil;
256
257         }
258     }
259 }
260
261 - (void)setAuthToken:(NSString *)anAuthToken {
262     if ([anAuthToken length] && ![anAuthToken isEqualToString:authToken]) {
263         authToken = anAuthToken;
264         
265         @synchronized(self) {
266             updatePithos = YES;
267         }
268     }
269 }
270
271 - (NSString *)storageURLPrefix {
272     if (![self urlIsValid:storageURLPrefix]) {
273         storageURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", self.versionResource];
274     }
275     return storageURLPrefix;
276 }
277
278 - (void)setStorageURLPrefix:(NSString *)aStorageURLPrefix {
279     if (![self.storageURLPrefix isEqualToString:aStorageURLPrefix] && [self urlIsValid:aStorageURLPrefix]) {
280         storageURLPrefix = aStorageURLPrefix;
281     }
282 }
283
284 - (NSString *)authURL {
285     if (![self urlIsValid:authURL]) {
286         authURL = [self.serverURL stringByAppendingFormat:@"/%@", self.versionResource];
287     }
288     return authURL;
289 }
290
291 - (void)setAuthURL:(NSString *)anAuthURL {
292     if (![self.authURL isEqualToString:anAuthURL] && [self urlIsValid:anAuthURL]) {
293         authURL = anAuthURL;
294     }
295 }
296
297 - (NSString *)publicURLPrefix {
298     if (![self urlIsValid:publicURLPrefix]) {
299         if (publicResource)
300             publicURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", publicResource];
301         else
302             publicURLPrefix = [self.serverURL copy];
303     }
304     return publicURLPrefix;
305 }
306
307 - (void)setPublicURLPrefix:(NSString *)aPublicURLPrefix {
308     if (![self.publicURLPrefix isEqualToString:aPublicURLPrefix] && [self urlIsValid:aPublicURLPrefix]) {
309         publicURLPrefix = aPublicURLPrefix;
310     }
311 }
312
313 - (NSString *)loginURLPrefix {
314     if (![self urlIsValid:loginURLPrefix]) {
315         loginURLPrefix = [self.serverURL stringByAppendingFormat:@"/%@", self.loginResource];
316     }
317     return loginURLPrefix;
318 }
319
320 - (void)setLoginURLPrefix:(NSString *)aLoginURLPrefix {
321     if (![self.loginURLPrefix isEqualToString:aLoginURLPrefix] && [self urlIsValid:aLoginURLPrefix]) {
322         loginURLPrefix = aLoginURLPrefix;
323     }
324 }
325
326 - (NSString *)userCatalogURL {
327     if (![self urlIsValid:userCatalogURL]) {
328         userCatalogURL = [self.serverURL stringByAppendingFormat:@"/%@", self.userCatalogResource];
329     }
330     return userCatalogURL;
331 }
332
333 - (void)setUserCatalogURL:(NSString *)aUserCatalogURL {
334     if (![self.userCatalogURL isEqualToString:aUserCatalogURL] && [self urlIsValid:aUserCatalogURL]) {
335         userCatalogURL = aUserCatalogURL;
336     }
337 }
338
339 - (NSMutableDictionary *)userCatalog {
340     if (!userCatalog) {
341         userCatalog = [NSMutableDictionary dictionary];
342     }
343     return userCatalog;
344 }
345
346 - (ASIPithos *)pithos {
347     @synchronized(self) {
348         if (!pithos || updatePithos) {
349             pithos = [ASIPithos pithos];
350             pithos.authUser = authUser;
351             pithos.authToken = authToken;
352             pithos.storageURLPrefix = self.storageURLPrefix;
353             pithos.authURL = self.authURL;
354             pithos.publicURLPrefix = self.publicURLPrefix;
355             pithos.userCatalogURL = self.userCatalogURL;
356             updatePithos = NO;
357         }
358     }
359     return pithos;
360 }
361
362 - (PithosAccountNode *)accountNode {
363     if (!accountNode) {
364         accountNode = [[PithosAccountNode alloc] initWithPithos:self.pithos];
365         accountNode.childrenUpdatedNotificationName = nil;
366         accountNode.inheritChildrenUpdatedNotificationName = YES;
367         accountNode.pithosAccountManager = self;
368     }
369     return accountNode;
370 }
371
372 - (PithosSharingAccountsNode *)sharingAccountsNode {
373     if (!sharingAccountsNode) {
374         sharingAccountsNode = [[PithosSharingAccountsNode alloc] initWithPithos:self.pithos];
375         sharingAccountsNode.childrenUpdatedNotificationName = nil;
376         sharingAccountsNode.inheritChildrenUpdatedNotificationName = YES;
377         sharingAccountsNode.pithosAccountManager = self;
378     }
379     return sharingAccountsNode;
380 }
381
382 #pragma mark -
383 #pragma mark Actions
384
385 - (void)authenticateWithServerURL:(NSString *)aServerURL authUser:(NSString *)anAuthUser authToken:(NSString *)anAuthToken {
386     self.serverURL = aServerURL;
387     self.authUser = anAuthUser;
388     self.authToken = anAuthToken;
389     DLog(@"Account: %@\nauthentication", self);
390     if (![authUser length] || ![authToken length]) {
391         self.active = NO;
392         self.syncActive = NO;
393         // XXX Show preferences with self as the selected account?
394     } else  {
395         self.active = YES;
396         if (syncDaemon) {
397             self.syncDaemon.pithos = self.pithos;
398             if (self.syncActive)
399                 [self.syncDaemon startDaemon];
400         }
401         if (accountNode)
402             self.accountNode.pithos = self.pithos;
403         if (sharingAccountsNode)
404             self.sharingAccountsNode.pithos = self.pithos;
405         if (accountNode) {
406             if (self.accountNode.children) {
407             }
408             [self.accountNode refreshInfo];
409         }
410         if (sharingAccountsNode && self.sharingAccountsNode.children) {
411         }
412         
413         [self updateUserCatalogForForDisplaynames:nil UUIDs:[NSArray arrayWithObject:authUser]];
414     }
415 }
416
417 - (void)loginWithServerURL:(NSString *)aServerURL {
418     self.serverURL = aServerURL;
419     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
420     NSString *loginURL = [NSString stringWithFormat:@"%@?next=pithos://%d/%@&force=", 
421                           self.loginURLPrefix, [processInfo processIdentifier], [ASIPithosRequest encodeToPercentEscape:self.name]];
422     DLog(@"Account: %@\nloginURL: %@", self, loginURL);
423     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:loginURL]];
424 }
425
426 - (void)updateSyncWithSyncActive:(BOOL)aSyncActive 
427                syncDirectoryPath:(NSString *)aSyncDirectoryPath 
428           syncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary 
429                   syncSkipHidden:(BOOL)aSyncSkipHidden {
430     self.syncAccountsDictionary = aSyncAccountsDictionary;
431     self.syncDirectoryPath = aSyncDirectoryPath;
432     self.syncSkipHidden = aSyncSkipHidden;
433     self.syncActive = aSyncActive;
434     if (syncDaemon) {
435         self.syncDaemon.accountsDictionary = self.syncAccountsDictionary;
436         self.syncDaemon.directoryPath = self.syncDirectoryPath;
437         self.syncDaemon.skipHidden = self.syncSkipHidden;
438         if (self.syncActive)
439             [self.syncDaemon startDaemon];
440     }    
441 }
442
443 - (ASIPithosRequest *)updateUserCatalogForForDisplaynames:(NSArray *)displaynames UUIDs:(NSArray *)UUIDs {
444     ASIPithosRequest *userCatalogRequest = [ASIPithosRequest userCatalogRequestWithPithos:self.pithos
445                                                                              displaynames:displaynames
446                                                                                     UUIDs:UUIDs];
447     [PithosUtilities startAndWaitForRequest:userCatalogRequest];
448     if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
449         // Don't show alert on 404, since it can be a pre-UUID server.
450         [PithosUtilities httpRequestErrorAlertWithRequest:userCatalogRequest];
451     } else if (userCatalogRequest.responseStatusCode == 200) {
452         NSDictionary *catalogs = [userCatalogRequest catalogs];
453         NSDictionary *displaynameCatalog = [catalogs objectForKey:@"displayname_catalog"];
454         for (NSString *displayname in displaynameCatalog) {
455             [self.userCatalog setObject:displayname forKey:[displaynameCatalog objectForKey:displayname]];
456         }
457         if (UUIDs) {
458             NSDictionary *UUIDCatalog = [catalogs objectForKey:@"uuid_catalog"];
459             for (NSString *UUID in UUIDs) {
460                 NSString *displayname = [UUIDCatalog objectForKey:UUID];
461                 if (displayname) {
462                     [self.userCatalog setObject:displayname forKey:UUID];
463                 } else {
464                     [self.userCatalog removeObjectForKey:UUID];
465                 }
466             }
467         }
468     }
469     return userCatalogRequest;
470 }
471
472 - (NSString *)displaynameForUUID:(NSString *)UUID safe:(BOOL)safe {
473     NSString *displayName = [userCatalog objectForKey:UUID];
474     if (safe && !displayName) {
475         return UUID;
476     } else {
477         return displayName;
478     }
479 }
480
481 - (NSString *)displaynameForUUID:(NSString *)UUID {
482     return [self displaynameForUUID:UUID safe:NO];
483 }
484
485 #pragma mark -
486 #pragma mark NSCoding
487
488 - (id)initWithCoder:(NSCoder *)decoder {
489     if ((self = [super init])) {
490         self.uniqueName = [decoder decodeObjectForKey:@"uniqueName"];
491         self.active = [decoder decodeBoolForKey:@"active"];
492         name = [decoder decodeObjectForKey:@"name"];
493         self.clientVersion = [decoder decodeObjectForKey:@"clientVersion"];
494
495         self.syncActive = [decoder decodeBoolForKey:@"syncActive"];
496         self.syncDirectoryPath = [decoder decodeObjectForKey:@"syncDirectoryPath"];
497         self.syncAccountsDictionary = [decoder decodeObjectForKey:@"syncAccountsDictionary"];
498         self.syncSkipHidden = [decoder decodeBoolForKey:@"syncSkipHidden"];
499         self.syncLastCompleted = [decoder decodeObjectForKey:@"syncLastCompleted"];
500         
501         self.serverURL = [decoder decodeObjectForKey:@"serverURL"];
502         self.versionResource = [decoder decodeObjectForKey:@"versionResource"];
503         self.loginResource = [decoder decodeObjectForKey:@"loginResource"];
504         self.publicResource = [decoder decodeObjectForKey:@"publicResource"];
505         self.userCatalogResource = [decoder decodeObjectForKey:@"userCatalogResource"];
506         
507         self.authUser = [decoder decodeObjectForKey:@"authUser"];
508         self.authToken = [decoder decodeObjectForKey:@"authToken"];
509         self.storageURLPrefix = [decoder decodeObjectForKey:@"storageURLPrefix"];
510         self.authURL = [decoder decodeObjectForKey:@"authURL"];
511         self.publicURLPrefix = [decoder decodeObjectForKey:@"publicURLPrefix"];
512         self.loginURLPrefix = [decoder decodeObjectForKey:@"loginURLPrefix"];
513         self.userCatalogURL = [decoder decodeObjectForKey:@"userCatalogURL"];
514         self.userCatalog = [decoder decodeObjectForKey:@"userCatalog"];
515         
516         if (![authUser length] || ![authToken length] || ![self.storageURLPrefix length])
517             self.active = NO;
518         
519         NSString *currentVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
520         if (!clientVersion || ![clientVersion isEqualToString:currentVersion]) {
521             resetSyncDaemonLocalState = YES;
522             self.clientVersion = currentVersion;
523         } else {
524             resetSyncDaemonLocalState = NO;
525         }
526     }
527     return self;
528 }
529
530 - (void)encodeWithCoder:(NSCoder *)encoder {
531     [encoder encodeObject:uniqueName forKey:@"uniqueName"];
532     [encoder encodeBool:active forKey:@"active"];
533     [encoder encodeObject:name forKey:@"name"];
534     [encoder encodeObject:clientVersion forKey:@"clientVersion"];
535     
536     [encoder encodeBool:syncActive forKey:@"syncActive"];
537     [encoder encodeObject:syncDirectoryPath forKey:@"syncDirectoryPath"];
538     [encoder encodeObject:syncAccountsDictionary forKey:@"syncAccountsDictionary"];
539     [encoder encodeBool:syncSkipHidden forKey:@"syncSkipHidden"];
540     [encoder encodeObject:self.syncLastCompleted forKey:@"syncLastCompleted"];
541
542     [encoder encodeObject:serverURL forKey:@"serverURL"];
543     [encoder encodeObject:versionResource forKey:@"versionResource"];
544     [encoder encodeObject:publicResource forKey:@"publicResource"];
545     [encoder encodeObject:loginResource forKey:@"loginResource"];
546     [encoder encodeObject:loginResource forKey:@"userCatalogResource"];
547     
548     [encoder encodeObject:authUser forKey:@"authUser"];
549     [encoder encodeObject:authToken forKey:@"authToken"];
550     [encoder encodeObject:storageURLPrefix forKey:@"storageURLPrefix"];
551     [encoder encodeObject:authURL forKey:@"authURL"];
552     [encoder encodeObject:publicURLPrefix forKey:@"publicURLPrefix"];
553     [encoder encodeObject:loginURLPrefix forKey:@"loginURLPrefix"];
554     [encoder encodeObject:userCatalogURL forKey:@"userCatalogURL"];
555     [encoder encodeObject:userCatalog forKey:@"userCatalog"];
556 }
557
558 @end