Add directory exclusion support for a syncing container.
[pithos-macos] / pithos-macos / PithosSyncDaemon.m
index da02195..e1a74f0 100644 (file)
         [self syncOperationFinishedWithSuccess:NO];
         return;
     }
-    NSString *containerDirectoryPath;
     for (ASIPithosContainer *pithosContainer in pithosContainers) {
-        containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
+        NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
         error = nil;
         if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) {
             if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || 
             return;
         }
         self.previousRemoteObjects = remoteObjects;
+        // remoteObjects contains all remote objects for the legal containers, without enforcing directory exclusions
         
         if (operation.isCancelled) {
             [self listRequestFailed:containerRequest];
                               withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
         });
         NSFileManager *fileManager = [NSFileManager defaultManager];
-        NSError *error;
-        NSMutableDictionary *subPaths = [NSMutableDictionary dictionaryWithCapacity:containersCount];
-        NSUInteger subPathsCount = 0;
-        NSString *containerDirectoryPath;
-        NSArray *containerSubPaths;
-        for (ASIPithosContainer *pithosContainer in pithosContainers) {
-            error = nil;
-            containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
-            containerSubPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error];
-            if (error) {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
-                                                            message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
-                                                              error:error];
-                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                          message:@"Sync: Failed to read contents of sync directory" 
-                                                    pithosAccount:pithosAccount];
-                });
-                // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccessfully
-                [self syncOperationFinishedWithSuccess:NO];
-                [pool drain];
-                return;
-            }
-            [subPaths setObject:containerSubPaths forKey:pithosContainer.name];
-            subPathsCount += [containerSubPaths count];
-        }        
         
-        if (operation.isCancelled) {
-            operation.completionBlock = nil;
-            [self syncOperationFinishedWithSuccess:NO];
-            [pool drain];
-            return;
-        }
-
-        self.currentLocalObjectStates = [NSMutableDictionary dictionaryWithCapacity:subPathsCount];
-        NSMutableDictionary *containerStoredLocalObjectStates;
+        // Compute current state of legal existing local objects 
+        // and add an empty stored state for legal new local objects since last sync
+        self.currentLocalObjectStates = [NSMutableDictionary dictionary];
         for (ASIPithosContainer *pithosContainer in pithosContainers) {
-            containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
-            containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
-            for (NSString *objectName in [subPaths objectForKey:pithosContainer.name]) {
+            NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
+            NSMutableArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name];
+            BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""];
+            NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
+            NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:containerDirectoryPath];
+            for (NSString *objectName in dirEnumerator) {
                 if (operation.isCancelled) {
                     operation.completionBlock = nil;
                     [self saveLocalState];
                     return;
                 }
 
-                PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:objectName];
                 NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
-                if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
-                    // New or modified existing local object, compute current state
-                    if (!storedLocalObjectState)
-                        // For new local object, also create empty stored state
-                        [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
-                    [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath 
-                                                                                               blockHash:pithosContainer.blockHash 
-                                                                                               blockSize:pithosContainer.blockSize] 
-                                                          forKey:filePath];
-                } else {
-                    // Local object hasn't changed, set stored state also to current
-                    [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath];
+                NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
+                BOOL isDirectory;
+                BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
+                if ([[attributes fileType] isEqualToString:NSFileTypeSymbolicLink]) {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [PithosUtilities fileActionFailedAlertWithTitle:@"Sync Error" 
+                                                                message:[NSString stringWithFormat:@"Sync directory at '%@' contains symbolic links. Remove them and re-activate sync for account '%@'.", 
+                                                                         containerDirectoryPath, pithosAccount.name] 
+                                                                  error:nil];
+                    });
+                    pithosAccount.syncActive = NO;
+                    return;
+                } else if (fileExists) {
+                    NSArray *pathComponents = [objectName pathComponents];
+                    if ([pathComponents count] == 1) {
+                        if ([containerExcludedDirectories containsObject:[objectName lowercaseString]]) {
+                            // Skip excluded directory and its descendants, or root file with same name
+                            if (isDirectory)
+                                [dirEnumerator skipDescendants];
+                            // Remove stored state if any
+                            [containerStoredLocalObjectStates removeObjectForKey:objectName];
+                            continue;
+                        } else if (!isDirectory && containerExludeRootFiles) {
+                            // Skip excluded root file
+                            // Remove stored state if any
+                            [containerStoredLocalObjectStates removeObjectForKey:objectName];
+                            continue;
+                        }
+                    }
+                    // Include local object
+                    PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:objectName];
+                    if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
+                        // New or modified existing local object, compute current state
+                        if (!storedLocalObjectState)
+                            // For new local object, also create empty stored state
+                            [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
+                        [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath 
+                                                                                                   blockHash:pithosContainer.blockHash 
+                                                                                                   blockSize:pithosContainer.blockSize] 
+                                                     forKey:filePath];
+                    } else {
+                        // Local object hasn't changed, set stored state also to current
+                        [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath];
+                    }
                 }
             }
             [self saveLocalState];
