From 0224a49fa4560d37a92db9a3571513ce9a7a64a2 Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Thu, 20 Oct 2011 11:26:51 +0300 Subject: [PATCH] Initial implementation of the syncing algorithm. Other fixes and changes. --- pithos-apple-common | 2 +- pithos-macos.xcodeproj/project.pbxproj | 76 +- pithos-macos/FileMD5Hash.c | 120 +++ pithos-macos/FileMD5Hash.h | 73 ++ pithos-macos/PithosActivity.h | 3 +- pithos-macos/PithosActivityFacility.h | 1 + pithos-macos/PithosActivityFacility.m | 13 + pithos-macos/PithosBrowserController.m | 32 +- pithos-macos/PithosLocalObjectState.h | 54 ++ pithos-macos/PithosLocalObjectState.m | 112 +++ pithos-macos/PithosSyncDaemon.h | 80 ++ pithos-macos/PithosSyncDaemon.m | 1293 ++++++++++++++++++++++++++++++++ pithos-macos/PithosUtilities.h | 12 + pithos-macos/PithosUtilities.m | 74 +- pithos-macos/en.lproj/MainMenu.xib | 362 +-------- pithos-macos/pithos-macos-Info.plist | 18 +- pithos-macos/pithos_macosAppDelegate.h | 12 + pithos-macos/pithos_macosAppDelegate.m | 48 +- 18 files changed, 1975 insertions(+), 410 deletions(-) create mode 100644 pithos-macos/FileMD5Hash.c create mode 100644 pithos-macos/FileMD5Hash.h create mode 100644 pithos-macos/PithosLocalObjectState.h create mode 100644 pithos-macos/PithosLocalObjectState.m create mode 100644 pithos-macos/PithosSyncDaemon.h create mode 100644 pithos-macos/PithosSyncDaemon.m diff --git a/pithos-apple-common b/pithos-apple-common index 20e74a1..0a4b9bd 160000 --- a/pithos-apple-common +++ b/pithos-apple-common @@ -1 +1 @@ -Subproject commit 20e74a13c1a904064b0544b398b677cc30f28ae8 +Subproject commit 0a4b9bd3aa71857ebe3439befde0dc5c3b3c96b0 diff --git a/pithos-macos.xcodeproj/project.pbxproj b/pithos-macos.xcodeproj/project.pbxproj index 54aac14..e45a1c5 100644 --- a/pithos-macos.xcodeproj/project.pbxproj +++ b/pithos-macos.xcodeproj/project.pbxproj @@ -69,6 +69,9 @@ 61C65AE31428D41C002597C2 /* PolicyVersioningTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 61C65AE21428D41C002597C2 /* PolicyVersioningTransformer.m */; }; 61C65AE6142918DD002597C2 /* PithosObjectNodeInfoController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61C65AE5142918DD002597C2 /* PithosObjectNodeInfoController.m */; }; 61E99D9413EC348500E48DA5 /* 145-persondot.png in Resources */ = {isa = PBXBuildFile; fileRef = 61E99D9313EC348500E48DA5 /* 145-persondot.png */; }; + 61F040F31448547000A0C788 /* FileMD5Hash.c in Sources */ = {isa = PBXBuildFile; fileRef = 61F040F11448547000A0C788 /* FileMD5Hash.c */; }; + 61F04132144DB97200A0C788 /* PithosLocalObjectState.m in Sources */ = {isa = PBXBuildFile; fileRef = 61F040EE144757B500A0C788 /* PithosLocalObjectState.m */; }; + 61F04133144DB97600A0C788 /* PithosSyncDaemon.m in Sources */ = {isa = PBXBuildFile; fileRef = 61F040EA144724F500A0C788 /* PithosSyncDaemon.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -192,6 +195,12 @@ 61C65AE4142918DC002597C2 /* PithosObjectNodeInfoController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PithosObjectNodeInfoController.h; sourceTree = ""; }; 61C65AE5142918DD002597C2 /* PithosObjectNodeInfoController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PithosObjectNodeInfoController.m; sourceTree = ""; }; 61E99D9313EC348500E48DA5 /* 145-persondot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "145-persondot.png"; sourceTree = ""; }; + 61F040E9144724F500A0C788 /* PithosSyncDaemon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PithosSyncDaemon.h; sourceTree = ""; }; + 61F040EA144724F500A0C788 /* PithosSyncDaemon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PithosSyncDaemon.m; sourceTree = ""; }; + 61F040ED144757B500A0C788 /* PithosLocalObjectState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PithosLocalObjectState.h; sourceTree = ""; }; + 61F040EE144757B500A0C788 /* PithosLocalObjectState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PithosLocalObjectState.m; sourceTree = ""; }; + 61F040F11448547000A0C788 /* FileMD5Hash.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = FileMD5Hash.c; path = "pithos-macos/FileMD5Hash.c"; sourceTree = ""; }; + 61F040F21448547000A0C788 /* FileMD5Hash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FileMD5Hash.h; path = "pithos-macos/FileMD5Hash.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -253,25 +262,17 @@ 610DD2FB13E6BB2000ED982F /* pithos-macos */ = { isa = PBXGroup; children = ( + 61F040E71447218F00A0C788 /* PithosBrowser */, + 61F040E51447217100A0C788 /* PithosSyncDaemon */, + 61F040E8144721B200A0C788 /* PithosActivityFacility */, 61F1C5DB1444A8CF00C1E6EB /* PithosNodes */, 61F1C5DD1444A92B00C1E6EB /* PithosNodeInfoControllers */, 610DD2FC13E6BB2000ED982F /* Supporting Files */, 610DD30713E6BB2000ED982F /* pithos_macosAppDelegate.h */, 610DD30813E6BB2000ED982F /* pithos_macosAppDelegate.m */, 610DD30A13E6BB2000ED982F /* MainMenu.xib */, - 610DD34C13E6BEF400ED982F /* PithosBrowserController.h */, - 610DD34D13E6BEF400ED982F /* PithosBrowserController.m */, - 610DD34F13E6C00E00ED982F /* PithosBrowserController.xib */, - 6121250813F033F400063041 /* PithosBrowserPreviewController.xib */, - 619B85D213F8076F00C9371F /* PithosPreferencesController.h */, - 619B85D313F8077100C9371F /* PithosPreferencesController.m */, - 619B85D413F8077300C9371F /* PithosPreferencesController.xib */, 61C24BEA14161EC0007004DC /* PithosUtilities.h */, 61C24BEB14161EC3007004DC /* PithosUtilities.m */, - 618A7FD61438CE5D0040F043 /* PithosActivityFacility.h */, - 618A7FD71438CE5D0040F043 /* PithosActivityFacility.m */, - 618A7FF4143A20830040F043 /* PithosActivity.h */, - 618A7FF5143A20830040F043 /* PithosActivity.m */, ); path = "pithos-macos"; sourceTree = ""; @@ -350,8 +351,7 @@ 6139837313F01CFC004CE444 /* Utilities */ = { isa = PBXGroup; children = ( - 61433BC7141BA1CE00CD978D /* HashMapHash.h */, - 61433BC8141BA1CE00CD978D /* HashMapHash.m */, + 61F040F41448547B00A0C788 /* Hashing */, 615A442E140E5ECA00308614 /* Formatters */, 6180C22713FAED1D00BCA40B /* Cells */, 6180C22813FAED4B00BCA40B /* Value Transformers */, @@ -412,6 +412,53 @@ name = "Value Transformers"; sourceTree = ""; }; + 61F040E51447217100A0C788 /* PithosSyncDaemon */ = { + isa = PBXGroup; + children = ( + 61F040E9144724F500A0C788 /* PithosSyncDaemon.h */, + 61F040EA144724F500A0C788 /* PithosSyncDaemon.m */, + 61F040ED144757B500A0C788 /* PithosLocalObjectState.h */, + 61F040EE144757B500A0C788 /* PithosLocalObjectState.m */, + ); + name = PithosSyncDaemon; + sourceTree = ""; + }; + 61F040E71447218F00A0C788 /* PithosBrowser */ = { + isa = PBXGroup; + children = ( + 610DD34C13E6BEF400ED982F /* PithosBrowserController.h */, + 610DD34D13E6BEF400ED982F /* PithosBrowserController.m */, + 610DD34F13E6C00E00ED982F /* PithosBrowserController.xib */, + 6121250813F033F400063041 /* PithosBrowserPreviewController.xib */, + 619B85D213F8076F00C9371F /* PithosPreferencesController.h */, + 619B85D313F8077100C9371F /* PithosPreferencesController.m */, + 619B85D413F8077300C9371F /* PithosPreferencesController.xib */, + ); + name = PithosBrowser; + sourceTree = ""; + }; + 61F040E8144721B200A0C788 /* PithosActivityFacility */ = { + isa = PBXGroup; + children = ( + 618A7FD61438CE5D0040F043 /* PithosActivityFacility.h */, + 618A7FD71438CE5D0040F043 /* PithosActivityFacility.m */, + 618A7FF4143A20830040F043 /* PithosActivity.h */, + 618A7FF5143A20830040F043 /* PithosActivity.m */, + ); + name = PithosActivityFacility; + sourceTree = ""; + }; + 61F040F41448547B00A0C788 /* Hashing */ = { + isa = PBXGroup; + children = ( + 61433BC7141BA1CE00CD978D /* HashMapHash.h */, + 61433BC8141BA1CE00CD978D /* HashMapHash.m */, + 61F040F11448547000A0C788 /* FileMD5Hash.c */, + 61F040F21448547000A0C788 /* FileMD5Hash.h */, + ); + name = Hashing; + sourceTree = ""; + }; 61F1C5DB1444A8CF00C1E6EB /* PithosNodes */ = { isa = PBXGroup; children = ( @@ -566,6 +613,9 @@ 618A7FF6143A20830040F043 /* PithosActivity.m in Sources */, 613629B5143E0F8B00363787 /* GroupMembersDictionaryTransformer.m in Sources */, 613629B9143E51E000363787 /* GroupAndGroupMemberFormatter.m in Sources */, + 61F040F31448547000A0C788 /* FileMD5Hash.c in Sources */, + 61F04132144DB97200A0C788 /* PithosLocalObjectState.m in Sources */, + 61F04133144DB97600A0C788 /* PithosSyncDaemon.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/pithos-macos/FileMD5Hash.c b/pithos-macos/FileMD5Hash.c new file mode 100644 index 0000000..a889e51 --- /dev/null +++ b/pithos-macos/FileMD5Hash.c @@ -0,0 +1,120 @@ +/* + * FileMD5Hash.c + * FileMD5Hash + * + * Copyright © 2010 Joel Lopes Da Silva. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +//--------------------------------------------------------- +// Includes +//--------------------------------------------------------- + +// Header file +#include "FileMD5Hash.h" + +// Standard library +#include +#include + +// Core Foundation +#include + +// Cryptography +#include + + +//--------------------------------------------------------- +// Function definition +//--------------------------------------------------------- + +CFStringRef FileMD5HashCreateWithPath(CFStringRef filePath, + size_t chunkSizeForReadingData) { + + // Declare needed variables + CFStringRef result = NULL; + CFReadStreamRef readStream = NULL; + + // Get the file URL + CFURLRef fileURL = + CFURLCreateWithFileSystemPath(kCFAllocatorDefault, + (CFStringRef)filePath, + kCFURLPOSIXPathStyle, + (Boolean)false); + if (!fileURL) goto done; + + // Create and open the read stream + readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, + (CFURLRef)fileURL); + if (!readStream) goto done; + bool didSucceed = (bool)CFReadStreamOpen(readStream); + if (!didSucceed) goto done; + + // Initialize the hash object + CC_MD5_CTX hashObject; + CC_MD5_Init(&hashObject); + + // Make sure chunkSizeForReadingData is valid + if (!chunkSizeForReadingData) { + chunkSizeForReadingData = FileHashDefaultChunkSizeForReadingData; + } + + // Feed the data to the hash object + bool hasMoreData = true; + while (hasMoreData) { + uint8_t buffer[chunkSizeForReadingData]; + CFIndex readBytesCount = CFReadStreamRead(readStream, + (UInt8 *)buffer, + (CFIndex)sizeof(buffer)); + if (readBytesCount == -1) break; + if (readBytesCount == 0) { + hasMoreData = false; + continue; + } + CC_MD5_Update(&hashObject, + (const void *)buffer, + (CC_LONG)readBytesCount); + } + + // Check if the read operation succeeded + didSucceed = !hasMoreData; + + // Compute the hash digest + unsigned char digest[CC_MD5_DIGEST_LENGTH]; + CC_MD5_Final(digest, &hashObject); + + // Abort if the read operation failed + if (!didSucceed) goto done; + + // Compute the string result + char hash[2 * sizeof(digest) + 1]; + for (size_t i = 0; i < sizeof(digest); ++i) { + snprintf(hash + (2 * i), 3, "%02x", (int)(digest[i])); + } + result = CFStringCreateWithCString(kCFAllocatorDefault, + (const char *)hash, + kCFStringEncodingUTF8); + +done: + + if (readStream) { + CFReadStreamClose(readStream); + CFRelease(readStream); + } + if (fileURL) { + CFRelease(fileURL); + } + return result; +} diff --git a/pithos-macos/FileMD5Hash.h b/pithos-macos/FileMD5Hash.h new file mode 100644 index 0000000..5e9de69 --- /dev/null +++ b/pithos-macos/FileMD5Hash.h @@ -0,0 +1,73 @@ +/* + * FileMD5Hash.h + * FileMD5Hash + * + * Copyright © 2010 Joel Lopes Da Silva. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef FILEMD5HASH_H +#define FILEMD5HASH_H + +//--------------------------------------------------------- +// Includes +//--------------------------------------------------------- + +// Core Foundation +#include + + +//--------------------------------------------------------- +// Constant declaration +//--------------------------------------------------------- + +// In bytes +#define FileHashDefaultChunkSizeForReadingData 4096 + + +//--------------------------------------------------------- +// Function declaration +//--------------------------------------------------------- + +#include + +// General imports for Objective-C +#ifdef __OBJC__ +#if TARGET_OS_IPHONE +#import +#import +#elif TARGET_OS_MAC +#import +#endif +#endif + + +//--------------------------------------------------------- +// Macros +//--------------------------------------------------------- + +// Extern +#if defined(__cplusplus) +#define FILEMD5HASH_EXTERN extern "C" +#else +#define FILEMD5HASH_EXTERN extern +#endif + + +FILEMD5HASH_EXTERN CFStringRef FileMD5HashCreateWithPath(CFStringRef filePath, + size_t chunkSizeForReadingData); + + +#endif diff --git a/pithos-macos/PithosActivity.h b/pithos-macos/PithosActivity.h index c94b751..6569d69 100644 --- a/pithos-macos/PithosActivity.h +++ b/pithos-macos/PithosActivity.h @@ -43,7 +43,8 @@ typedef enum { PithosActivityCopy, PithosActivityMove, PithosActivityCreateDirectory, - PithosActivityDelete + PithosActivityDelete, + PithosActivityOther } PithosActivityType; @interface PithosActivity : NSObject { diff --git a/pithos-macos/PithosActivityFacility.h b/pithos-macos/PithosActivityFacility.h index 8006962..912f246 100644 --- a/pithos-macos/PithosActivityFacility.h +++ b/pithos-macos/PithosActivityFacility.h @@ -68,6 +68,7 @@ currentBytes:(NSUInteger)currentBytes; - (PithosActivity *)startActivityWithType:(PithosActivityType)type message:(NSString *)message; +- (PithosActivity *)startAndEndActivityWithType:(PithosActivityType)type message:(NSString *)message; - (void)updateActivity:(PithosActivity *)activity withMessage:(NSString *)message totalBytes:(NSUInteger)totalBytes diff --git a/pithos-macos/PithosActivityFacility.m b/pithos-macos/PithosActivityFacility.m index 168476c..a11ab79 100644 --- a/pithos-macos/PithosActivityFacility.m +++ b/pithos-macos/PithosActivityFacility.m @@ -228,6 +228,19 @@ static PithosActivityFacility *defaultPithosActivityFacility = nil; return [self startActivityWithType:type message:message totalBytes:0 currentBytes:0]; } +- (PithosActivity *)startAndEndActivityWithType:(PithosActivityType)type message:(NSString *)message { + PithosActivity *activity = [[[PithosActivity alloc] initWithType:type] autorelease]; + activity.message = message; + activity.totalBytes = 0; + activity.currentBytes = 0; + @synchronized(self) { + [endingActivities addObject:activity]; + } + NSLog(@"PithosActivityFacility startedAndEndedActivity %@", activity); + + return activity; +} + - (void)updateActivity:(PithosActivity *)activity withMessage:(NSString *)message totalBytes:(NSUInteger)totalBytes diff --git a/pithos-macos/PithosBrowserController.m b/pithos-macos/PithosBrowserController.m index 0b42490..6bffdca 100644 --- a/pithos-macos/PithosBrowserController.m +++ b/pithos-macos/PithosBrowserController.m @@ -97,7 +97,7 @@ @end -@interface PithosBrowserController (Private) {} +@interface PithosBrowserController (Private) - (void)resetContainers:(NSNotification *)notification; - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode; - (BOOL)moveNodes:(NSArray *)nodes toNode:(PithosNode *)destinationNode; @@ -741,7 +741,7 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { - (BOOL)uploadFiles:(NSArray *)filenames toNode:(PithosNode *)destinationNode { if (([destinationNode class] != [PithosSubdirNode class]) && ([destinationNode class] != [PithosContainerNode class])) return NO; - NSFileManager *defaultManager = [NSFileManager defaultManager]; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *containerName = [NSString stringWithString:destinationNode.pithosContainer.name]; NSString *objectNamePrefix; if ([destinationNode class] == [PithosSubdirNode class]) @@ -757,7 +757,7 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { if ([containerRequest error]) { [PithosUtilities httpRequestErrorAlertWithRequest:containerRequest]; return NO; - } else if (containerRequest.responseStatusCode != 200) { + } else if (containerRequest.responseStatusCode != 204) { [PithosUtilities unexpectedResponseStatusAlertWithRequest:containerRequest]; return NO; } @@ -769,7 +769,7 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { for (NSString *filePath in filenames) { BOOL isDirectory; - if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { + if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { if (!isDirectory) { // Upload file NSString *objectName = [objectNamePrefix stringByAppendingPathComponent:[filePath lastPathComponent]]; @@ -1088,9 +1088,9 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { // XXX change contentLength to objectContentLength if it is fixed in the server if (([objectRequest contentLength] == 0) && (![[objectRequest contentType] isEqualToString:@"application/directory"])) { NSLog(@"Downloaded 0 bytes"); - NSFileManager *defaultManager = [NSFileManager defaultManager]; - if (![defaultManager fileExistsAtPath:filePath]) { - if (![defaultManager createFileAtPath:filePath contents:nil attributes:nil]) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:filePath]) { + if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText:@"Create File Error"]; [alert setInformativeText:[NSString stringWithFormat:@"Cannot create zero length file at %@", filePath]]; @@ -1900,20 +1900,22 @@ forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column { for (PithosNode *node in ((NSArray *)[sender representedObject])) { if (([node class] == [PithosObjectNode class]) || (([node class] == [PithosSubdirNode class]) && !node.pithosObject.subdir && [node.pithosObject.name hasSuffix:@"/"])) { + NSString *fileName = [node.pithosObject.name lastPathComponent]; + if ([node.pithosObject.name hasSuffix:@"/"]) + fileName = [fileName stringByAppendingString:@"/"]; ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:node.pithosContainer.name objectName:node.pithosObject.name]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(deleteObjectFinished:); objectRequest.didFailSelector = @selector(deleteObjectFailed:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete - message:[NSString stringWithFormat:@"Deleting '%@'", - [objectRequest.userInfo objectForKey:@"fileName"]]]; - [(NSMutableDictionary *)(objectRequest.userInfo) addEntriesFromDictionary: - [NSDictionary dictionaryWithObjectsAndKeys: - [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", - activity, @"activity", - [NSNumber numberWithUnsignedInteger:10], @"retries", - nil]]; + message:[NSString stringWithFormat:@"Deleting '%@'", fileName]]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + fileName, @"fileName", + [NSArray arrayWithObject:node.parent], @"forceRefreshNodes", + activity, @"activity", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; } else if ([node class] == [PithosSubdirNode class]) { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); diff --git a/pithos-macos/PithosLocalObjectState.h b/pithos-macos/PithosLocalObjectState.h new file mode 100644 index 0000000..50a8688 --- /dev/null +++ b/pithos-macos/PithosLocalObjectState.h @@ -0,0 +1,54 @@ +// +// PithosLocalObjectState.h +// pithos-macos +// +// Copyright 2011 GRNET S.A. All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and +// documentation are those of the authors and should not be +// interpreted as representing official policies, either expressed +// or implied, of GRNET S.A. + +#import + +@interface PithosLocalObjectState : NSObject { + NSString *md5; + NSString *hashMapHash; + NSString *tmpDownloadFile; + BOOL isDirectory; +} + ++ (id)nullObjectState; + +@property (nonatomic, retain) NSString *md5; +@property (nonatomic, retain) NSString *hashMapHash; +@property (nonatomic, retain) NSString *tmpDownloadFile; +@property (nonatomic, assign) BOOL isDirectory; + +@end diff --git a/pithos-macos/PithosLocalObjectState.m b/pithos-macos/PithosLocalObjectState.m new file mode 100644 index 0000000..091cd19 --- /dev/null +++ b/pithos-macos/PithosLocalObjectState.m @@ -0,0 +1,112 @@ +// +// PithosLocalObjectState.m +// pithos-macos +// +// Copyright 2011 GRNET S.A. All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and +// documentation are those of the authors and should not be +// interpreted as representing official policies, either expressed +// or implied, of GRNET S.A. + +#import "PithosLocalObjectState.h" +#import "PithosUtilities.h" + +@implementation PithosLocalObjectState +@synthesize md5, hashMapHash, tmpDownloadFile, isDirectory; + +#pragma mark - +#pragma mark Object Lifecycle + ++ (id)nullObjectState { + PithosLocalObjectState *localObjectState = [[[self alloc] init] autorelease]; + localObjectState.md5 = @" "; + localObjectState.hashMapHash = @" "; + localObjectState.tmpDownloadFile = nil; + localObjectState.isDirectory = NO; + return localObjectState; +} + +- (void)dealloc { + self.tmpDownloadFile = nil; + [hashMapHash release]; + [md5 release]; + [super dealloc]; +} + +#pragma mark - +#pragma mark Properties + +- (void)setIsDirectory:(BOOL)anIsDirectory { + isDirectory = anIsDirectory; + if (isDirectory) { + self.md5 = @" "; + self.hashMapHash = @" "; + self.tmpDownloadFile = nil; + } +} + +- (void)setTmpDownloadFile:(NSString *)aTmpDownloadFile { + if (![tmpDownloadFile isEqualToString:aTmpDownloadFile]) { + if (!aTmpDownloadFile) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:tmpDownloadFile]) { + NSError *error = nil; + if (![fileManager removeItemAtPath:tmpDownloadFile error:&error] || error) + [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" + message:[NSString stringWithFormat:@"Cannot remove file at '%@'", tmpDownloadFile] + error:error]; + } + } + [tmpDownloadFile release]; + tmpDownloadFile = [aTmpDownloadFile retain]; + } +} + +#pragma mark - +#pragma mark NSCoding + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + self.md5 = [decoder decodeObjectForKey:@"md5"]; + self.hashMapHash = [decoder decodeObjectForKey:@"hashMapHash"]; + self.tmpDownloadFile = [decoder decodeObjectForKey:@"tmpDownloadFile"]; + self.isDirectory = [decoder decodeBoolForKey:@"isDirectory"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:md5 forKey:@"md5"]; + [encoder encodeObject:hashMapHash forKey:@"hashMapHash"]; + [encoder encodeObject:tmpDownloadFile forKey:@"tmpDownloadFile"]; + [encoder encodeBool:isDirectory forKey:@"isDirectory"]; +} + +@end diff --git a/pithos-macos/PithosSyncDaemon.h b/pithos-macos/PithosSyncDaemon.h new file mode 100644 index 0000000..8b454a3 --- /dev/null +++ b/pithos-macos/PithosSyncDaemon.h @@ -0,0 +1,80 @@ +// +// PithosSyncDaemon.h +// pithos-macos +// +// Copyright 2011 GRNET S.A. All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and +// documentation are those of the authors and should not be +// interpreted as representing official policies, either expressed +// or implied, of GRNET S.A. + +#import +@class PithosActivityFacility; +@class ASINetworkQueue; + +@interface PithosSyncDaemon : NSObject { + NSString *directoryPath; + NSString *containerName; + NSTimeInterval timeInterval; + + NSString *blockHash; + NSUInteger blockSize; + NSDate *lastModified; + NSMutableArray *objects; + NSMutableDictionary *remoteObjects; + NSMutableDictionary *storedLocalObjectStates; + + NSString *pithosStateFilePath; + NSString *tempDownloadsDirPath; + + NSUInteger syncOperationCount; + BOOL newSyncRequested; + + ASINetworkQueue *queue; + NSTimer *timer; + + PithosActivityFacility *activityFacility; +} + +@property (nonatomic, retain) NSString *blockHash; +@property (nonatomic, assign) NSUInteger blockSize; +@property (nonatomic, retain) NSDate *lastModified; +@property (nonatomic, retain) NSMutableDictionary *remoteObjects; +@property (nonatomic, retain) NSMutableDictionary *storedLocalObjectStates; + +@property (nonatomic, readonly) NSString *pithosStateFilePath; +@property (nonatomic, readonly) NSString *tempDownloadsDirPath; + +- (id)initWithDirectoryPath:(NSString *)aDirectoryPath + containerName:(NSString *)aContainerName + timeInterval:(NSTimeInterval)aTimeInterval; +- (void)sync; + +@end diff --git a/pithos-macos/PithosSyncDaemon.m b/pithos-macos/PithosSyncDaemon.m new file mode 100644 index 0000000..107d154 --- /dev/null +++ b/pithos-macos/PithosSyncDaemon.m @@ -0,0 +1,1293 @@ +// +// PithosSyncDaemon.m +// pithos-macos +// +// Copyright 2011 GRNET S.A. All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and +// documentation are those of the authors and should not be +// interpreted as representing official policies, either expressed +// or implied, of GRNET S.A. + +#import "PithosSyncDaemon.h" +#import "PithosLocalObjectState.h" +#import "PithosActivityFacility.h" +#import "PithosUtilities.h" +#import "ASINetworkQueue.h" +#import "ASIPithosRequest.h" +#import "ASIPithosContainerRequest.h" +#import "ASIPithosObjectRequest.h" +#import "ASIPithosObject.h" +#import "FileMD5Hash.h" +#import "HashMapHash.h" + +#define DATA_MODEL_FILE @"localstate.archive" +#define ARCHIVE_KEY @"Data" + +@interface PithosSyncDaemon (Private) +- (NSString *)pithosStateFilePath; +- (void)saveLocalState; +- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState; +- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState + remoteObjectHash:(NSString *)remoteObjectHash + remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory; +- (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath; +-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState + object:(ASIPithosObject *)object + localFilePath:(NSString *)filePath; +- (void)requestFailed:(ASIPithosRequest *)request; +@end + +@implementation PithosSyncDaemon +@synthesize blockHash, blockSize, lastModified, remoteObjects, storedLocalObjectStates; +@synthesize pithosStateFilePath, tempDownloadsDirPath; + +#pragma mark - +#pragma Object Lifecycle + +- (id)initWithDirectoryPath:(NSString *)aDirectoryPath + containerName:(NSString *)aContainerName + timeInterval:(NSTimeInterval)aTimeInterval { + if ((self = [super init])) { + directoryPath = [aDirectoryPath copy]; + containerName = [aContainerName copy]; + timeInterval = aTimeInterval; + + syncOperationCount = 0; + newSyncRequested = NO; + + activityFacility = [PithosActivityFacility defaultPithosActivityFacility]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) { + NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath]; + NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease]; + self.storedLocalObjectStates = [unarchiver decodeObjectForKey:ARCHIVE_KEY]; + [unarchiver finishDecoding]; + } else { + self.storedLocalObjectStates = [NSMutableDictionary dictionary]; + } + + queue = [[ASINetworkQueue alloc] init]; + queue.shouldCancelAllRequestsOnFailure = NO; + [queue go]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillTerminate:) + name:NSApplicationWillTerminateNotification + object:[NSApplication sharedApplication]]; + + timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain]; + [timer fire]; + } + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [queue cancelAllOperations]; + [queue release]; + [timer invalidate]; + [timer release]; + [tempDownloadsDirPath release]; + [pithosStateFilePath release]; + [storedLocalObjectStates release]; + [remoteObjects release]; + [objects release]; + [lastModified release]; + [blockHash release]; + [containerName release]; + [directoryPath release]; + [super dealloc]; +} + +#pragma mark - +#pragma mark Observers + +- (void)applicationWillTerminate:(NSNotification *)notification { + [self saveLocalState]; +} + +#pragma mark - +#pragma mark Properties + +- (NSString *)pithosStateFilePath { + if (!pithosStateFilePath) { + pithosStateFilePath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:DATA_MODEL_FILE] retain]; + } + return [pithosStateFilePath copy]; +} + +- (NSString *)tempDownloadsDirPath { + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (!tempDownloadsDirPath || ![fileManager fileExistsAtPath:tempDownloadsDirPath]) { + // Get the path from user defaults + tempDownloadsDirPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosTempDownloadsDirPath"]; + if (tempDownloadsDirPath) { + // Check if the path exists + BOOL isDirectory; + BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory]; + NSError *error = nil; + if (fileExists && !isDirectory) + [fileManager removeItemAtPath:tempDownloadsDirPath error:&error]; + if (!error & !fileExists) + [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) + tempDownloadsDirPath = nil; + } + if (!tempDownloadsDirPath) { + NSString *tempDirTemplate = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Temp Downloads XXXXXX"]; + const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation]; + char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1); + strcpy(tempDirNameCString, tempDirTemplateCString); + tempDirNameCString = mkdtemp(tempDirNameCString); + if (tempDirNameCString != NULL) + tempDownloadsDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)]; + free(tempDirNameCString); + } + if (!tempDownloadsDirPath) + [[NSUserDefaults standardUserDefaults] setObject:tempDownloadsDirPath forKey:@"PithosTempDownloadsDirPath"]; + [tempDownloadsDirPath retain]; + } + return [tempDownloadsDirPath copy]; +} + +#pragma mark - +#pragma mark Sync + +- (void)saveLocalState { + NSMutableData *data = [NSMutableData data]; + NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease]; + [archiver encodeObject:storedLocalObjectStates forKey:ARCHIVE_KEY]; + [archiver finishEncoding]; + [data writeToFile:self.pithosStateFilePath atomically:YES]; +} + +- (void)sync { + @synchronized(self) { + if (syncOperationCount) { + // If at least one operation is running return + newSyncRequested = YES; + return; + } else { + // The first operation is the server listing + syncOperationCount = 1; + newSyncRequested = NO; + } + } + + NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDirectory; + NSError *error = nil; + if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory] && + (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || + error)) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" + message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath] + error:error]; + @synchronized(self) { + syncOperationCount = 0; + } + return; + } else if (!isDirectory) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" + message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", containerDirectoryPath] + error:nil]; + @synchronized(self) { + syncOperationCount = 0; + } + return; + } + + ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName + limit:0 + marker:nil + prefix:nil + delimiter:nil + path:nil + meta:nil + shared:NO + until:nil + ifModifiedSince:lastModified]; + containerRequest.delegate = self; + containerRequest.didFinishSelector = @selector(listRequestFinished:); + containerRequest.didFailSelector = @selector(listRequestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther + message:@"Sync: Getting server listing"]; + containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + activity, @"activity", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; +// [[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh]]; +} + +- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState { + if (currentState.isDirectory) + // Currently a directory, check previous state + return (!storedState.isDirectory); + if (storedState.isDirectory) + // Previously a directory, currently a file or doesn't exist, state has changed + return YES; + if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "] && + (![storedState.md5 isEqualToString:@" "] || ![storedState.hashMapHash isEqualToString:@" "])) + // Currently doesn't exist, previously a file, state has changed + return YES; + if (![storedState.md5 isEqualToString:currentState.md5] && ![storedState.hashMapHash isEqualToString:currentState.hashMapHash]) + // Neither hash remained the same, different files, state has changed + return YES; + else + // At least one hash remained the same (the other is either the same or emmpty), state hasn't changed + return NO; +} + +- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState + remoteObjectHash:(NSString *)remoteObjectHash + remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory { + if (remoteObjectIsDirectory) + // Remotely a directory, check previous state + return (!storedState.isDirectory); + if (storedState.isDirectory) + // Previously a directory, remotely a file or doesn't exist, state has changed + return YES; + if ([remoteObjectHash length] == 32) + // Remotely a file, check previous state + return ![remoteObjectHash isEqualToString:storedState.md5]; + else if ([remoteObjectHash length] == 64) + // Remotely a file, check previous state + return ![remoteObjectHash isEqualToString:storedState.hashMapHash]; + else if ([remoteObjectHash isEqualToString:@" "]) { + // Remotely doesn't exist + if ([storedState.md5 isEqualToString:@" "] && [storedState.hashMapHash isEqualToString:@" "]) + // Previously didn't exist, state hasn't changed + return NO; + else + // Previously did exist, state has changed + return YES; + } + // Only if the server doesn't respond properly this will be reached, leave as is for now + return NO; +} + +- (void)updateLocalStateWithObject:(ASIPithosObject *)object + localFilePath:(NSString *)filePath { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + BOOL isDirectory; + BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; + PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + if ([object.hash isEqualToString:@" "]) { + // Delete local object + // XXX move to local trash instead + NSLog(@"Sync::delete local object: %@", filePath); + BOOL deleteFailed = NO; + if (fileExists) { + error = nil; + if (![fileManager removeItemAtPath:filePath error:&error] || error) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" + message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] + error:error]; + deleteFailed = YES; + } + } + if (!deleteFailed) { + [activityFacility startAndEndActivityWithType:PithosActivityOther + message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]]; + [storedLocalObjectStates removeObjectForKey:object.name]; + [self saveLocalState]; + } + } else if ([object.contentType isEqualToString:@"application/directory"]) { + // Create local directory object + NSLog(@"Sync::create local directory object: %@", filePath); + BOOL directoryCreated = NO; + if (fileExists && !isDirectory) { + NSLog(@"Sync::delete local file object: %@", filePath); + error = nil; + if (![fileManager removeItemAtPath:filePath error:&error] || error) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" + message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] + error:error]; + } + } + if (![fileManager fileExistsAtPath:filePath]) { + NSLog(@"Sync::local directory object doesn't exist: %@", filePath); + error = nil; + if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" + message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] + error:error]; + } else { + directoryCreated = YES; + storedState.isDirectory = YES; + [self saveLocalState]; + } + } else if (isDirectory) { + NSLog(@"Sync::local directory object exists: %@", filePath); + directoryCreated = YES; + } + if (directoryCreated) + [activityFacility startAndEndActivityWithType:PithosActivityOther + message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]]; + } else if (object.bytes == 0) { + // Create local object with zero length + NSLog(@"Sync::create local zero length object: %@", filePath); + BOOL fileCreated = NO; + if (fileExists && (isDirectory || [PithosUtilities bytesOfFile:filePath])) { + NSLog(@"Sync::delete local object: %@", filePath); + error = nil; + if (![fileManager removeItemAtPath:filePath error:&error] || error) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" + message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] + error:error]; + } + } + if (![fileManager fileExistsAtPath:filePath]) { + NSLog(@"Sync::local zero length object doesn't exist: %@", filePath); + error = nil; + if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" + message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] + error:error]; + } else { + fileCreated = YES; + if ([object.hash length] == 32) { + storedState.md5 = object.hash; + storedState.hashMapHash = @" "; + } else if([object.hash length] == 64) { + storedState.md5 = @" "; + storedState.hashMapHash = object.hash; + } else { + storedState.md5 = @" "; + storedState.hashMapHash = @" "; + } + storedState.tmpDownloadFile = nil; + [self saveLocalState]; + } + } else if (!isDirectory && ![PithosUtilities bytesOfFile:filePath]) { + NSLog(@"Sync::local zero length object exists: %@", filePath); + fileCreated = YES; + } + if (fileCreated) + [activityFacility startAndEndActivityWithType:PithosActivityOther + message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]]; + } else if (storedState.tmpDownloadFile == nil) { + // Create new local object + @synchronized(self) { + syncOperationCount++; + } + __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName + object:object + blockIndex:0 + blockSize:blockSize]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:); + objectRequest.didFailSelector = @selector(requestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload + message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] + totalBytes:object.bytes + currentBytes:0]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + object, @"pithosObject", + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(blockSize + 0.0)))], @"missingBlocks", + [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", + filePath, @"filePath", + activity, @"activity", + [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", + [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", + [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; + [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", + [[objectRequest.userInfo valueForKey:@"pithosObject"] name], + (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; +// [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } else { + // Resume local object download + @synchronized(self) { + syncOperationCount++; + } + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithContainerName:containerName + objectName:object.name]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(downloadObjectHashMapFinished:); + // The fail method for block download does exactly what we want + objectRequest.didFailSelector = @selector(requestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload + message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] + totalBytes:object.bytes + currentBytes:0]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + object, @"pithosObject", + filePath, @"filePath", + activity, @"activity", + [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", + [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", + [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; +// [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } +} + +-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState + object:(ASIPithosObject *)object + localFilePath:(NSString *)filePath { + if (currentState.isDirectory) { + // Create remote directory object + @synchronized(self) { + syncOperationCount++; + } + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName + objectName:object.name + eTag:nil + contentType:@"application/directory" + contentEncoding:nil + contentDisposition:nil + manifest:nil + sharing:nil + isPublic:ASIPithosObjectRequestPublicIgnore + metadata:nil + data:[NSData data]]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:); + objectRequest.didFailSelector = @selector(requestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory + message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name]]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + object, @"pithosObject", + activity, @"activity", + [NSString stringWithFormat:@"Sync: Creating directory '%@' (stopped)", object.name], @"stoppedActivityMessage", + [NSString stringWithFormat:@"Sync: Creating directory '%@' (failed)", object.name], @"failedActivityMessage", + [NSString stringWithFormat:@"Sync: Creating directory '%@' (finished)", object.name], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; +// [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } else if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "]) { + // Delete remote object + @synchronized(self) { + syncOperationCount++; + } + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithContainerName:containerName + objectName:object.name]; + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(deleteObjectFinished:); + objectRequest.didFailSelector = @selector(requestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete + message:[NSString stringWithFormat:@"Sync: Deleting '%@'", object.name]]; + objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: + object, @"pithosObject", + activity, @"activity", + [NSString stringWithFormat:@"Sync: Deleting '%@' (stopped)", object.name], @"stoppedActivityMessage", + [NSString stringWithFormat:@"Sync: Deleting '%@' (failed)", object.name], @"failedActivityMessage", + [NSString stringWithFormat:@"Sync: Deleting '%@' (finished)", object.name], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]; +// [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } else { + // Upload file to remote object + NSError *error = nil; + object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error]; + if (object.contentType == nil) + object.contentType = @"application/octet-stream"; + if (error) + NSLog(@"contentType detection error: %@", error); + NSArray *hashes = nil; + ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName + objectName:object.name + contentType:object.contentType + blockSize:blockSize + blockHash:blockHash + forFile:filePath + checkIfExists:NO + hashes:&hashes + sharingAccount:nil]; + if (objectRequest) { + @synchronized(self) { + syncOperationCount++; + } + objectRequest.delegate = self; + objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:); + objectRequest.didFailSelector = @selector(requestFailed:); + PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload + message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name] + totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue] + currentBytes:0]; + [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + object, @"pithosObject", + filePath, @"filePath", + hashes, @"hashes", + [NSNumber numberWithUnsignedInteger:10], @"iteration", + activity, @"activity", + [NSString stringWithFormat:@"Sync: Uploading '%@' (stopped)", object.name], @"stoppedActivityMessage", + [NSString stringWithFormat:@"Sync: Uploading '%@' (failed)", object.name], @"failedActivityMessage", + [NSString stringWithFormat:@"Sync: Uploading '%@' (100%%)", object.name], @"finishedActivityMessage", + [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", + [NSNumber numberWithUnsignedInteger:10], @"retries", + nil]]; +// [[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + +} + +#pragma mark - +#pragma mark ASIHTTPRequestDelegate + +- (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest { + NSLog(@"Sync::list request finished: %@", containerRequest.url); + if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) { + if (containerRequest.responseStatusCode == 200) { + NSArray *someObjects = [containerRequest objects]; + if (objects == nil) { + objects = [[NSMutableArray alloc] initWithArray:someObjects]; + } else { + [objects addObjectsFromArray:someObjects]; + } + if ([someObjects count] < 10000) { + self.blockHash = [containerRequest blockHash]; + self.blockSize = [containerRequest blockSize]; + self.lastModified = [containerRequest lastModified]; + self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]]; + for (ASIPithosObject *object in objects) { + [remoteObjects setObject:object forKey:object.name]; + } + [objects release]; + objects = nil; + } else { + // Do an additional request to fetch more objects + ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName + limit:0 + marker:[[someObjects lastObject] name] + prefix:nil + delimiter:nil + path:nil + meta:nil + shared:NO + until:nil + ifModifiedSince:lastModified]; + newContainerRequest.delegate = self; + newContainerRequest.didFinishSelector = @selector(listRequestFinished:); + newContainerRequest.didFailSelector = @selector(listRequestFailed:); + newContainerRequest.userInfo = newContainerRequest.userInfo; + [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; +// [[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityHigh]]; + return; + } + } + [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] + withMessage:@"Sync: Getting server listing (finished)"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName]; + NSError *error = nil; + NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error]; + if (error) { + [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"]; + @synchronized(self) { + // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccesfully + syncOperationCount = 0; + if (newSyncRequested) + [self sync]; + } + return; + } + for (NSString *objectName in subPaths) { + if (![storedLocalObjectStates objectForKey:objectName]) { + [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName]; + } + } + [self saveLocalState]; + + for (NSString *objectName in remoteObjects) { + if (![storedLocalObjectStates objectForKey:objectName]) + [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName]; + } + + for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; + if ([objectName hasSuffix:@"/"]) + filePath = [filePath stringByAppendingString:@":"]; + ASIPithosObject *object = [ASIPithosObject object]; + object.name = objectName; + NSLog(@"Sync::object name: %@", objectName); + + PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:object.name]; + PithosLocalObjectState *currentLocalObjectState = [PithosLocalObjectState nullObjectState]; + BOOL isDirectory; + if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { + if (isDirectory) { + currentLocalObjectState.isDirectory = YES; + } else { + currentLocalObjectState.md5 = (NSString *)FileMD5HashCreateWithPath((CFStringRef)filePath, + FileHashDefaultChunkSizeForReadingData); + currentLocalObjectState.hashMapHash = [HashMapHash calculateHashMapHash:[HashMapHash calculateObjectHashMap:filePath + withBlockHash:blockHash + andBlockSize:blockSize]]; + } + } + if (currentLocalObjectState.isDirectory) + object.contentType = @"application/directory"; + + NSString *remoteObjectHash = @" "; + BOOL remoteObjectIsDirectory = NO; + ASIPithosObject *remoteObject = [remoteObjects objectForKey:objectName]; + if (remoteObject) { + remoteObjectHash = remoteObject.hash; + if ([remoteObject.contentType isEqualToString:@"application/directory"]) { + remoteObjectIsDirectory = YES; + object.contentType = @"application/directory"; + } + } + NSLog(@"Sync::remote object is directory: %d", remoteObjectIsDirectory); + + BOOL localStateHasChanged = [self localStateHasChanged:storedLocalObjectState currentState:currentLocalObjectState]; + BOOL serverStateHasChanged = [self serverStateHasChanged:storedLocalObjectState + remoteObjectHash:remoteObjectHash + remoteObjectIsDirectory:remoteObjectIsDirectory]; + NSLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged); + // XXX shouldn't we first do all the deletes? in order not to face a dir that becomes a file and vice versa + if (!localStateHasChanged) { + // Local state hasn't changed + if (serverStateHasChanged) { + // Server state has changed + // Update local state to match that of the server + object.bytes = [remoteObject bytes]; + object.version = [remoteObject version]; + object.contentType = [remoteObject contentType]; + object.hash = remoteObjectHash; + [self updateLocalStateWithObject:object localFilePath:filePath]; + } + } else { + // Local state has changed + if (!serverStateHasChanged) { + // Server state hasn't changed + [self updateServerStateWithCurrentState:currentLocalObjectState + object:object + localFilePath:filePath]; + } else { + // Server state has also changed + if (remoteObjectIsDirectory && currentLocalObjectState.isDirectory) { + // Both did the same change (directory) + storedLocalObjectState.isDirectory = YES; + [self saveLocalState]; + } else if ([remoteObjectHash isEqualToString:currentLocalObjectState.md5] || + [remoteObjectHash isEqualToString:currentLocalObjectState.hashMapHash]) { + // Both did the same change (object edit or delete) + if ([remoteObjectHash length] == 32) + storedLocalObjectState.md5 = remoteObjectHash; + else if ([remoteObjectHash length] == 64) + storedLocalObjectState.hashMapHash = remoteObjectHash; + else if ([remoteObjectHash isEqualToString:@" "]) + [storedLocalObjectStates removeObjectForKey:object.name]; + [self saveLocalState]; + } else { + // Conflict, we ask the user which change to keep + NSString *informativeText; + NSString *firstButtonText; + NSString *secondButtonText; + + if ([remoteObjectHash isEqualToString:@" "]) { + // Remote object has been deleted + informativeText = [NSString stringWithFormat:@"'%@' has been modified locally, while it has been deleted from server.", object.name ]; + firstButtonText = @"Delete local file"; + secondButtonText = @"Upload file to server"; + } else if ([currentLocalObjectState.md5 isEqualToString:@" "] && [currentLocalObjectState.hashMapHash isEqualToString:@" "]) { + informativeText = [NSString stringWithFormat:@"'%@' has been modified on the server, while it has been deleted locally.", object.name]; + firstButtonText = @"Download file from server"; + secondButtonText = @"Delete file on server"; + } else { + informativeText = [NSString stringWithFormat:@"'%@' has been modifed both locally and on the server.", object.name]; + firstButtonText = @"Keep server version"; + secondButtonText = @"Keep local version"; + } + NSAlert *alert = [[[NSAlert alloc] init] autorelease]; + [alert setMessageText:@"Conflict"]; + [alert setInformativeText:informativeText]; + [alert addButtonWithTitle:firstButtonText]; + [alert addButtonWithTitle:secondButtonText]; + [alert addButtonWithTitle:@"Do nothing"]; + NSInteger choice = [alert runModal]; + if (choice == NSAlertFirstButtonReturn) { + object.bytes = [remoteObject bytes]; + object.version = [remoteObject version]; + object.contentType = [remoteObject contentType]; + object.hash = remoteObjectHash; + [self updateLocalStateWithObject:object localFilePath:filePath]; + } if (choice == NSAlertSecondButtonReturn) { + [self updateServerStateWithCurrentState:currentLocalObjectState + object:object + localFilePath:filePath]; + } + } + } + } + } + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + } else { + NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue]; + if (retries > 0) { + ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest]; + [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; +// [[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityHigh]]; + } else { + [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] + withMessage:@"Sync: Getting server listing (failed)"]; + @synchronized(self) { + // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully + syncOperationCount = 0; + if (newSyncRequested) + [self sync]; + } + } + } +} + +- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest { + if ([containerRequest isCancelled]) { + [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] + withMessage:@"Sync: Getting server listing (stopped)"]; + [objects release]; + objects = nil; + @synchronized(self) { + syncOperationCount = 0; + } + return; + } + // If the server listing fails, the sync should start over, so just retrying is enough + NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue]; + if (retries > 0) { + ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest]; + [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; +// [[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:NSOperationQueuePriorityVeryHigh]]; + } else { + [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] + withMessage:@"Sync: Getting server listing (failed)"]; + [objects release]; + objects = nil; + @synchronized(self) { + // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully + syncOperationCount = 0; + if (newSyncRequested) + [self sync]; + } + } +} + +- (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest { + NSLog(@"Sync::download object block finished: %@", objectRequest.url); + if (objectRequest.responseStatusCode == 206) { + ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; + + NSString *downloadsDirPath = self.tempDownloadsDirPath; + if (!downloadsDirPath) { + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } + + PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + if ((storedState.tmpDownloadFile == nil) || ![fileManager fileExistsAtPath:storedState.tmpDownloadFile]) { + NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"]; + const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation]; + char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1); + strcpy(tempFileNameCString, tempFileTemplateCString); + int fileDescriptor = mkstemp(tempFileNameCString); + NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; + free(tempFileNameCString); + if (fileDescriptor == -1) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" + message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", storedState.tmpDownloadFile] + error:nil]; + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } + close(fileDescriptor); + storedState.tmpDownloadFile = tempFilePath; + [self saveLocalState]; + } + + + NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; + NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile]; + [tempFileHandle seekToFileOffset:missingBlockIndex*blockSize]; + [tempFileHandle writeData:[objectRequest responseData]]; + [tempFileHandle closeFile]; + + NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"]; + missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; + if (missingBlockIndex == NSNotFound) { + NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"]; + NSString *dirPath = [filePath stringByDeletingLastPathComponent]; + if ([fileManager fileExistsAtPath:filePath]) { + error = nil; + // XXX don't delete but move to local trash instead + [fileManager removeItemAtPath:filePath error:&error]; + if (error != nil) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" + message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] + error:error]; + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } + } else if (![fileManager fileExistsAtPath:dirPath]) { + // File doesn't exist but also the containing directory doesn't exist + // In most cases this should have been resolved as an update of the corresponding local object, + // but it never hurts to check + error = nil; + [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; + if (error != nil) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" + message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] + error:error]; + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } + } + // Move file from tmp download + error = nil; + [fileManager moveItemAtPath:storedState.tmpDownloadFile toPath:filePath error:&error]; + if (error != nil) { + [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" + message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpDownloadFile, filePath] + error:error]; + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] + totalBytes:activity.totalBytes + currentBytes:activity.totalBytes]; + + if ([object.hash length] == 32) { + storedState.md5 = object.hash; + storedState.hashMapHash = @" "; + } else if([object.hash length] == 64) { + storedState.md5 = @" "; + storedState.hashMapHash = object.hash; + } else { + storedState.md5 = @" "; + storedState.hashMapHash = @" "; + } + + storedState.tmpDownloadFile = nil; + [self saveLocalState]; + + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + return; + } else { + if (newSyncRequested) { + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (!syncOperationCount) + [self sync]; + } + return; + } else { + __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName + object:object + blockIndex:missingBlockIndex + blockSize:blockSize]; + newObjectRequest.delegate = self; + newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:); + newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:); + newObjectRequest.userInfo = objectRequest.userInfo; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", + object.name, + (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; +// [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityHigh]]; + } + } + } else if (objectRequest.responseStatusCode == 412) { + // The object has changed on the server + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + } else { + [self requestFailed:objectRequest]; + } +} + +- (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest { + NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url); + if (objectRequest.responseStatusCode == 200) { + if (newSyncRequested) { + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (!syncOperationCount) + [self sync]; + } + return; + } else { + ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; + PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + if ([PithosUtilities bytesOfFile:storedState.tmpDownloadFile] > object.bytes) + [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile] truncateFileAtOffset:object.bytes]; + PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; + NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpDownloadFile + blockSize:blockSize + blockHash:blockHash + withHashes:[objectRequest hashes]]; + NSUInteger missingBlockIndex = [missingBlocks firstIndex]; + [activityFacility endActivity:activity + withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", + object.name, + (100*(activity.totalBytes - [missingBlocks count]*blockSize + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.totalBytes - [missingBlocks count]*blockSize)]; + + __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName + object:object + blockIndex:missingBlockIndex + blockSize:blockSize]; + newObjectRequest.delegate = self; + newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:); + newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:); + newObjectRequest.userInfo = objectRequest.userInfo; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", + object.name, + (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; +// [[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityHigh] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:NSOperationQueuePriorityHigh]]; + } + } else { + [self requestFailed:objectRequest]; + } +} + +- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest { + NSLog(@"Sync::upload directory object finished: %@", objectRequest.url); + if (objectRequest.responseStatusCode == 201) { + PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; + storedState.isDirectory = YES; + [self saveLocalState]; + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + } else { + [self requestFailed:objectRequest]; + } +} + +- (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest { + NSLog(@"Sync::delete object finished: %@", objectRequest.url); + if (objectRequest.responseStatusCode == 204) { + [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; + [self saveLocalState]; + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + } else { + [self requestFailed:objectRequest]; + } +} + +- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest { + NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url); + ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; + PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; + NSUInteger totalBytes = activity.totalBytes; + NSUInteger currentBytes = activity.currentBytes; + if (objectRequest.responseStatusCode == 201) { + NSLog(@"Sync::object created: %@", objectRequest.url); + NSString *eTag = [objectRequest eTag]; + if ([eTag length] == 32) + storedState.md5 = eTag; + else if([eTag length] == 64) + storedState.hashMapHash = eTag; + [self saveLocalState]; + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] + totalBytes:totalBytes + currentBytes:totalBytes]; + @synchronized(self) { + syncOperationCount--; + if (newSyncRequested && !syncOperationCount) + [self sync]; + } + } else if (objectRequest.responseStatusCode == 409) { + if (newSyncRequested) { + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (!syncOperationCount) + [self sync]; + } + return; + } else { + NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue]; + if (iteration == 0) { + NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url); + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + } + return; + } + NSLog(@"Sync::object is missing hashes: %@", objectRequest.url); + NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"] + withMissingHashesResponse:[objectRequest responseString]]; + if (totalBytes >= [missingBlocks count]*blockSize) + currentBytes = totalBytes - [missingBlocks count]*blockSize; + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] + totalBytes:totalBytes + currentBytes:currentBytes]; + NSUInteger missingBlockIndex = [missingBlocks firstIndex]; + __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName + blockSize:blockSize + forFile:[objectRequest.userInfo objectForKey:@"filePath"] + missingBlockIndex:missingBlockIndex + sharingAccount:nil]; + newContainerRequest.delegate = self; + newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:); + newContainerRequest.didFailSelector = @selector(requestFailed:); + newContainerRequest.userInfo = objectRequest.userInfo; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; +// [[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } + } else { + [self requestFailed:objectRequest]; + } +} + +- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest { + NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url); + if (containerRequest.responseStatusCode == 202) { + ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"]; + PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"]; + NSUInteger totalBytes = activity.totalBytes; + NSUInteger currentBytes = activity.currentBytes + blockSize; + if (currentBytes > totalBytes) + currentBytes = totalBytes; + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] + totalBytes:totalBytes + currentBytes:currentBytes]; + NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"]; + NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; + missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; + if (missingBlockIndex == NSNotFound) { + NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"]; + ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName + objectName:object.name + contentType:object.contentType + blockSize:blockSize + blockHash:blockHash + forFile:[containerRequest.userInfo objectForKey:@"filePath"] + checkIfExists:NO + hashes:&hashes + sharingAccount:nil]; + newObjectRequest.delegate = self; + newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:); + newObjectRequest.didFailSelector = @selector(requestFailed:); + newObjectRequest.userInfo = containerRequest.userInfo; + [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"]; + [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"]; +// [[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } else { + if (newSyncRequested) { + [activityFacility endActivity:activity + withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (!syncOperationCount) + [self sync]; + } + return; + } else { + __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName + blockSize:blockSize + forFile:[containerRequest.userInfo objectForKey:@"filePath"] + missingBlockIndex:missingBlockIndex + sharingAccount:nil]; + newContainerRequest.delegate = self; + newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:); + newContainerRequest.didFailSelector = @selector(requestFailed:); + newContainerRequest.userInfo = containerRequest.userInfo; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; + [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; + [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ + [activityFacility updateActivity:activity + withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + totalBytes:activity.totalBytes + currentBytes:(activity.currentBytes + size)]; + }]; +// [[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } + } + } else { + [self requestFailed:containerRequest]; + } +} + +- (void)requestFailed:(ASIPithosRequest *)request { + if ([request isCancelled]) { + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + } + return; + } + if (newSyncRequested) { + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + if (!syncOperationCount) + [self sync]; + } + return; + } + NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue]; + if (retries > 0) { + ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request]; + [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; +// [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous]; + [queue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]]; + } else { + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]]; + @synchronized(self) { + syncOperationCount--; + } + } +} + + +@end diff --git a/pithos-macos/PithosUtilities.h b/pithos-macos/PithosUtilities.h index ff7fcc0..483afb3 100644 --- a/pithos-macos/PithosUtilities.h +++ b/pithos-macos/PithosUtilities.h @@ -38,6 +38,7 @@ @class ASIPithosRequest; @class ASIPithosContainerRequest; @class ASIPithosObjectRequest; +@class ASIPithosObject; @interface PithosUtilities : NSObject @@ -52,6 +53,16 @@ checkIfExists:(BOOL)ifExists sharingAccount:(NSString *)sharingAccount; + ++ (ASIPithosObjectRequest *)objectBlockDataRequestWithContainerName:(NSString *)containerName + object:(ASIPithosObject *)object + blockIndex:(NSUInteger)blockIndex + blockSize:(NSUInteger)blockSize; ++ (NSIndexSet *)missingBlocksForFile:(NSString *)filePath + blockSize:(NSUInteger)blockSize + blockHash:(NSString *)blockHash + withHashes:(NSArray *)hashes; + + (ASIPithosObjectRequest *)writeObjectDataRequestWithContainerName:(NSString *)containerName objectName:(NSString *)objectName contentType:(NSString *)contentType @@ -128,6 +139,7 @@ + (NSInteger)httpRequestErrorAlertWithRequest:(ASIPithosRequest *)request; + (NSInteger)unexpectedResponseStatusAlertWithRequest:(ASIPithosRequest *)request; ++ (NSInteger)fileActionFailedAlertWithTitle:(NSString *)title message:(NSString *)message error:(NSError *)error; + (ASIPithosRequest *)prepareRequest:(ASIPithosRequest *)request priority:(NSOperationQueuePriority)priority; + (ASIPithosRequest *)prepareRequest:(ASIPithosRequest *)request; diff --git a/pithos-macos/PithosUtilities.m b/pithos-macos/PithosUtilities.m index 62db1e5..8fecb7b 100644 --- a/pithos-macos/PithosUtilities.m +++ b/pithos-macos/PithosUtilities.m @@ -56,10 +56,10 @@ fileName = [fileName stringByAppendingString:@"/"]; fileName = [fileName stringByReplacingOccurrencesOfString:@"/" withString:@":"]; - NSFileManager *defaultManager = [NSFileManager defaultManager]; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *destinationPath = [directoryPath stringByAppendingPathComponent:fileName]; - if (ifExists && [defaultManager fileExistsAtPath:destinationPath]) { + if (ifExists && [fileManager fileExistsAtPath:destinationPath]) { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText:@"File Exists"]; [alert setInformativeText:[NSString stringWithFormat:@"A file or directory named '%@' already exists, do you want to replace it?", fileName]]; @@ -71,12 +71,12 @@ } BOOL directoryIsDirectory; - BOOL directoryExists = [defaultManager fileExistsAtPath:directoryPath isDirectory:&directoryIsDirectory]; + BOOL directoryExists = [fileManager fileExistsAtPath:directoryPath isDirectory:&directoryIsDirectory]; NSError *error = nil; if (!directoryExists) { - [defaultManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error]; + [fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error]; } else if (!directoryIsDirectory) { - [defaultManager removeItemAtPath:directoryPath error:&error]; + [fileManager removeItemAtPath:directoryPath error:&error]; } if (error) { NSLog(@"Cannot remove existing file '%@': %@", fileName, error); @@ -124,12 +124,12 @@ if (objects == nil) return nil; - NSFileManager *defaultManager = [NSFileManager defaultManager]; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableArray *objectRequests = [NSMutableArray arrayWithCapacity:[objects count]]; NSUInteger subdirPrefixLength = [objectName length]; NSError *error = nil; - [defaultManager createDirectoryAtPath:[directoryPath stringByAppendingPathComponent:subdirName] withIntermediateDirectories:YES attributes:nil error:&error]; + [fileManager createDirectoryAtPath:[directoryPath stringByAppendingPathComponent:subdirName] withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { NSLog(@"Cannot create directory at '%@': %@", directoryPath, error); NSAlert *alert = [[[NSAlert alloc] init] autorelease]; @@ -146,10 +146,10 @@ subdirDirectoryPath = [subdirDirectoryPath stringByAppendingPathComponent:[object.name substringFromIndex:subdirPrefixLength]]; BOOL directoryIsDirectory; - BOOL directoryExists = [defaultManager fileExistsAtPath:subdirDirectoryPath isDirectory:&directoryIsDirectory]; + BOOL directoryExists = [fileManager fileExistsAtPath:subdirDirectoryPath isDirectory:&directoryIsDirectory]; NSError *error = nil; if (!directoryExists) { - [defaultManager createDirectoryAtPath:subdirDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]; + [fileManager createDirectoryAtPath:subdirDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { NSLog(@"Cannot create directory at '%@': %@", subdirDirectoryPath, error); NSAlert *alert = [[[NSAlert alloc] init] autorelease]; @@ -160,7 +160,7 @@ [alert runModal]; } } else if (!directoryIsDirectory) { - [defaultManager removeItemAtPath:subdirDirectoryPath error:&error]; + [fileManager removeItemAtPath:subdirDirectoryPath error:&error]; if (error) { NSLog(@"Cannot remove existing file at '%@': %@", subdirDirectoryPath, error); NSAlert *alert = [[[NSAlert alloc] init] autorelease]; @@ -193,6 +193,35 @@ } #pragma mark - +#pragma mark Download Block + ++ (ASIPithosObjectRequest *)objectBlockDataRequestWithContainerName:(NSString *)containerName + object:(ASIPithosObject *)object + blockIndex:(NSUInteger)blockIndex + blockSize:(NSUInteger)blockSize { + NSUInteger rangeStart = blockIndex * blockSize; + NSUInteger rangeEnd = (rangeStart + blockSize <= object.bytes) ? (rangeStart + blockSize - 1) : (object.bytes - 1); + ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectDataRequestWithContainerName:containerName + objectName:object.name + version:nil + range:[NSString stringWithFormat:@"bytes=%lu-%lu", rangeStart, rangeEnd] + ifMatch:object.hash]; + return objectRequest; +} + ++ (NSIndexSet *)missingBlocksForFile:(NSString *)filePath + blockSize:(NSUInteger)blockSize + blockHash:(NSString *)blockHash + withHashes:(NSArray *)hashes { + NSArray *fileHashes = [HashMapHash objectHashMapStrings:filePath withBlockHash:blockHash andBlockSize:blockSize]; + return [hashes indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop) { + if ((idx >= [fileHashes count]) || ![(NSString *)obj isEqualToString:[fileHashes objectAtIndex:idx]]) + return YES; + return NO; + }]; +} + +#pragma mark - #pragma mark Upload + (ASIPithosObjectRequest *)writeObjectDataRequestWithContainerName:(NSString *)containerName @@ -259,7 +288,7 @@ sharingAccount:(NSString *)sharingAccount { NSIndexSet *missingBlocks = [self missingBlocksForHashes:hashes withMissingHashesResponse:missingHashesResponse]; - NSFileManager *defaultManager = [NSFileManager defaultManager]; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath]; // http://cocoawithlove.com/2009/07/temporary-files-and-folders-in-cocoa.html @@ -271,7 +300,7 @@ char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1); strcpy(tempFileNameCString, tempFileTemplateCString); int fileDescriptor = mkstemp(tempFileNameCString); - NSString *tempFilePath = [defaultManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; + NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; if (fileDescriptor == -1) { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText:@"Create Temporary File Error"]; @@ -334,9 +363,9 @@ if (ifExists && ![self proceedIfObjectExistsAtContainerName:containerName objectName:objectName sharingAccount:sharingAccount]) return nil; - NSFileManager *defaultManager = [NSFileManager defaultManager]; + NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; - NSArray *subPaths = [defaultManager subpathsOfDirectoryAtPath:directoryPath error:&error]; + NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:directoryPath error:&error]; if (error) { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText:@"Directory Read Error"]; @@ -380,7 +409,7 @@ NSString *fileName; for (NSString *objectNameSuffix in subPaths) { filePath = [directoryPath stringByAppendingPathComponent:objectNameSuffix]; - if ([defaultManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { + if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) { if (!isDirectory) { hashes = [HashMapHash objectHashMapStrings:filePath withBlockHash:blockHash andBlockSize:blockSize]; if (hashes) { @@ -795,8 +824,8 @@ // Size of the file in bytes + (NSUInteger)bytesOfFile:(NSString *)filePath { - NSFileManager *defaultManager = [NSFileManager defaultManager]; - NSDictionary *attributes = [defaultManager attributesOfItemAtPath:filePath error:nil]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:filePath error:nil]; return [[attributes objectForKey:NSFileSize] intValue]; } @@ -1057,6 +1086,17 @@ return [alert runModal]; } ++ (NSInteger)fileActionFailedAlertWithTitle:(NSString *)title message:(NSString *)message error:(NSError *)error { + NSAlert *alert = [[[NSAlert alloc] init] autorelease]; + [alert setMessageText:title]; + if (error) + [alert setInformativeText:[NSString stringWithFormat:@"%@: %@", message, error]]; + else + [alert setInformativeText:message]; + [alert addButtonWithTitle:@"OK"]; + return [alert runModal]; +} + #pragma mark - #pragma mark Request Helper Methods diff --git a/pithos-macos/en.lproj/MainMenu.xib b/pithos-macos/en.lproj/MainMenu.xib index 98f82bd..fdd2fc2 100644 --- a/pithos-macos/en.lproj/MainMenu.xib +++ b/pithos-macos/en.lproj/MainMenu.xib @@ -2172,6 +2172,14 @@ 559 + + + syncNow: + + + + 561 + @@ -3635,359 +3643,9 @@ - 560 - - - - YES - - NSDocument - - YES - - YES - printDocument: - revertDocumentToSaved: - runPageLayout: - saveDocument: - saveDocumentAs: - saveDocumentTo: - - - YES - id - id - id - id - id - id - - - - YES - - YES - printDocument: - revertDocumentToSaved: - runPageLayout: - saveDocument: - saveDocumentAs: - saveDocumentTo: - - - YES - - printDocument: - id - - - revertDocumentToSaved: - id - - - runPageLayout: - id - - - saveDocument: - id - - - saveDocumentAs: - id - - - saveDocumentTo: - id - - - - - IBProjectSource - ./Classes/NSDocument.h - - - - PithosBrowserController - NSWindowController - - refresh: - id - - - refresh: - - refresh: - id - - - - YES - - YES - activityProgressIndicator - activityTextField - browser - horizontalSplitView - leftBottomView - leftTopView - outlineView - verticalSplitView - - - YES - NSProgressIndicator - NSTextField - NSBrowser - NSSplitView - NSView - NSView - NSOutlineView - NSSplitView - - - - YES - - YES - activityProgressIndicator - activityTextField - browser - horizontalSplitView - leftBottomView - leftTopView - outlineView - verticalSplitView - - - YES - - activityProgressIndicator - NSProgressIndicator - - - activityTextField - NSTextField - - - browser - NSBrowser - - - horizontalSplitView - NSSplitView - - - leftBottomView - NSView - - - leftTopView - NSView - - - outlineView - NSOutlineView - - - verticalSplitView - NSSplitView - - - - - IBProjectSource - ./Classes/PithosBrowserController.h - - - - PithosPreferencesController - NSWindowController - - YES - - YES - authenticationLogin: - toolbarItemSelected: - - - YES - id - id - - - - YES - - YES - authenticationLogin: - toolbarItemSelected: - - - YES - - authenticationLogin: - id - - - toolbarItemSelected: - id - - - - - YES - - YES - authenticationRenewCheckBox - authenticationTokenTextField - authenticationUserTextField - pithosBrowserController - userDefaultsController - - - YES - NSButton - NSTextField - NSTextField - PithosBrowserController - NSUserDefaultsController - - - - YES - - YES - authenticationRenewCheckBox - authenticationTokenTextField - authenticationUserTextField - pithosBrowserController - userDefaultsController - - - YES - - authenticationRenewCheckBox - NSButton - - - authenticationTokenTextField - NSTextField - - - authenticationUserTextField - NSTextField - - - pithosBrowserController - PithosBrowserController - - - userDefaultsController - NSUserDefaultsController - - - - - IBProjectSource - ./Classes/PithosPreferencesController.h - - - - pithos_macosAppDelegate - NSObject - - YES - - YES - aboutPithos: - showPithosBrowser: - showPithosPreferences: - - - YES - id - id - id - - - - YES - - YES - aboutPithos: - showPithosBrowser: - showPithosPreferences: - - - YES - - aboutPithos: - id - - - showPithosBrowser: - id - - - showPithosPreferences: - id - - - - - YES - - YES - pithosBrowserController - pithosPreferencesController - statusMenu - userDefaultsController - - - YES - PithosBrowserController - PithosPreferencesController - NSMenu - NSUserDefaultsController - - - - YES - - YES - pithosBrowserController - pithosPreferencesController - statusMenu - userDefaultsController - - - YES - - pithosBrowserController - PithosBrowserController - - - pithosPreferencesController - PithosPreferencesController - - - statusMenu - NSMenu - - - userDefaultsController - NSUserDefaultsController - - - - - IBProjectSource - ./Classes/pithos_macosAppDelegate.h - - - + 561 + 0 IBCocoaFramework diff --git a/pithos-macos/pithos-macos-Info.plist b/pithos-macos/pithos-macos-Info.plist index 2796712..3fafe29 100644 --- a/pithos-macos/pithos-macos-Info.plist +++ b/pithos-macos/pithos-macos-Info.plist @@ -39,13 +39,19 @@ MainMenu NSPrincipalClass NSApplication - PithosStorageURLPrefix - https://pithos.dev.grnet.gr/v1 - PithosPublicURLPrefix - https://pithos.dev.grnet.gr - PithosLoginURLPrefix - https://pithos.dev.grnet.gr/login PithosAboutURL https://pithos.dev.grnet.gr/docs + PithosLoginURLPrefix + https://pithos.dev.grnet.gr/login + PithosPublicURLPrefix + https://pithos.dev.grnet.gr + PithosStorageURLPrefix + https://pithos.dev.grnet.gr/v1 + PithosSyncDirectoryPath + + PithosSyncContainerName + pithos + PithosSyncTimeInterval + 180 diff --git a/pithos-macos/pithos_macosAppDelegate.h b/pithos-macos/pithos_macosAppDelegate.h index 7d999ef..c684274 100644 --- a/pithos-macos/pithos_macosAppDelegate.h +++ b/pithos-macos/pithos_macosAppDelegate.h @@ -38,10 +38,13 @@ #import @class PithosBrowserController; @class PithosPreferencesController; +@class PithosSyncDaemon; @interface pithos_macosAppDelegate : NSObject { IBOutlet PithosBrowserController *pithosBrowserController; IBOutlet PithosPreferencesController *pithosPreferencesController; + + PithosSyncDaemon *pithosSyncDaemon; IBOutlet NSMenu *statusMenu; NSStatusItem *statusItem; @@ -52,18 +55,27 @@ NSString *publicURLPrefix; NSString *loginURLPrefix; NSString *aboutURL; + + NSString *syncDirectoryPath; + NSString *syncContainerName; + NSTimeInterval syncTimeInterval; } - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent; - (IBAction)showPithosBrowser:(id)sender; - (IBAction)showPithosPreferences:(id)sender; - (IBAction)aboutPithos:(id)sender; +- (IBAction)syncNow:(id)sender; - (void)authenticateWithAuthUser:(NSString *)authUser authToken:(NSString *)authToken; +- (void)startSyncWithDirectoryPath:(NSString *)directoryPath containerName:(NSString *)containerName; @property (nonatomic, readonly) NSString *storageURLPrefix; @property (nonatomic, readonly) NSString *publicURLPrefix; @property (nonatomic, readonly) NSString *loginURLPrefix; @property (nonatomic, readonly) NSString *aboutURL; +@property (nonatomic, readonly) NSString *syncDirectoryPath; +@property (nonatomic, readonly) NSString *syncContainerName; +@property (nonatomic, assign) NSTimeInterval syncTimeInterval; @end diff --git a/pithos-macos/pithos_macosAppDelegate.m b/pithos-macos/pithos_macosAppDelegate.m index 5188f15..235a9f7 100644 --- a/pithos-macos/pithos_macosAppDelegate.m +++ b/pithos-macos/pithos_macosAppDelegate.m @@ -38,15 +38,17 @@ #import "pithos_macosAppDelegate.h" #import "PithosBrowserController.h" #import "PithosPreferencesController.h" +#import "PithosSyncDaemon.h" #import "ASIPithosRequest.h" #import "ASIDownloadCache.h" @implementation pithos_macosAppDelegate @synthesize storageURLPrefix, publicURLPrefix, loginURLPrefix, aboutURL; +@synthesize syncDirectoryPath, syncContainerName, syncTimeInterval; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSURL *testURL; - storageURLPrefix = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosStorageURLPrefix"]; + storageURLPrefix = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosStorageURLPrefix"]; if (!storageURLPrefix) { storageURLPrefix = [NSString stringWithString:@"https://pithos.dev.grnet.gr/v1"]; } else { @@ -56,7 +58,7 @@ } [storageURLPrefix retain]; - publicURLPrefix = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosPublicURLPrefix"]; + publicURLPrefix = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosPublicURLPrefix"]; if (!publicURLPrefix) { publicURLPrefix = [NSString stringWithString:@"https://pithos.dev.grnet.gr"]; } else { @@ -66,7 +68,7 @@ } [publicURLPrefix retain]; - loginURLPrefix = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosLoginURLPrefix"]; + loginURLPrefix = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosLoginURLPrefix"]; if (!loginURLPrefix) { loginURLPrefix = [NSString stringWithString:@"https://pithos.dev.grnet.gr/login"]; } else { @@ -76,7 +78,7 @@ } [loginURLPrefix retain]; - aboutURL = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosAboutURL"]; + aboutURL = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosAboutURL"]; if (!aboutURL) { aboutURL = [NSString stringWithString:@"https://pithos.dev.grnet.gr/docs"]; } else { @@ -86,6 +88,27 @@ } [aboutURL retain]; + syncDirectoryPath = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosSyncDirectoryPath"]; + if (!syncDirectoryPath || ![syncDirectoryPath length]) { + syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Pithos"]; + } else { + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:syncDirectoryPath isDirectory:&isDirectory] && !isDirectory) + syncDirectoryPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Pithos"]; + } + [syncDirectoryPath retain]; + + syncContainerName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosSyncContainerName"]; + if (!syncContainerName || ![syncContainerName length]) { + syncContainerName = [NSString stringWithString:@"pithos"]; + } + [syncContainerName retain]; + + syncTimeInterval = [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"PithosSyncTimeInterval"] doubleValue]; + if (syncTimeInterval <= 0) { + syncTimeInterval = 180.0; + } + [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass @@ -185,22 +208,37 @@ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:self.aboutURL]]; } +- (IBAction)syncNow:(id)sender { + [pithosSyncDaemon sync]; +} + #pragma mark - #pragma Authentication - (void)authenticateWithAuthUser:(NSString *)authUser authToken:(NSString *)authToken { NSLog(@"Authentication - storageURLPrefix:%@, authUser:%@, authToken:%@", storageURLPrefix, authUser, authToken); - if ([authUser length] && [authToken length]) { + if ([authUser length] && [authToken length] && + (![[ASIPithosRequest authUser] isEqualToString:authUser] || ![[ASIPithosRequest authToken] isEqualToString:authToken])) { [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; [[ASIPithosRequest sharedQueue] cancelAllOperations]; + [ASIPithosRequest setAuthURL:storageURLPrefix]; [ASIPithosRequest setStorageURLPrefix:storageURLPrefix]; [ASIPithosRequest setAuthUser:authUser]; [ASIPithosRequest setAuthToken:authToken]; [ASIPithosRequest setPublicURLPrefix:publicURLPrefix]; + [self startSyncWithDirectoryPath:syncDirectoryPath containerName:syncContainerName]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"PithosAuthenticationCredentialsUpdated" object:self]; } } +- (void)startSyncWithDirectoryPath:(NSString *)directoryPath containerName:(NSString *)containerName { + [pithosSyncDaemon release]; + pithosSyncDaemon = [[PithosSyncDaemon alloc] initWithDirectoryPath:syncDirectoryPath + containerName:syncContainerName + timeInterval:syncTimeInterval]; +} + @end -- 1.7.10.4