-        }
+        }    
         
         if (operation.isCancelled) {
             operation.completionBlock = nil;
             return;
         }
 
+        // Add an empty stored state for legal new remote objects since last sync
         for (ASIPithosContainer *pithosContainer in pithosContainers) {
-            containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
-            for (NSString *objectName in [remoteObjects objectForKey:pithosContainer.name]) {
+            NSMutableArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name];
+            BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""];
+            NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
+            NSMutableDictionary *containerRemoteObjects = [remoteObjects objectForKey:pithosContainer.name];
+            for (NSString *objectName in containerRemoteObjects) {
                 if (operation.isCancelled) {
                     operation.completionBlock = nil;
                     [self saveLocalState];
                     return;
                 }
 
-                if (![containerStoredLocalObjectStates objectForKey:objectName])
-                    [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
+                ASIPithosObject *object = [containerRemoteObjects objectForKey:objectName];
+                NSString *localObjectName;
+                if ([object.name hasSuffix:@"/"])
+                    localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
+                else
+                    localObjectName = [NSString stringWithString:object.name];
+                NSArray *pathComponents = [localObjectName pathComponents];
+                if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
+                    // Skip excluded directory object and its descendants, or root file object with same name
+                    // Remove stored state if any
+                    [containerStoredLocalObjectStates removeObjectForKey:object.name];
+                    continue;
+                } else if (containerExludeRootFiles && 
+                           ([pathComponents count] == 1) && 
+                           ![PithosUtilities isContentTypeDirectory:object.contentType]) {
+                    // Skip root file object
+                    // Remove stored state if any
+                    [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
+                    continue;
+                }
+                if (![containerStoredLocalObjectStates objectForKey:object.name])
+                    [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:object.name];
             }
             [self saveLocalState];
         }
             return;
         }
 
-        NSMutableDictionary *containerRemoteObjects;
+        // For each stored state compare with current and remote state
+        // Stored states of local objects that have been deleted, 
+        // haven't been checked for legality (only existing local remote objects)
+        // These should be identified and skipped
         for (ASIPithosContainer *pithosContainer in pithosContainers) {
-            containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
-            containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
-            containerRemoteObjects = [remoteObjects objectForKey:pithosContainer.name];
+            NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name];
+            NSMutableArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name];
+            BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""];
+            NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name];
+            NSMutableDictionary *containerRemoteObjects = [remoteObjects objectForKey:pithosContainer.name];
             for (NSString *objectName in [[containerStoredLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
                 if (operation.isCancelled) {
                     operation.completionBlock = nil;
             
                 PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:object.name];
                 PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath];
-                if (!currentLocalObjectState)
+                if (!currentLocalObjectState) {
+                    // The stored state corresponds to a remote or deleted local object, that's why there is no current state
+                    // In the latter case it must be checked for legality, which can be determined by its stored state
+                    // If it existed locally, but was deleted, state.exists is true, 
+                    // else if the stored state is an empty state that was created due to the server object, state.exists is false
+                    if (storedLocalObjectState.exists) {
+                        NSString *localObjectName;
+                        if ([object.name hasSuffix:@"/"])
+                            localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
+                        else
+                            localObjectName = [NSString stringWithString:object.name];
+                        NSArray *pathComponents = [localObjectName pathComponents];
+                        if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
+                            // Skip excluded directory object and its descendants, or root file object with same name
+                            // Remove stored state
+                            [containerStoredLocalObjectStates removeObjectForKey:object.name];
+                            [self saveLocalState];
+                            continue;
+                        } else if (containerExludeRootFiles && ([pathComponents count] == 1) && !storedLocalObjectState.isDirectory) {
+                            // Skip root file object
+                            // Remove stored state
+                            [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
+                            [self saveLocalState];
+                            continue;
+                        }
+                    }
+                    // There is also the off case that a local object has been created in the meantime
+                    // This call works in any case, existent or non-existent local object 
                     currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath 
                                                                                      blockHash:pithosContainer.blockHash 
                                                                                      blockSize:pithosContainer.blockSize];
-//                if (currentLocalObjectState.isDirectory)
-//                    object.contentType = @"application/directory";
+                }
             
                 PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState];
                 ASIPithosObject *remoteObject = [containerRemoteObjects objectForKey:object.name];
                 if (remoteObject) {
                     if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) {
                         remoteObjectState.isDirectory = YES;
-//                        object.contentType = @"application/directory";
                     } else {
                         remoteObjectState.hash = remoteObject.objectHash;
                     }