diff --git a/ArqRestoreCommand.h b/ArqRestoreCommand.h index aee6859..1a33f16 100644 --- a/ArqRestoreCommand.h +++ b/ArqRestoreCommand.h @@ -6,10 +6,11 @@ // // +#import "S3RestorerDelegate.h" @class Target; -@interface ArqRestoreCommand : NSObject { +@interface ArqRestoreCommand : NSObject { Target *target; } diff --git a/ArqRestoreCommand.m b/ArqRestoreCommand.m index 9a3eb76..a166a1e 100644 --- a/ArqRestoreCommand.m +++ b/ArqRestoreCommand.m @@ -14,6 +14,11 @@ #import "UserAndComputer.h" #import "Bucket.h" #import "Repo.h" +#import "S3RestorerParamSet.h" +#import "Tree.h" +#import "Commit.h" +#import "BlobKey.h" +#import "S3Restorer.h" @implementation ArqRestoreCommand @@ -54,17 +59,17 @@ // Valid command, but no additional args. } else if ([cmd isEqualToString:@"listfolders"]) { - if ([args count] < 4) { + if ((argc - targetParamsIndex) < 2) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments for listfolders command"); return NO; } - targetParamsIndex = 4; + targetParamsIndex += 2; } else if ([cmd isEqualToString:@"restore"]) { - if ([args count] < 5) { + if ((argc - targetParamsIndex) < 3) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments"); return NO; } - targetParamsIndex = 5; + targetParamsIndex += 3; } else { SETNSERROR([self errorDomain], ERROR_USAGE, @"unknown command: %@", cmd); return NO; @@ -84,11 +89,11 @@ return NO; } } else if ([cmd isEqualToString:@"listfolders"]) { - if (![self listBucketsForComputerUUID:[args objectAtIndex:2] encryptionPassword:[args objectAtIndex:3] error:error]) { + if (![self listBucketsForComputerUUID:[args objectAtIndex:index+1] encryptionPassword:[args objectAtIndex:index+2] error:error]) { return NO; } } else if ([cmd isEqualToString:@"restore"]) { - if (![self restoreComputerUUID:[args objectAtIndex:2] bucketUUID:[args objectAtIndex:4] encryptionPassword:[args objectAtIndex:3] error:error]) { + if (![self restoreComputerUUID:[args objectAtIndex:index+1] bucketUUID:[args objectAtIndex:index+3] encryptionPassword:[args objectAtIndex:index+2] error:error]) { return NO; } } else { @@ -339,12 +344,89 @@ if (commitBlobKey == nil) { return NO; } + Commit *commit = [repo commitForBlobKey:commitBlobKey dataSize:NULL error:error]; + if (commit == nil) { + return NO; + } + + NSString *destinationPath = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:[[myBucket localPath] lastPathComponent]]; + if ([[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) { + SETNSERROR([self errorDomain], -1, @"%@ already exists", destinationPath); + return NO; + } + printf("target %s\n", [[[myBucket target] endpointDisplayName] UTF8String]); printf("computer %s\n", [[myBucket computerUUID] UTF8String]); printf("\nrestoring folder %s\n\n", [[myBucket localPath] UTF8String]); + AWSRegion *region = [AWSRegion regionWithS3Endpoint:[target endpoint]]; + BOOL isGlacierDestination = [region supportsGlacier]; + if ([myBucket storageType] == StorageTypeGlacier && isGlacierDestination) { +// [[[GlacierRestoreController alloc] initWithAppConfig:appConfig +// doChownsAbove499:NO +// destinationPath:destination +// displayBucket:sourceOutlineController.selectedDisplayBucket +// displayCommit:sourceOutlineController.selectedDisplayCommit +// displayNode:selectedNode +// mainWindow:mainWindow] autorelease]; + } else if ([myBucket storageType] == StorageTypeS3Glacier && isGlacierDestination) { +// [[[S3GlacierRestoreSetupController alloc] initWithLocalComputerUUID:[appConfig computerUUIDForTargetUUID:[target targetUUID]] +// doChownsAbove499:NO +// destinationPath:destination +// displayBucket:sourceOutlineController.selectedDisplayBucket +// displayCommit:sourceOutlineController.selectedDisplayCommit +// displayNode:selectedNode +// mainWindow:mainWindow] autorelease]; + } else { +// [[[S3RestoreController alloc] initWithAppConfig:appConfig +// doChownsAbove499:doChowns +// destinationPath:destination +// displayBucket:sourceOutlineController.selectedDisplayBucket +// displayCommit:sourceOutlineController.selectedDisplayCommit + // displayNode:selectedNode] autorelease]; + S3RestorerParamSet *paramSet = [[[S3RestorerParamSet alloc] initWithBucket:myBucket + encryptionPassword:theEncryptionPassword + commitBlobKey:commitBlobKey + rootItemName:[[myBucket localPath] lastPathComponent] + treeVersion:CURRENT_TREE_VERSION + treeIsCompressed:[[commit treeBlobKey] compressed] + treeBlobKey:[commit treeBlobKey] + nodeName:nil + targetUID:getuid() + targetGID:getgid() + useTargetUIDAndGID:YES + destinationPath:destinationPath + logLevel:global_hslog_level] autorelease]; + [[[S3Restorer alloc] initWithParamSet:paramSet delegate:self] autorelease]; + } + return YES; } + +#pragma mark S3RestorerDelegate +// Methods return YES if cancel is requested. + +- (BOOL)s3RestorerMessageDidChange:(NSString *)message { + printf("status: %s\n", [message UTF8String]); + return NO; +} +- (BOOL)s3RestorerBytesTransferredDidChange:(NSNumber *)theTransferred { + return NO; +} +- (BOOL)s3RestorerTotalBytesToTransferDidChange:(NSNumber *)theTotal { + return NO; +} +- (BOOL)s3RestorerErrorMessage:(NSString *)theErrorMessage didOccurForPath:(NSString *)thePath { + printf("%s error: %s\n", [thePath UTF8String], [theErrorMessage UTF8String]); + return NO; +} +- (BOOL)s3RestorerDidSucceed { + return NO; +} +- (BOOL)s3RestorerDidFail:(NSError *)error { + printf("failed: %s\n", [[error localizedDescription] UTF8String]); + return NO; +} @end diff --git a/arq_restore.xcodeproj/project.pbxproj b/arq_restore.xcodeproj/project.pbxproj index 9c353ba..b6ae033 100644 --- a/arq_restore.xcodeproj/project.pbxproj +++ b/arq_restore.xcodeproj/project.pbxproj @@ -181,6 +181,14 @@ F8F2D96B1986BF5100997A15 /* GunzipInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D96A1986BF5100997A15 /* GunzipInputStream.m */; }; F8F2D96E1986BF6800997A15 /* PackSet.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D96D1986BF6800997A15 /* PackSet.m */; }; F8F2D9701986C09300997A15 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8F2D96F1986C09300997A15 /* IOKit.framework */; }; + F8F2D9741986D02D00997A15 /* S3RestorerParamSet.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D9731986D02D00997A15 /* S3RestorerParamSet.m */; }; + F8F2D9771986D32B00997A15 /* S3Restorer.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D9761986D32B00997A15 /* S3Restorer.m */; }; + F8F2D97C1986D38900997A15 /* OSStatusDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D97B1986D38900997A15 /* OSStatusDescription.m */; }; + F8F2D97F1986D3A100997A15 /* FileAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D97E1986D3A100997A15 /* FileAttributes.m */; }; + F8F2D9821986D3B100997A15 /* FileACL.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D9811986D3B100997A15 /* FileACL.m */; }; + F8F2D9851986D3C400997A15 /* XAttrSet.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D9841986D3C400997A15 /* XAttrSet.m */; }; + F8F2D9921986D4C700997A15 /* CalculateItem.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D98E1986D4C700997A15 /* CalculateItem.m */; }; + F8F2D9931986D4C700997A15 /* RestoreItem.m in Sources */ = {isa = PBXBuildFile; fileRef = F8F2D9901986D4C700997A15 /* RestoreItem.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -553,6 +561,24 @@ F8F2D96C1986BF6800997A15 /* PackSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PackSet.h; sourceTree = ""; }; F8F2D96D1986BF6800997A15 /* PackSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PackSet.m; sourceTree = ""; }; F8F2D96F1986C09300997A15 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; + F8F2D9721986D02D00997A15 /* S3RestorerParamSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = S3RestorerParamSet.h; sourceTree = ""; }; + F8F2D9731986D02D00997A15 /* S3RestorerParamSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = S3RestorerParamSet.m; sourceTree = ""; }; + F8F2D9751986D32B00997A15 /* S3Restorer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = S3Restorer.h; sourceTree = ""; }; + F8F2D9761986D32B00997A15 /* S3Restorer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = S3Restorer.m; sourceTree = ""; }; + F8F2D9791986D36D00997A15 /* S3RestorerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = S3RestorerDelegate.h; sourceTree = ""; }; + F8F2D97A1986D38900997A15 /* OSStatusDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSStatusDescription.h; sourceTree = ""; }; + F8F2D97B1986D38900997A15 /* OSStatusDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSStatusDescription.m; sourceTree = ""; }; + F8F2D97D1986D3A100997A15 /* FileAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileAttributes.h; sourceTree = ""; }; + F8F2D97E1986D3A100997A15 /* FileAttributes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileAttributes.m; sourceTree = ""; }; + F8F2D9801986D3B100997A15 /* FileACL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileACL.h; sourceTree = ""; }; + F8F2D9811986D3B100997A15 /* FileACL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileACL.m; sourceTree = ""; }; + F8F2D9831986D3C400997A15 /* XAttrSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XAttrSet.h; sourceTree = ""; }; + F8F2D9841986D3C400997A15 /* XAttrSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XAttrSet.m; sourceTree = ""; }; + F8F2D98D1986D4C700997A15 /* CalculateItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CalculateItem.h; sourceTree = ""; }; + F8F2D98E1986D4C700997A15 /* CalculateItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CalculateItem.m; sourceTree = ""; }; + F8F2D98F1986D4C700997A15 /* RestoreItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoreItem.h; sourceTree = ""; }; + F8F2D9901986D4C700997A15 /* RestoreItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoreItem.m; sourceTree = ""; }; + F8F2D9911986D4C700997A15 /* Restorer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Restorer.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -584,6 +610,8 @@ F829512019868345001DC91B /* cocoastack */, F8F2D92B1986BA0700997A15 /* repo */, 08FB7795FE84155DC02AAC07 /* arq_restore */, + F8F2D98C1986D49600997A15 /* commonrestore */, + F8F2D9711986D00C00997A15 /* s3restore */, 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */, 1AB674ADFE9D54B511CA2CBB /* Products */, F89A204513FAE29E0071D321 /* libz.dylib */, @@ -690,6 +718,8 @@ F829523919868E94001DC91B /* CWLSynthesizeSingleton.h */, F8F2D8E11986B75C00997A15 /* Computer.h */, F8F2D8E21986B75C00997A15 /* Computer.m */, + F8F2D9801986D3B100997A15 /* FileACL.h */, + F8F2D9811986D3B100997A15 /* FileACL.m */, F8F2D9631986BE7600997A15 /* NSData-GZip.h */, F8F2D9641986BE7600997A15 /* NSData-GZip.m */, F829512719868394001DC91B /* NSError_extra.h */, @@ -703,6 +733,8 @@ F829512319868345001DC91B /* HSLog.m */, F829523E19868EC7001DC91B /* NSString_extra.h */, F829523F19868EC7001DC91B /* NSString_extra.m */, + F8F2D97A1986D38900997A15 /* OSStatusDescription.h */, + F8F2D97B1986D38900997A15 /* OSStatusDescription.m */, F829DBFA19868FCA00D637E0 /* RFC822.h */, F829DBFB19868FCA00D637E0 /* RFC822.m */, F829521419868DEA001DC91B /* RegexKitLite.h */, @@ -1114,6 +1146,8 @@ F8F2D9501986BE2A00997A15 /* Fark.h */, F8F2D9511986BE2A00997A15 /* FarkImpl.h */, F8F2D9521986BE2A00997A15 /* FarkImpl.m */, + F8F2D97D1986D3A100997A15 /* FileAttributes.h */, + F8F2D97E1986D3A100997A15 /* FileAttributes.m */, F8F2D95D1986BE6100997A15 /* Node.h */, F8F2D95E1986BE6100997A15 /* Node.m */, F8F2D9541986BE3C00997A15 /* PackId.h */, @@ -1128,10 +1162,36 @@ F8F2D9481986BAD200997A15 /* Repo.m */, F8F2D95F1986BE6100997A15 /* Tree.h */, F8F2D9601986BE6100997A15 /* Tree.m */, + F8F2D9831986D3C400997A15 /* XAttrSet.h */, + F8F2D9841986D3C400997A15 /* XAttrSet.m */, ); path = repo; sourceTree = ""; }; + F8F2D9711986D00C00997A15 /* s3restore */ = { + isa = PBXGroup; + children = ( + F8F2D9751986D32B00997A15 /* S3Restorer.h */, + F8F2D9761986D32B00997A15 /* S3Restorer.m */, + F8F2D9791986D36D00997A15 /* S3RestorerDelegate.h */, + F8F2D9721986D02D00997A15 /* S3RestorerParamSet.h */, + F8F2D9731986D02D00997A15 /* S3RestorerParamSet.m */, + ); + path = s3restore; + sourceTree = ""; + }; + F8F2D98C1986D49600997A15 /* commonrestore */ = { + isa = PBXGroup; + children = ( + F8F2D98D1986D4C700997A15 /* CalculateItem.h */, + F8F2D98E1986D4C700997A15 /* CalculateItem.m */, + F8F2D98F1986D4C700997A15 /* RestoreItem.h */, + F8F2D9901986D4C700997A15 /* RestoreItem.m */, + F8F2D9911986D4C700997A15 /* Restorer.h */, + ); + path = commonrestore; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1188,6 +1248,7 @@ F8F2D8711986B5D900997A15 /* GoogleDriveTargetConnection.m in Sources */, F8F2D9681986BEA300997A15 /* BlobKeyIO.m in Sources */, F8F2D8821986B63400997A15 /* BaseTargetConnection.m in Sources */, + F8F2D9931986D4C700997A15 /* RestoreItem.m in Sources */, F8F2D93D1986BA7900997A15 /* UserLibrary.m in Sources */, F8F2D86E1986B5CC00997A15 /* SFTPTargetConnection.m in Sources */, F829522019868E26001DC91B /* GoogleDrive.m in Sources */, @@ -1248,15 +1309,18 @@ F829524319868ED6001DC91B /* NSData-InputStream.m in Sources */, F8F2D8E01986B74400997A15 /* UserAndComputer.m in Sources */, F829DC12198691CB00D637E0 /* DateIO.m in Sources */, + F8F2D9771986D32B00997A15 /* S3Restorer.m in Sources */, F8F2D8931986B66000997A15 /* GlacierJobLister.m in Sources */, F8F2D9031986B78600997A15 /* RealNode.m in Sources */, F8F2D87C1986B61800997A15 /* GoogleDriveRemoteFS.m in Sources */, F829521319868DE6001DC91B /* RegexKitLite.m in Sources */, F8F2D8FD1986B78600997A15 /* ArrayNode.m in Sources */, + F8F2D9741986D02D00997A15 /* S3RestorerParamSet.m in Sources */, F82951ED19868D90001DC91B /* HTTPConnectionFactory.m in Sources */, F8F2D9251986B98600997A15 /* BlobKey.m in Sources */, F829523319868E59001DC91B /* SBJsonParser.m in Sources */, F8F2D8A51986B67500997A15 /* Vault.m in Sources */, + F8F2D97F1986D3A100997A15 /* FileAttributes.m in Sources */, F829DC15198691CB00D637E0 /* NSErrorIO.m in Sources */, F82951EE19868D90001DC91B /* HTTPInputStream.m in Sources */, F829523419868E59001DC91B /* SBJsonWriter.m in Sources */, @@ -1275,6 +1339,7 @@ F8F2D9061986B78600997A15 /* XMLPListReader.m in Sources */, F8295164198683D5001DC91B /* packet.c in Sources */, F829516B198683D5001DC91B /* userauth.c in Sources */, + F8F2D9921986D4C700997A15 /* CalculateItem.m in Sources */, F829515E198683D5001DC91B /* kex.c in Sources */, F8F2D8A61986B67500997A15 /* VaultDeleter.m in Sources */, F8F2D8BB1986B6E000997A15 /* SubscribeResponse.m in Sources */, @@ -1322,6 +1387,7 @@ F8F2D87B1986B61800997A15 /* SFTPRemoteFS.m in Sources */, F829523C19868EA4001DC91B /* NetMonitor.m in Sources */, F8295191198683F9001DC91B /* RemoteS3Signer.m in Sources */, + F8F2D9851986D3C400997A15 /* XAttrSet.m in Sources */, F8F2D9051986B78600997A15 /* XMLPlistParser.m in Sources */, F8F2D9441986BA9B00997A15 /* Bucket.m in Sources */, F829DC2E1986927000D637E0 /* FileOutputStream.m in Sources */, @@ -1330,11 +1396,13 @@ F82951EC19868D90001DC91B /* NSData-Base64Extensions.m in Sources */, F8295193198683F9001DC91B /* S3DeleteReceiver.m in Sources */, F8F2D9311986BA2200997A15 /* CommitFailedFile.m in Sources */, + F8F2D97C1986D38900997A15 /* OSStatusDescription.m in Sources */, F8F2D8851986B64D00997A15 /* GlacierAuthorizationProvider.m in Sources */, F83C1AE311CA7C7C0001958F /* arq_restore.m in Sources */, F8295190198683F9001DC91B /* PathReceiver.m in Sources */, F829DC281986924E00D637E0 /* DataOutputStream.m in Sources */, F8295196198683F9001DC91B /* S3MultiDeleteResponse.m in Sources */, + F8F2D9821986D3B100997A15 /* FileACL.m in Sources */, F8F2D9021986B78600997A15 /* IntegerNode.m in Sources */, F8F2D8921986B66000997A15 /* GlacierJob.m in Sources */, F8F2D8AD1986B6A300997A15 /* SHA256Hash.m in Sources */, diff --git a/cocoastack/shared/FileACL.h b/cocoastack/shared/FileACL.h new file mode 100644 index 0000000..894c0d9 --- /dev/null +++ b/cocoastack/shared/FileACL.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "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 THE COPYRIGHT + OWNER 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. + */ + + + + +@interface FileACL : NSObject { +} ++ (BOOL)aclText:(NSString **)aclText forFile:(NSString *)path error:(NSError **)error; ++ (BOOL)writeACLText:(NSString *)aclText toFile:(NSString *)path error:(NSError **)error; +@end diff --git a/cocoastack/shared/FileACL.m b/cocoastack/shared/FileACL.m new file mode 100644 index 0000000..067a7eb --- /dev/null +++ b/cocoastack/shared/FileACL.m @@ -0,0 +1,101 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "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 THE COPYRIGHT + OWNER 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. + */ + +#include +#include +#import "FileACL.h" + +#import "NSError_extra.h" + +@implementation FileACL ++ (BOOL)aclText:(NSString **)aclText forFile:(NSString *)path error:(NSError **)error { + *aclText = nil; + const char *pathChars = [path fileSystemRepresentation]; + acl_t acl = acl_get_link_np(pathChars, ACL_TYPE_EXTENDED); + if (!acl) { + if (errno != ENOENT) { + int errnum = errno; + HSLogError(@"acl_get_link_np(%@) error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to get ACL of %@: %s", path, strerror(errnum)); + return NO; + } + } else { + char *aclTextChars = acl_to_text(acl, NULL); + if (!aclTextChars) { + acl_free(acl); + int errnum = errno; + HSLogError(@"acl_to_text from %@ error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to convert ACL of %@ to text: %s", path, strerror(errnum)); + return NO; + } + *aclText = [NSString stringWithUTF8String:aclTextChars]; + acl_free(aclTextChars); + acl_free(acl); + } + return YES; +} ++ (BOOL)writeACLText:(NSString *)aclText toFile:(NSString *)path error:(NSError **)error { + HSLogTrace(@"applying ACL %@ to %@", aclText, path); + const char *pathChars = [path fileSystemRepresentation]; + acl_t acl = acl_from_text([aclText UTF8String]); + if (!acl) { + int errnum = errno; + HSLogError(@"acl_from_text(%@) error %d: %s", aclText, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to convert ACL text '%@' to ACL: %s", aclText, strerror(errnum)); + return NO; + } + + BOOL ret = NO; + struct stat st; + if (lstat(pathChars, &st) == -1) { + int errnum = errno; + HSLogError(@"lstat(%@) error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"%@: %s", path, strerror(errnum)); + goto writeACLText_error; + } + if (S_ISLNK(st.st_mode)) { + ret = acl_set_link_np(pathChars, ACL_TYPE_EXTENDED, acl) != -1; + } else { + ret = acl_set_file(pathChars, ACL_TYPE_EXTENDED, acl) != -1; + } + if (!ret) { + int errnum = errno; + HSLogError(@"acl_set(%@) error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to set ACL '%@' on %@: %s", aclText, path, strerror(errnum)); + goto writeACLText_error; + } + ret = YES; +writeACLText_error: + acl_free(acl); + return ret; +} +@end diff --git a/cocoastack/shared/OSStatusDescription.h b/cocoastack/shared/OSStatusDescription.h new file mode 100644 index 0000000..66cd4a1 --- /dev/null +++ b/cocoastack/shared/OSStatusDescription.h @@ -0,0 +1,40 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "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 THE COPYRIGHT + OWNER 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. + */ + + + + +@interface OSStatusDescription : NSObject { + +} ++ (NSString *)descriptionForOSStatus:(OSStatus)status; +@end diff --git a/cocoastack/shared/OSStatusDescription.m b/cocoastack/shared/OSStatusDescription.m new file mode 100644 index 0000000..652a036 --- /dev/null +++ b/cocoastack/shared/OSStatusDescription.m @@ -0,0 +1,82 @@ +/* + Copyright (c) 2009-2011, Stefan Reitshamer http://www.haystacksoftware.com + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the names of PhotoMinds LLC or Haystack Software, nor the names of + their contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "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 THE COPYRIGHT + OWNER 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. + */ + +#import "OSStatusDescription.h" + + +@implementation OSStatusDescription ++ (NSString *)descriptionForOSStatus:(OSStatus)status { + NSString *msg = [(NSString *)SecCopyErrorMessageString(status, NULL) autorelease]; + if (msg == nil) { + switch (status) { + case ioErr: + return @"I/O error"; // GetMacOSStatusCommentString() returns "I/O error (bummers)", which isn't appropriate! + case nsvErr: + return @"No such volume"; + case bdNamErr: + return @"Bad file name"; + case fnfErr: + return @"File not found"; + case errAuthorizationSuccess: + return @"The operation completed successfully."; + case errAuthorizationInvalidSet: + return @"The set parameter is invalid."; + case errAuthorizationInvalidRef: + return @"The authorization parameter is invalid."; + case errAuthorizationInvalidPointer: + return @"The authorizedRights parameter is invalid."; + case errAuthorizationDenied: + return @"The Security Server denied authorization for one or more requested rights. This error is also returned if there was no definition found in the policy database, or a definition could not be created."; + case errAuthorizationCanceled: + return @"The user canceled the operation"; + case errAuthorizationInteractionNotAllowed: + return @"The Security Server denied authorization because no user interaction is allowed."; + case errAuthorizationInternal: + return @"An unrecognized internal error occurred."; + case errAuthorizationExternalizeNotAllowed: + return @"The Security Server denied externalization of the authorization reference."; + case errAuthorizationInternalizeNotAllowed: + return @"The Security Server denied internalization of the authorization reference."; + case errAuthorizationInvalidFlags: + return @"The flags parameter is invalid."; + case errAuthorizationToolExecuteFailure: + return @"The tool failed to execute."; + case errAuthorizationToolEnvironmentError: + return @"The attempt to execute the tool failed to return a success or an error code."; + } + } + if ([msg length] == 0) { + msg = [NSString stringWithFormat:@"error %ld", (long)status]; + } + return msg; +} +@end diff --git a/commonrestore/CalculateItem.h b/commonrestore/CalculateItem.h new file mode 100644 index 0000000..55712d8 --- /dev/null +++ b/commonrestore/CalculateItem.h @@ -0,0 +1,31 @@ +// +// CalculateItem.h +// Arq +// +// Created by Stefan Reitshamer on 6/10/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#include +@class Tree; +@class Node; +@class Repo; +@protocol Restorer; + + +@interface CalculateItem : NSObject { + NSString *path; + Tree *tree; + Node *node; + struct stat st; + unsigned long long bytesToTransfer; + NSMutableSet *filesToSkip; + NSMutableArray *nextItems; +} +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree; +- (id)initWithPath:(NSString *)thePath node:(Node *)theNode; +- (BOOL)calculateWithRepo:(Repo *)theRepo restorer:(id )theRestorer error:(NSError **)error; +- (unsigned long long)bytesToTransfer; +- (NSSet *)filesToSkip; +- (NSArray *)nextItems; +@end diff --git a/commonrestore/CalculateItem.m b/commonrestore/CalculateItem.m new file mode 100644 index 0000000..e655dda --- /dev/null +++ b/commonrestore/CalculateItem.m @@ -0,0 +1,115 @@ +// +// CalculateItem.m +// Arq +// +// Created by Stefan Reitshamer on 6/10/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "CalculateItem.h" +#import "Tree.h" +#import "Node.h" +#import "Repo.h" +#import "BlobKey.h" +#import "FileInputStream.h" +#import "BufferedInputStream.h" +#import "NSData-GZip.h" +#import "SHA1Hash.h" +#import "Restorer.h" + + +@implementation CalculateItem +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree { + if (self = [super init]) { + path = [thePath retain]; + tree = [theTree retain]; + filesToSkip = [[NSMutableSet alloc] init]; + nextItems = [[NSMutableArray alloc] init]; + } + return self; +} +- (id)initWithPath:(NSString *)thePath node:(Node *)theNode { + if (self = [super init]) { + path = [thePath retain]; + node = [theNode retain]; + filesToSkip = [[NSMutableSet alloc] init]; + nextItems = [[NSMutableArray alloc] init]; + } + return self; +} +- (void)dealloc { + [path release]; + [node release]; + [filesToSkip release]; + [nextItems release]; + [super dealloc]; +} + +- (BOOL)calculateWithRepo:(Repo *)theRepo restorer:(id )theRestorer error:(NSError **)error { + if (tree != nil) { + if (![self addBlobKeyToBytesToTransfer:[tree xattrsBlobKey] restorer:theRestorer error:error]) { + return NO; + } + if (![self addBlobKeyToBytesToTransfer:[tree aclBlobKey] restorer:theRestorer error:error]) { + return NO; + } + for (NSString *childNodeName in [tree childNodeNames]) { + Node *childNode = [tree childNodeWithName:childNodeName]; + NSString *childPath = [path stringByAppendingPathComponent:childNodeName]; + [nextItems addObject:[[[CalculateItem alloc] initWithPath:childPath node:childNode] autorelease]]; + } + } else { + NSAssert(node != nil, @"node can't be nil if tree is nil"); + if ([node isTree]) { + Tree *childTree = [theRepo treeForBlobKey:[node treeBlobKey] error:error]; + if (childTree == nil) { + return NO; + } + [nextItems addObject:[[[CalculateItem alloc] initWithPath:path tree:childTree] autorelease]]; + } else { + if (![self addBlobKeyToBytesToTransfer:[node xattrsBlobKey] restorer:theRestorer error:error]) { + return NO; + } + if (![self addBlobKeyToBytesToTransfer:[node aclBlobKey] restorer:theRestorer error:error]) { + return NO; + } + for (BlobKey *dataBlobKey in [node dataBlobKeys]) { + if (![self addBlobKeyToBytesToTransfer:dataBlobKey restorer:theRestorer error:error]) { + return NO; + } + } + } + } + return YES; +} +- (unsigned long long)bytesToTransfer { + return bytesToTransfer; +} +- (NSSet *)filesToSkip { + return filesToSkip; +} +- (NSArray *)nextItems { + return nextItems; +} + + +#pragma mark internal +- (BOOL)addBlobKeyToBytesToTransfer:(BlobKey *)theBlobKey restorer:(id )theRestorer error:(NSError **)error { + if (theBlobKey == nil) { + return YES; + } + NSError *myError = nil; + NSNumber *size = [theRestorer sizeOfBlob:theBlobKey error:&myError]; + if (size == nil) { + SETERRORFROMMYERROR; + if ([myError isErrorWithDomain:[theRestorer errorDomain] code:ERROR_NOT_FOUND]) { + HSLogError(@"%@", [myError localizedDescription]); + size = [NSNumber numberWithUnsignedLongLong:0]; + } else { + return NO; + } + } + bytesToTransfer += [size unsignedLongLongValue]; + return YES; +} +@end diff --git a/commonrestore/RestoreItem.h b/commonrestore/RestoreItem.h new file mode 100644 index 0000000..f552590 --- /dev/null +++ b/commonrestore/RestoreItem.h @@ -0,0 +1,34 @@ +// +// RestoreItem.h +// Arq +// +// Created by Stefan Reitshamer on 5/30/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + + +@class Tree; +@class Node; +@class Repo; +@protocol Restorer; +@class FileOutputStream; + + +@interface RestoreItem : NSObject { + Tree *tree; + Node *node; + NSString *path; + int restoreAction; + FileOutputStream *fileOutputStream; + NSUInteger dataBlobKeyIndex; + BOOL errorOccurred; +} +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree; +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree node:(Node *)theNode; +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree node:(Node *)theNode fileOutputStream:(FileOutputStream *)theFileOutputStream dataBlobKeyIndex:(NSUInteger)theDataBlobKeyIndex; + +- (NSString *)errorDomain; +- (NSString *)path; +- (BOOL)restoreWithHardlinks:(NSMutableDictionary *)theHardlinks restorer:(id )theRestorer error:(NSError **)error; +- (NSArray *)nextItemsWithRepo:(Repo *)theRepo error:(NSError **)error; +@end diff --git a/commonrestore/RestoreItem.m b/commonrestore/RestoreItem.m new file mode 100644 index 0000000..650a3bb --- /dev/null +++ b/commonrestore/RestoreItem.m @@ -0,0 +1,644 @@ +// +// RestoreItem.m +// Arq +// +// Created by Stefan Reitshamer on 5/30/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "RestoreItem.h" +#import "Tree.h" +#import "Node.h" +#import "Repo.h" +#import "OSStatusDescription.h" +#import "FileAttributes.h" +#import "FileACL.h" +#import "DataInputStream.h" +#import "BufferedInputStream.h" +#import "BlobKey.h" +#import "NSData-GZip.h" +#import "XAttrSet.h" +#import "Restorer.h" +#import "NSFileManager_extra.h" +#import "FileOutputStream.h" +#import "BufferedOutputStream.h" + + +enum { + kRestoreActionRestoreTree=1, + kRestoreActionRestoreNode=2, + kRestoreActionApplyTree=3, + kRestoreActionRestoreFileData=4 +} RestoreAction; + + +@implementation RestoreItem +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree { + if (self = [super init]) { + path = [thePath retain]; + tree = [theTree retain]; + restoreAction = kRestoreActionRestoreTree; + } + return self; +} +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree node:(Node *)theNode { + if (self = [super init]) { + path = [thePath retain]; + tree = [theTree retain]; + node = [theNode retain]; + restoreAction = kRestoreActionRestoreNode; + } + return self; +} +- (id)initWithPath:(NSString *)thePath tree:(Tree *)theTree node:(Node *)theNode fileOutputStream:(FileOutputStream *)theFileOutputStream dataBlobKeyIndex:(NSUInteger)theDataBlobKeyIndex { + if (self = [super init]) { + path = [thePath retain]; + tree = [theTree retain]; + node = [theNode retain]; + fileOutputStream = [theFileOutputStream retain]; + dataBlobKeyIndex = theDataBlobKeyIndex; + restoreAction = kRestoreActionRestoreFileData; + } + return self; +} + +- (void)dealloc { + [tree release]; + [node release]; + [path release]; + [fileOutputStream release]; + [super dealloc]; +} + +- (NSString *)errorDomain { + return @"RestoreItemErrorDomain"; +} +- (NSString *)path { + return path; +} +- (BOOL)restoreWithHardlinks:(NSMutableDictionary *)theHardlinks restorer:(id )theRestorer error:(NSError **)error { + BOOL ret = NO; + switch (restoreAction) { + case kRestoreActionRestoreTree: + ret = [self restoreTreeWithHardlinks:theHardlinks restorer:theRestorer error:error]; + break; + case kRestoreActionRestoreNode: + ret = [self restoreNodeWithHardlinks:theHardlinks restorer:theRestorer error:error]; + break; + case kRestoreActionApplyTree: + ret = [self applyTreeWithHardlinks:theHardlinks restorer:theRestorer error:error]; + break; + case kRestoreActionRestoreFileData: + ret = [self restoreFileDataWithRestorer:theRestorer error:error]; + break; + default: + NSAssert(0==1, @"unknown restore action"); + break; + } + return ret; +} +- (NSArray *)nextItemsWithRepo:(Repo *)theRepo error:(NSError **)error { + NSArray *ret = nil; + switch (restoreAction) { + case kRestoreActionRestoreTree: + ret = [self nextItemsForTreeWithRepo:theRepo error:error]; + break; + case kRestoreActionRestoreFileData: + case kRestoreActionRestoreNode: + if (!errorOccurred && fileOutputStream != nil && [[node dataBlobKeys] count] > dataBlobKeyIndex) { + RestoreItem *nextItem = [[[RestoreItem alloc] initWithPath:path tree:tree node:node fileOutputStream:fileOutputStream dataBlobKeyIndex:dataBlobKeyIndex] autorelease]; + ret = [NSArray arrayWithObject:nextItem]; + } else { + ret = [NSArray array]; + } + break; + case kRestoreActionApplyTree: + ret = [NSArray array]; + break; + break; + default: + NSAssert(0==1, @"unknown restore action"); + break; + } + return ret; +} + + +#pragma mark internal +- (id)initApplyItemWithTree:(Tree *)theTree path:(NSString *)thePath { + if (self = [super init]) { + tree = [theTree retain]; + restoreAction = kRestoreActionApplyTree; + path = [thePath retain]; + } + return self; +} + +- (BOOL)restoreTreeWithHardlinks:(NSMutableDictionary *)theHardlinks restorer:(id )theRestorer error:(NSError **)error { + NSNumber *inode = [NSNumber numberWithInt:[tree st_ino]]; + NSString *existing = nil; + if ([tree st_nlink] > 1) { + existing = [theHardlinks objectForKey:inode]; + } + if (existing != nil) { + // Link. + if (link([existing fileSystemRepresentation], [path fileSystemRepresentation]) == -1) { + int errnum = errno; + SETNSERROR([self errorDomain], -1, @"link(%@,%@): %s", existing, path, strerror(errnum)); + HSLogError(@"link() failed"); + return NO; + } + } else { + BOOL isDir = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) { + if (!isDir) { + if (![[NSFileManager defaultManager] removeItemAtPath:path error:error]) { + return NO; + } + } + } else { + if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:error]) { + return NO; + } + } + [theHardlinks setObject:path forKey:inode]; + } + if ([theRestorer useTargetUIDAndGID]) { + HSLogDebug(@"use restorer %@ target UID %d and GID %d", theRestorer, [theRestorer targetUID], [theRestorer targetGID]); + if (![FileAttributes applyUID:[theRestorer targetUID] gid:[theRestorer targetGID] toPath:path error:error]) { + return NO; + } + } else { + HSLogDebug(@"use tree %@ UID %d and GID %d", tree, [tree uid], [tree gid]); + if (![FileAttributes applyUID:[tree uid] gid:[tree gid] toPath:path error:error]) { + return NO; + } + } + return YES; +} +- (BOOL)restoreNodeWithHardlinks:(NSMutableDictionary *)theHardlinks restorer:(id )theRestorer error:(NSError **)error { + NSNumber *inode = [NSNumber numberWithInt:[node st_ino]]; + NSString *existing = nil; + if ([node st_nlink] > 1) { + existing = [theHardlinks objectForKey:inode]; + } + if (existing != nil) { + // Link. + if (link([existing fileSystemRepresentation], [path fileSystemRepresentation]) == -1) { + int errnum = errno; + SETNSERROR([self errorDomain], -1, @"link(%@,%@): %s", existing, path, strerror(errnum)); + HSLogError(@"link() failed"); + return NO; + } + } else { + struct stat st; + if (lstat([path fileSystemRepresentation], &st) == -1) { + int errnum = errno; + if (errnum != ENOENT) { + SETNSERROR(@"UnixErrorDomain", errnum, @"lstat(%@): %s", path, strerror(errnum)); + return NO; + } + } else { + BOOL shouldSkip = [theRestorer shouldSkipFile:path]; + if (!S_ISREG(st.st_mode) || !shouldSkip) { + HSLogDetail(@"removing %@ because it's in the way", path); + if (![[NSFileManager defaultManager] removeItemAtPath:path error:error]) { + HSLogError(@"error removing %@", path); + return NO; + } + } + } + int mode = [node mode]; + if (S_ISFIFO(mode)) { + if (mkfifo([path fileSystemRepresentation], mode) == -1) { + int errnum = errno; + SETNSERROR([self errorDomain], errnum, @"mkfifo(%@): %s", path, strerror(errnum)); + return NO; + } + if (![self applyNodeWithRestorer:theRestorer error:error]) { + HSLogError(@"applyNode error"); + return NO; + } + } else if (S_ISSOCK(mode)) { + // Skip socket -- restoring it doesn't make any sense. + } else { + if (![self createFile:node restorer:theRestorer error:error]) { + return NO; + } + // We call this once here in case it's not a regular file. We also call it after the last blob in restoreFileDataWithRestorer so the metadata are set after the last file blob. + if (![self applyNodeWithRestorer:theRestorer error:error]) { + return NO; + } + } + [theHardlinks setObject:path forKey:inode]; + } + return YES; +} +- (BOOL)applyTreeWithHardlinks:(NSMutableDictionary *)theHardlinks restorer:(id )theRestorer error:(NSError **)error { + // Make sure all items are available for download. + if ([tree xattrsBlobKey] != nil) { + NSNumber *available = [theRestorer isObjectAvailableForBlobKey:[tree xattrsBlobKey] error:error]; + if (available == nil) { + return NO; + } + if (![available boolValue]) { + SETNSERROR([self errorDomain], ERROR_GLACIER_OBJECT_NOT_AVAILABLE, @"%@ not available", [tree xattrsBlobKey]); + return NO; + } + } + if ([tree aclBlobKey] != nil) { + NSNumber *available = [theRestorer isObjectAvailableForBlobKey:[tree aclBlobKey] error:error]; + if (available == nil) { + return NO; + } + if (![available boolValue]) { + SETNSERROR([self errorDomain], ERROR_GLACIER_OBJECT_NOT_AVAILABLE, @"%@ not available", [tree aclBlobKey]); + return NO; + } + } + + + if ([tree xattrsBlobKey] != nil && ![self applyXAttrsBlobKey:[tree xattrsBlobKey] restorer:theRestorer error:error]) { + return NO; + } + + FSRef fsRef; + Boolean isDirectory; + OSStatus oss = FSPathMakeRef((UInt8*)[path fileSystemRepresentation], &fsRef, &isDirectory); + if (oss != noErr) { + if (oss == bdNamErr) { + HSLogInfo(@"not applying some metadata to %@: bad name", path); + return YES; + } else { + SETNSERROR([self errorDomain], -1, @"%@", [OSStatusDescription descriptionForOSStatus:oss]); + return NO; + } + } + + if (![FileAttributes applyFinderFlags:[tree finderFlags] to:&fsRef isDirectory:YES error:error] + || ![FileAttributes applyExtendedFinderFlags:[tree extendedFinderFlags] to:&fsRef isDirectory:YES error:error]) { + return NO; + } + if (![FileAttributes applyMode:[tree mode] toPath:path isDirectory:YES error:error]) { + return NO; + } + if (!S_ISLNK([tree mode]) && [tree treeVersion] >= 7 && ![FileAttributes applyMTimeSec:tree.mtime_sec mTimeNSec:tree.mtime_nsec toPath:path error:error]) { + return NO; + } + if (([tree treeVersion] >= 7) && ![FileAttributes applyCreateTimeSec:tree.createTime_sec createTimeNSec:tree.createTime_nsec to:&fsRef error:error]) { + return NO; + } + if (![FileAttributes applyFlags:(unsigned long)[tree flags] toPath:path error:error]) { + return NO; + } + if (([tree uid] < 500 && [tree gid] < 500) || ![theRestorer useTargetUIDAndGID]) { + if ([tree aclBlobKey] != nil && ![self applyACLBlobKey:[tree aclBlobKey] restorer:theRestorer error:error]) { + return NO; + } + } + return YES; +} +- (BOOL)applyXAttrsBlobKey:(BlobKey *)xattrsBlobKey restorer:(id )theRestorer error:(NSError **)error { + NSAssert(xattrsBlobKey != nil, @"xattrsBlobKey may not be nil"); + NSData *xattrsData = [theRestorer dataForBlobKey:xattrsBlobKey error:error]; + if (xattrsData == nil) { + return NO; + } + if ([xattrsBlobKey compressed]) { + xattrsData = [xattrsData gzipInflate:error]; + if (xattrsData == nil) { + return NO; + } + } + DataInputStream *dis = [[[DataInputStream alloc] initWithData:xattrsData description:[NSString stringWithFormat:@"xattrs %@", xattrsBlobKey]] autorelease]; + BufferedInputStream *bis = [[[BufferedInputStream alloc] initWithUnderlyingStream:dis] autorelease]; + XAttrSet *set = [[[XAttrSet alloc] initWithBufferedInputStream:bis error:error] autorelease]; + if (!set) { + return NO; + } + if (![set applyToFile:path error:error]) { + return NO; + } + return YES; +} +- (BOOL)applyACLBlobKey:(BlobKey *)theACLBlobKey restorer:(id )theRestorer error:(NSError **)error { + NSAssert(theACLBlobKey != nil, @"theACLBlobKey may not be nil"); + NSData *data = [theRestorer dataForBlobKey:theACLBlobKey error:error]; + if (data == nil) { + return NO; + } + if ([theACLBlobKey compressed]) { + data = [data gzipInflate:error]; + if (data == nil) { + return NO; + } + } + NSString *aclString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; + + NSString *currentAclString = nil; + if (![FileACL aclText:¤tAclString forFile:path error:error]) { + return NO; + } + if (![currentAclString isEqualToString:aclString] && [aclString length] > 0) { + if (![FileACL writeACLText:aclString toFile:path error:error]) { + return NO; + } + } + return YES; +} +- (BOOL)createFile:(Node *)theNode restorer:(id )theRestorer error:(NSError **)error { + // Make sure all items are available for download. + if ([theNode xattrsBlobKey] != nil) { + NSNumber *available = [theRestorer isObjectAvailableForBlobKey:[theNode xattrsBlobKey] error:error]; + if (available == nil) { + return NO; + } + if (![available boolValue]) { + SETNSERROR([self errorDomain], ERROR_GLACIER_OBJECT_NOT_AVAILABLE, @"xattrs blob %@ not available", [theNode xattrsBlobKey]); + return NO; + } + } + if ([theNode aclBlobKey] != nil) { + NSNumber *available = [theRestorer isObjectAvailableForBlobKey:[theNode aclBlobKey] error:error]; + if (available == nil) { + return NO; + } + if (![available boolValue]) { + SETNSERROR([self errorDomain], ERROR_GLACIER_OBJECT_NOT_AVAILABLE, @"acl blob %@ not available", [theNode aclBlobKey]); + return NO; + } + } + + BOOL shouldSkipFile = [theRestorer shouldSkipFile:path]; + BOOL shouldRestoreUIDAndGID = YES; + + if (![[NSFileManager defaultManager] ensureParentPathExistsForPath:path targetUID:[theRestorer targetUID] targetGID:[theRestorer targetGID] error:error]) { + HSLogError(@"error ensuring path %@ exists", path); + return NO; + } + if (!shouldSkipFile) { + if ([[NSFileManager defaultManager] fileExistsAtPath:path] && ![[NSFileManager defaultManager] removeItemAtPath:path error:error]) { + HSLogError(@"error removing existing file %@", path); + return NO; + } + + HSLogTrace(@"%qu bytes -> %@", [node uncompressedDataSize], path); + if (S_ISLNK([node mode])) { + NSMutableData *data = [NSMutableData data]; + for (BlobKey *dataBlobKey in [node dataBlobKeys]) { + NSData *blobData = [theRestorer dataForBlobKey:dataBlobKey error:error]; + if (blobData == nil) { + HSLogError(@"error getting data for %@", dataBlobKey); + return NO; + } + if ([dataBlobKey compressed]) { + blobData = [blobData gzipInflate:error]; + if (blobData == nil) { + return NO; + } + } + [data appendData:blobData]; + } + NSString *target = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; + + if (symlink([target fileSystemRepresentation], [path fileSystemRepresentation]) == -1) { + int errnum = errno; + HSLogError(@"symlink(%@, %@) error %d: %s", target, path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to create symlink %@ to %@: %s", path, target, strerror(errnum)); + return NO; + } + HSLogDetail(@"restored %@", path); + } else if ([node uncompressedDataSize] > 0) { + fileOutputStream = [[FileOutputStream alloc] initWithPath:path append:NO]; + if ([[node dataBlobKeys] count] > 0) { + if (![self restoreFileDataWithRestorer:theRestorer error:error]) { + return NO; + } + shouldRestoreUIDAndGID = NO; + } + } else { + // It's a zero-byte file. + int fd = open([path fileSystemRepresentation], O_CREAT|O_EXCL, S_IRWXU); + if (fd == -1) { + int errnum = errno; + HSLogError(@"open(%@) error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to open %@: %s", path, strerror(errnum)); + return NO; + } + close(fd); + HSLogDetail(@"restored %@", path); + } + } else { + HSLogDetail(@"skipped restoring %@", path); + } + if (shouldRestoreUIDAndGID) { + if ([theRestorer useTargetUIDAndGID]) { + HSLogDebug(@"use restorer %@ target UID %d and GID %d", theRestorer, [theRestorer targetUID], [theRestorer targetGID]); + if (![FileAttributes applyUID:[theRestorer targetUID] gid:[theRestorer targetGID] toPath:path error:error]) { + return NO; + } + } else { + HSLogDebug(@"use node %@ UID %d and GID %d", node, [node uid], [node gid]); + if (![FileAttributes applyUID:[node uid] gid:[node gid] toPath:path error:error]) { + return NO; + } + } + } + + return YES; +} +- (BOOL)restoreFileDataWithRestorer:(id )theRestorer error:(NSError **)error { + NSError *myError = nil; + if (![self doRestoreFileDataWithRestorer:theRestorer error:&myError]) { + SETERRORFROMMYERROR; + if ([myError isErrorWithDomain:[self errorDomain] code:ERROR_GLACIER_OBJECT_NOT_AVAILABLE]) { + HSLogDebug(@"object #%ld not available yet", (unsigned long)dataBlobKeyIndex); + } else { + // An error occurred. Stop continuing to restore this file. + dataBlobKeyIndex = [[node dataBlobKeys] count]; + errorOccurred = YES; + } + return NO; + } + return YES; +} +- (BOOL)doRestoreFileDataWithRestorer:(id )theRestorer error:(NSError **)error { + BlobKey *theBlobKey = [[node dataBlobKeys] objectAtIndex:dataBlobKeyIndex]; + NSNumber *available = [theRestorer isObjectAvailableForBlobKey:theBlobKey error:error]; + if (available == nil) { + return NO; + } + if (![available boolValue]) { + SETNSERROR([self errorDomain], ERROR_GLACIER_OBJECT_NOT_AVAILABLE, @"acl blob %@ not available", theBlobKey); + return NO; + } + NSData *blobData = [theRestorer dataForBlobKey:theBlobKey error:error]; + if (blobData == nil) { + NSError *myError = nil; + if (![[NSFileManager defaultManager] removeItemAtPath:path error:&myError]) { + HSLogError(@"error deleting incomplete file %@: %@", path, [myError localizedDescription]); + } else { + HSLogError(@"deleted incomplete file %@", path); + } + return NO; + } + if ([theBlobKey compressed]) { + blobData = [blobData gzipInflate:error]; + if (blobData == nil) { + return NO; + NSError *myError = nil; + if (![[NSFileManager defaultManager] removeItemAtPath:path error:&myError]) { + HSLogError(@"error deleting incomplete file %@: %@", path, [myError localizedDescription]); + } else { + HSLogError(@"deleted incomplete file %@", path); + } + } + } + + BufferedOutputStream *bos = [[BufferedOutputStream alloc] initWithUnderlyingOutputStream:fileOutputStream]; + NSError *myError = nil; + BOOL ret = [bos writeFully:[blobData bytes] length:[blobData length] error:&myError] && [bos flush:&myError]; + [bos release]; + if (!ret) { + HSLogError(@"error appending data to %@: %@", path, myError); + SETERRORFROMMYERROR; + if (![[NSFileManager defaultManager] removeItemAtPath:path error:&myError]) { + HSLogError(@"error deleting incomplete file %@: %@", path, [myError localizedDescription]); + } else { + HSLogError(@"deleted incomplete file %@", path); + } + return NO; + } + dataBlobKeyIndex++; + HSLogDebug(@"appended blob %ld of %ld to %@", (unsigned long)dataBlobKeyIndex, (unsigned long)[[node dataBlobKeys] count], path); + + if (dataBlobKeyIndex >= [[node dataBlobKeys] count]) { + if ([theRestorer useTargetUIDAndGID]) { + HSLogDebug(@"use restorer %@ target UID %d and GID %d", theRestorer, [theRestorer targetUID], [theRestorer targetGID]); + if (![FileAttributes applyUID:[theRestorer targetUID] gid:[theRestorer targetGID] toPath:path error:error]) { + return NO; + } + } else { + HSLogDebug(@"use node %@ UID %d and GID %d", node, [node uid], [node gid]); + if (![FileAttributes applyUID:[node uid] gid:[node gid] toPath:path error:error]) { + return NO; + } + } + if (![self applyNodeWithRestorer:theRestorer error:error]) { + return NO; + } + } + return YES; +} +- (BOOL)applyNodeWithRestorer:(id )theRestorer error:(NSError **)error { + NSError *xattrsError = nil; + if ([node xattrsBlobKey] != nil && ![self applyXAttrsBlobKey:[node xattrsBlobKey] restorer:theRestorer error:&xattrsError]) { + HSLogError(@"failed to apply xattrs %@ to %@: %@", [node xattrsBlobKey], path, xattrsError); + } + if (([node uid] < 500 && [node gid] < 500) || ![theRestorer useTargetUIDAndGID]) { + NSError *aclError = nil; + if ([node aclBlobKey] != nil && ![self applyACLBlobKey:[node aclBlobKey] restorer:theRestorer error:&aclError]) { + HSLogError(@"failed to apply acl %@ to %@: %@", [node aclBlobKey], path, aclError); + } + } + + struct stat st; + if (lstat([path fileSystemRepresentation], &st) == -1) { + int errnum = errno; + HSLogError(@"lstat(%@) error %d: %s", path, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"%@: %s", path, strerror(errnum)); + return NO; + } + + FSRef fsRef; + Boolean isDirectory; + OSStatus oss = 0; + if (S_ISLNK(st.st_mode)) { + oss = SymlinkPathMakeRef((UInt8*)[path fileSystemRepresentation], &fsRef, &isDirectory); + } else { + oss = FSPathMakeRef((UInt8*)[path fileSystemRepresentation], &fsRef, &isDirectory); + } + if (oss == bdNamErr) { + HSLogInfo(@"skipping applying some metadata for %@: %@", path, [OSStatusDescription descriptionForOSStatus:oss]); + } else if (oss != noErr) { + SETNSERROR(@"MacFilesErrorDomain", oss, @"error making FSRef for %@: %@", path, [OSStatusDescription descriptionForOSStatus:oss]); + return NO; + } else { + if (!S_ISFIFO([node mode])) { + FileAttributes *fa = [[[FileAttributes alloc] initWithPath:path stat:&st error:error] autorelease]; + if (fa == nil) { + return NO; + } + if ([fa finderFlags] != [node finderFlags] && ![FileAttributes applyFinderFlags:[node finderFlags] to:&fsRef isDirectory:NO error:error]) { + return NO; + } + if ([fa extendedFinderFlags] != [node extendedFinderFlags] && ![FileAttributes applyExtendedFinderFlags:[node extendedFinderFlags] to:&fsRef isDirectory:NO error:error]) { + return NO; + } + if ([node finderFileType] != nil || [node finderFileCreator] != nil) { + if (![[fa finderFileType] isEqualToString:[node finderFileType]] || ![[fa finderFileCreator] isEqualToString:[node finderFileCreator]]) { + if (![FileAttributes applyFinderFileType:[node finderFileType] finderFileCreator:[node finderFileCreator] to:&fsRef error:error]) { + return NO; + } + } + } + } + if (([node treeVersion] >= 7) && ![FileAttributes applyCreateTimeSec:node.createTime_sec createTimeNSec:node.createTime_nsec to:&fsRef error:error]) { + return NO; + } + } + if (st.st_mode != [node mode] && ![FileAttributes applyMode:[node mode] toPath:path isDirectory:NO error:error]) { + return NO; + } + if (!S_ISLNK([node mode]) && [node treeVersion] >= 7 && ![FileAttributes applyMTimeSec:node.mtime_sec mTimeNSec:node.mtime_nsec toPath:path error:error]) { + return NO; + } + if (!S_ISFIFO([node mode])) { + if (st.st_flags != [node flags] && ![FileAttributes applyFlags:(unsigned long)[node flags] toPath:path error:error]) { + return NO; + } + } + return YES; +} +- (NSArray *)nextItemsForTreeWithRepo:(Repo *)theRepo error:(NSError **)error { + NSMutableArray *nextItems = [NSMutableArray array]; + NSAutoreleasePool *pool = nil; + for (NSString *childNodeName in [tree childNodeNames]) { + [pool drain]; + pool = [[NSAutoreleasePool alloc] init]; + Node *childNode = [tree childNodeWithName:childNodeName]; + NSString *childPath = [path stringByAppendingPathComponent:childNodeName]; + if ([childNode isTree]) { + Tree *childTree = [theRepo treeForBlobKey:[childNode treeBlobKey] error:error]; + if (childTree == nil) { + nextItems = nil; + break; + } + RestoreItem *childRestoreItem = [[[RestoreItem alloc] initWithPath:childPath tree:childTree] autorelease]; + [nextItems addObject:childRestoreItem]; + } else { + RestoreItem *childRestoreItem = [[[RestoreItem alloc] initWithPath:childPath tree:tree node:childNode] autorelease]; + [nextItems addObject:childRestoreItem]; + } + } + if (nextItems == nil && error != NULL) { + [*error retain]; + } + [pool drain]; + if (nextItems == nil && error != NULL) { + [*error autorelease]; + } + if (nextItems == nil) { + return nil; + } + RestoreItem *treeRestoreItem = [[[RestoreItem alloc] initApplyItemWithTree:tree path:path] autorelease]; + [nextItems addObject:treeRestoreItem]; + return nextItems; +} + + +#pragma mark NSObject +- (NSString *)description { + return [NSString stringWithFormat:@"", path]; +} +@end diff --git a/commonrestore/Restorer.h b/commonrestore/Restorer.h new file mode 100644 index 0000000..09762a1 --- /dev/null +++ b/commonrestore/Restorer.h @@ -0,0 +1,22 @@ +// +// Restorer.h +// Arq +// +// Created by Stefan Reitshamer on 6/12/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +@class BlobKey; + + +@protocol Restorer +- (NSString *)errorDomain; +- (BOOL)requestBlobKey:(BlobKey *)theBlobKey error:(NSError **)error; +- (NSNumber *)isObjectAvailableForBlobKey:(BlobKey *)theBlobKey error:(NSError **)error; +- (NSNumber *)sizeOfBlob:(BlobKey *)theBlobKey error:(NSError **)error; +- (NSData *)dataForBlobKey:(BlobKey *)theBlobKey error:(NSError **)error; +- (BOOL)shouldSkipFile:(NSString *)thePath; +- (BOOL)useTargetUIDAndGID; +- (uid_t)targetUID; +- (gid_t)targetGID; +@end diff --git a/repo/FileAttributes.h b/repo/FileAttributes.h new file mode 100644 index 0000000..b54bd8c --- /dev/null +++ b/repo/FileAttributes.h @@ -0,0 +1,45 @@ +// +// FileAttributes.h +// Backup +// +// Created by Stefan Reitshamer on 4/22/09. +// Copyright 2009 PhotoMinds LLC. All rights reserved. +// + +#include + + + +OSStatus SymlinkPathMakeRef(const UInt8 *path, FSRef *ref, Boolean *isDirectory); + +@interface FileAttributes : NSObject { + BOOL targetExists; + struct timespec createTime; + int finderFlags; + int extendedFinderFlags; + NSString *finderFileType; + NSString *finderFileCreator; + BOOL isFileExtensionHidden; +} ++ (NSString *)errorDomain; + +- (id)initWithPath:(NSString *)thePath stat:(struct stat *)theStat error:(NSError **)error; + +- (int)finderFlags; +- (int)extendedFinderFlags; +- (NSString *)finderFileType; +- (NSString *)finderFileCreator; +- (int64_t)createTime_sec; +- (int64_t)createTime_nsec; +- (BOOL)isFileExtensionHidden; + ++ (BOOL)applyFinderFileType:(NSString *)fft finderFileCreator:(NSString *)ffc to:(FSRef *)fsRef error:(NSError **)error; ++ (BOOL)applyFlags:(unsigned long)flags toPath:(NSString *)thePath error:(NSError **)error; ++ (BOOL)applyFinderFlags:(int)ff to:(FSRef *)fsRef isDirectory:(BOOL)isDirectory error:(NSError **)error; ++ (BOOL)applyExtendedFinderFlags:(int)eff to:(FSRef *)fsRef isDirectory:(BOOL)isDirectory error:(NSError **)error; ++ (BOOL)applyFileExtensionHidden:(BOOL)hidden toPath:(NSString *)thePath error:(NSError **)error; ++ (BOOL)applyUID:(int)uid gid:(int)gid toPath:(NSString *)thePath error:(NSError **)error; ++ (BOOL)applyMode:(int)mode toPath:(NSString *)thePath isDirectory:(BOOL)isDirectory error:(NSError **)error; ++ (BOOL)applyMTimeSec:(int64_t)mtime_sec mTimeNSec:(int64_t)mtime_nsec toPath:(NSString *)thePath error:(NSError **)error; ++ (BOOL)applyCreateTimeSec:(int64_t)theCreateTime_sec createTimeNSec:(int64_t)theCreateTime_nsec to:(FSRef *)fsRef error:(NSError **)error; +@end diff --git a/repo/FileAttributes.m b/repo/FileAttributes.m new file mode 100644 index 0000000..dbcb309 --- /dev/null +++ b/repo/FileAttributes.m @@ -0,0 +1,377 @@ +// +// FileAttributes.m +// Backup +// +// Created by Stefan Reitshamer on 4/22/09. +// Copyright 2009 PhotoMinds LLC. All rights reserved. +// + +#include +#include +#include +#include +#include +#import "FileAttributes.h" +#import "SHA1Hash.h" +#import "FileACL.h" + +#import "OSStatusDescription.h" +#import "NSError_extra.h" + +#define kCouldNotCreateCFString 4 +#define kCouldNotGetStringData 5 +#define MAX_PATH 1024 + +struct createDateBuf { + u_int32_t length; + struct timespec createTime; +}; + +static OSStatus ConvertCStringToHFSUniStr(const char* cStr, HFSUniStr255 *uniStr) { + OSStatus oss = noErr; + CFStringRef tmpStringRef = CFStringCreateWithCString(kCFAllocatorDefault, cStr, kCFStringEncodingUTF8); + if (tmpStringRef != NULL) { + if (CFStringGetCString(tmpStringRef, (char*)uniStr->unicode, sizeof(uniStr->unicode), kCFStringEncodingUnicode)) { + uniStr->length = CFStringGetLength(tmpStringRef); + } else { + oss = kCouldNotGetStringData; + } + CFRelease(tmpStringRef); + } else { + oss = kCouldNotCreateCFString; + } + return oss; +} +OSStatus SymlinkPathMakeRef(const UInt8 *path, FSRef *ref, Boolean *isDirectory) { + FSRef tmpFSRef; + char tmpPath[MAX_PATH]; + char *tmpNamePtr; + OSStatus oss; + + strcpy(tmpPath, (char *)path); + tmpNamePtr = strrchr(tmpPath, '/'); + if (*(tmpNamePtr + 1) == '\0') { + // Last character in the path is a '/'. + *tmpNamePtr = '\0'; + tmpNamePtr = strrchr(tmpPath, '/'); + } + *tmpNamePtr = '\0'; + tmpNamePtr++; + + // Get FSRef for parent directory. + oss = FSPathMakeRef((const UInt8 *)tmpPath, &tmpFSRef, NULL); + if (oss == noErr) { + HFSUniStr255 uniName; + oss = ConvertCStringToHFSUniStr(tmpNamePtr, &uniName); + if (oss == noErr) { + FSRef newFSRef; + oss = FSMakeFSRefUnicode(&tmpFSRef, uniName.length, uniName.unicode, kTextEncodingUnknown, &newFSRef); + tmpFSRef = newFSRef; + } + } + if (oss == noErr) { + *ref = tmpFSRef; + } + return oss; +} + +@implementation FileAttributes ++ (NSString *)errorDomain { + return @"FileAttributesErrorDomain"; +} +- (id)initWithPath:(NSString *)thePath stat:(struct stat *)theStat error:(NSError **)error { + if (self = [super init]) { + NSError *myError = nil; + NSDictionary *attribs = [[NSFileManager defaultManager] attributesOfItemAtPath:thePath error:&myError]; + if (attribs == nil) { + myError = [[[NSError alloc] initWithDomain:[FileAttributes errorDomain] + code:-1 + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:@"[FileAttributes attributesOfItemAtPath:%@]: %@", thePath, [myError localizedDescription]], NSLocalizedDescriptionKey, + myError, NSUnderlyingErrorKey, + nil]] autorelease]; + HSLogError(@"%@", myError); + SETERRORFROMMYERROR; + [self release]; + return nil; + } + isFileExtensionHidden = [[attribs objectForKey:NSFileExtensionHidden] boolValue]; + + targetExists = YES; + if (S_ISLNK(theStat->st_mode)) { + struct stat targetSt; + int ret = stat([thePath fileSystemRepresentation], &targetSt); + if (ret == -1 && errno == ENOENT) { + targetExists = NO; + } + } + if (targetExists) { + FSRef fsRef; + Boolean isDirectory = false; + OSStatus oss = 0; + if (S_ISLNK(theStat->st_mode)) { + oss = SymlinkPathMakeRef((UInt8*)[thePath fileSystemRepresentation], &fsRef, &isDirectory); + } else { + oss = FSPathMakeRef((UInt8*)[thePath fileSystemRepresentation], &fsRef, &isDirectory); + } + if (oss == bdNamErr) { + HSLogInfo(@"skipping finder flags for %@: %@", thePath, [OSStatusDescription descriptionForOSStatus:oss]); + } else if (oss == ioErr) { + HSLogInfo(@"skipping finder flags for %@: %@", thePath, [OSStatusDescription descriptionForOSStatus:oss]); + } else if (oss != noErr) { + HSLogError(@"error making FSRef for %@: %@", thePath, [OSStatusDescription descriptionForOSStatus:oss]); + SETNSERROR([FileAttributes errorDomain], oss, @"error making FSRef for %@: %@", thePath, [OSStatusDescription descriptionForOSStatus:oss]); + [self release]; + self = nil; + return self; + } else { + FSCatalogInfo catalogInfo; + OSErr oserr = FSGetCatalogInfo(&fsRef, kFSCatInfoCreateDate | kFSCatInfoFinderInfo | kFSCatInfoFinderXInfo, &catalogInfo, NULL, NULL, NULL); + if (oserr) { + HSLogError(@"FSGetCatalogInfo(%@): %@", thePath, [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + SETNSERROR([FileAttributes errorDomain], oss, @"FSGetCatalogInfo(%@): %@", thePath, [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + [self release]; + self = nil; + return self; + } + + CFTimeInterval theCreateTime; // double: seconds since reference date + if (UCConvertUTCDateTimeToCFAbsoluteTime(&catalogInfo.createDate, &theCreateTime) != noErr) { + HSLogError(@"error converting create time to CFAbsoluteTime"); + } else { + createTime.tv_sec = (__darwin_time_t)(theCreateTime + NSTimeIntervalSince1970); + CFTimeInterval subsecond = theCreateTime - (double)((int64_t)theCreateTime); + createTime.tv_nsec = (__darwin_time_t)(subsecond * 1000000000.0); + } + + finderFlags = 0; + extendedFinderFlags = 0; + if (isDirectory) { + FolderInfo *folderInfo = (FolderInfo *)&catalogInfo.finderInfo; + finderFlags = folderInfo->finderFlags; + ExtendedFolderInfo *extFolderInfo = (ExtendedFolderInfo *)&catalogInfo.extFinderInfo; + extendedFinderFlags = extFolderInfo->extendedFinderFlags; + finderFileType = [[NSString alloc] initWithString:@""]; + finderFileCreator = [[NSString alloc] initWithString:@""]; + } else { + FileInfo *fileInfo = (FileInfo *)&catalogInfo.finderInfo; + finderFlags = fileInfo->finderFlags; + ExtendedFileInfo *extFileInfo = (ExtendedFileInfo *)&catalogInfo.extFinderInfo; + extendedFinderFlags = extFileInfo->extendedFinderFlags; + + char fileType[5]; + fileType[0] = *((const char *)&fileInfo->fileType + 3); + fileType[1] = *((const char *)&fileInfo->fileType + 2); + fileType[2] = *((const char *)&fileInfo->fileType + 1); + fileType[3] = *((const char *)&fileInfo->fileType); + fileType[4] = 0; + finderFileType = [[NSString alloc] initWithCString:fileType encoding:NSUTF8StringEncoding]; + char fileCreator[5]; + fileCreator[0] = *((const char *)&fileInfo->fileCreator + 3); + fileCreator[1] = *((const char *)&fileInfo->fileCreator + 2); + fileCreator[2] = *((const char *)&fileInfo->fileCreator + 1); + fileCreator[3] = *((const char *)&fileInfo->fileCreator); + fileCreator[4] = 0; + finderFileCreator = [[NSString alloc] initWithCString:fileCreator encoding:NSUTF8StringEncoding]; + } + } + } + } + return self; +} +- (void)dealloc { + [finderFileType release]; + [finderFileCreator release]; + [super dealloc]; +} +- (int)finderFlags { + return finderFlags; +} +- (int)extendedFinderFlags { + return extendedFinderFlags; +} +- (NSString *)finderFileType { + return finderFileType; +} +- (NSString *)finderFileCreator { + return finderFileCreator; +} +- (int64_t)createTime_sec { + return createTime.tv_sec; +} +- (int64_t)createTime_nsec { + return createTime.tv_nsec; +} +- (BOOL)isFileExtensionHidden { + return isFileExtensionHidden; +} + ++ (BOOL)applyFinderFileType:(NSString *)fft finderFileCreator:(NSString *)ffc to:(FSRef *)fsRef error:(NSError **)error { + if ([fft length] != 4) { + HSLogTrace(@"not applying finder file type '%@': invalid length (must be 4 characters)", fft); + } else if ([ffc length] != 4) { + HSLogTrace(@"not applying finder file type '%@': invalid length (must be 4 characters)", ffc); + } else { + FSCatalogInfo catalogInfo; + OSErr oserr = FSGetCatalogInfo(fsRef, kFSCatInfoFinderInfo, &catalogInfo, NULL, NULL, NULL); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + FileInfo *fileInfo = (FileInfo *)&catalogInfo.finderInfo; + const char *fileType = [fft UTF8String]; + char *destFileType = (char *)&fileInfo->fileType; + destFileType[3] = fileType[0]; + destFileType[2] = fileType[1]; + destFileType[1] = fileType[2]; + destFileType[0] = fileType[3]; + + const char *fileCreator = [ffc UTF8String]; + char *destFileCreator = (char *)&fileInfo->fileCreator; + destFileCreator[3] = fileCreator[0]; + destFileCreator[2] = fileCreator[1]; + destFileCreator[1] = fileCreator[2]; + destFileCreator[0] = fileCreator[3]; + + oserr = FSSetCatalogInfo(fsRef, kFSCatInfoFinderInfo, &catalogInfo); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + } + return YES; +} ++ (BOOL)applyFlags:(unsigned long)flags toPath:(NSString *)thePath error:(NSError **)error { + HSLogTrace(@"chflags(%@, %ld)", thePath, flags); + if (chflags([thePath fileSystemRepresentation], (unsigned int)flags) == -1) { + int errnum = errno; + HSLogError(@"chflags(%@, %ld) error %d: %s", thePath, flags, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"error changing flags of %@: %s", thePath, strerror(errnum)); + return NO; + } + return YES; +} ++ (BOOL)applyFinderFlags:(int)ff to:(FSRef *)fsRef isDirectory:(BOOL)isDirectory error:(NSError **)error { + FSCatalogInfo catalogInfo; + OSErr oserr = FSGetCatalogInfo(fsRef, kFSCatInfoFinderInfo, &catalogInfo, NULL, NULL, NULL); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + if (isDirectory) { + FolderInfo *folderInfo = (FolderInfo *)&catalogInfo.finderInfo; + folderInfo->finderFlags = ff; + } else { + FileInfo *fileInfo = (FileInfo *)&catalogInfo.finderInfo; + fileInfo->finderFlags = ff; + } + oserr = FSSetCatalogInfo(fsRef, kFSCatInfoFinderInfo, &catalogInfo); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + return YES; +} ++ (BOOL)applyExtendedFinderFlags:(int)eff to:(FSRef *)fsRef isDirectory:(BOOL)isDirectory error:(NSError **)error { + FSCatalogInfo catalogInfo; + OSErr oserr = FSGetCatalogInfo(fsRef, kFSCatInfoFinderXInfo, &catalogInfo, NULL, NULL, NULL); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + if (isDirectory) { + ExtendedFolderInfo *extFolderInfo = (ExtendedFolderInfo *)&catalogInfo.extFinderInfo; + extFolderInfo->extendedFinderFlags = eff; + } else { + ExtendedFileInfo *extFileInfo = (ExtendedFileInfo *)&catalogInfo.extFinderInfo; + extFileInfo->extendedFinderFlags = eff; + } + oserr = FSSetCatalogInfo(fsRef, kFSCatInfoFinderXInfo, &catalogInfo); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + return YES; +} ++ (BOOL)applyFileExtensionHidden:(BOOL)hidden toPath:(NSString *)thePath error:(NSError **)error { + NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:hidden], NSFileExtensionHidden, nil]; + return [[NSFileManager defaultManager] setAttributes:attribs ofItemAtPath:thePath error:error]; +} ++ (BOOL)applyUID:(int)uid gid:(int)gid toPath:(NSString *)thePath error:(NSError **)error { + HSLogDebug(@"chown(%@, %d, %d)", thePath, uid, gid); + if (lchown([thePath fileSystemRepresentation], uid, gid) == -1) { + int errnum = errno; + HSLogError(@"lchown(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"error changing ownership of %@: %s", thePath, strerror(errnum)); + return NO; + } + HSLogDebug(@"lchown(%@, %d, %d); euid=%d", thePath, uid, gid, geteuid()); + return YES; +} ++ (BOOL)applyMode:(int)mode toPath:(NSString *)thePath isDirectory:(BOOL)isDirectory error:(NSError **)error { + if (isDirectory) { + int ret = chmod([thePath fileSystemRepresentation], mode); + if (ret == -1) { + int errnum = errno; + HSLogError(@"chmod(%@, %d) error %d: %s", thePath, mode, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to set permissions on %@: %s", thePath, strerror(errnum)); + return NO; + } + HSLogDebug(@"chmod(%@, 0%6o)", thePath, mode); + } else { + int fd = open([thePath fileSystemRepresentation], O_RDWR|O_SYMLINK); + if (fd == -1) { + int errnum = errno; + HSLogError(@"open(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to open %@: %s", thePath, strerror(errnum)); + return NO; + } + int ret = fchmod(fd, mode); + close(fd); + if (ret == -1) { + int errnum = errno; + HSLogError(@"fchmod(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to set permissions on %@: %s", thePath, strerror(errnum)); + return NO; + } + HSLogDebug(@"fchmod(%@, 0%6o)", thePath, mode); + } + return YES; +} ++ (BOOL)applyMTimeSec:(int64_t)mtime_sec mTimeNSec:(int64_t)mtime_nsec toPath:(NSString *)thePath error:(NSError **)error { + struct timespec mtimeSpec = { (__darwin_time_t)mtime_sec, (__darwin_time_t)mtime_nsec }; + struct timeval atimeVal; + struct timeval mtimeVal; + TIMESPEC_TO_TIMEVAL(&atimeVal, &mtimeSpec); // Just use mtime because we don't have atime, nor do we care about atime. + TIMESPEC_TO_TIMEVAL(&mtimeVal, &mtimeSpec); + struct timeval timevals[2]; + timevals[0] = atimeVal; + timevals[1] = mtimeVal; + if (utimes([thePath fileSystemRepresentation], timevals) == -1) { + int errnum = errno; + HSLogError(@"utimes(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to set timestamps on %@: %s", thePath, strerror(errnum)); + return NO; + } + return YES; +} ++ (BOOL)applyCreateTimeSec:(int64_t)theCreateTime_sec createTimeNSec:(int64_t)theCreateTime_nsec to:(FSRef *)fsRef error:(NSError **)error { + FSCatalogInfo catalogInfo; + OSErr oserr = FSGetCatalogInfo(fsRef, kFSCatInfoCreateDate, &catalogInfo, NULL, NULL, NULL); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + CFTimeInterval theCreateTime = (double)theCreateTime_sec - NSTimeIntervalSince1970 + (double)theCreateTime_nsec / 1000000000.0; + if (UCConvertCFAbsoluteTimeToUTCDateTime(theCreateTime, &catalogInfo.createDate) != noErr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"unable to convert CFAbsoluteTime %f to UTCDateTime", theCreateTime); + return NO; + } + oserr = FSSetCatalogInfo(fsRef, kFSCatInfoCreateDate, &catalogInfo); + if (oserr) { + SETNSERROR(@"FileManagerErrorDomain", -1, @"%@", [OSStatusDescription descriptionForOSStatus:(OSStatus)oserr]); + return NO; + } + return YES; +} +@end diff --git a/repo/PackSet.m b/repo/PackSet.m index 1149a59..9af9a78 100644 --- a/repo/PackSet.m +++ b/repo/PackSet.m @@ -173,6 +173,8 @@ loadExistingMutablePackFiles:(BOOL)theLoadExistingMutablePackFiles { PackIndexEntry *pie = [packIndexEntriesByObjectSHA1 objectForKey:sha1]; if (pie != nil) { ret = [fark dataForPackIndexEntry:pie storageType:storageType error:error]; + } else { + SETNSERROR([self errorDomain], ERROR_NOT_FOUND, @"%@ not found in pack set", sha1); } return ret; } diff --git a/repo/Repo.m b/repo/Repo.m index 44bd2dd..5fcf69b 100644 --- a/repo/Repo.m +++ b/repo/Repo.m @@ -376,6 +376,7 @@ targetConnectionDelegate:(id)theTCD SETERRORFROMMYERROR; if (![myError isErrorWithDomain:[blobsPackSet errorDomain] code:ERROR_NOT_FOUND]) { // Return nil if not a not-found error. + SETERRORFROMMYERROR; return nil; } data = [fark dataForSHA1:[theBlobKey sha1] storageType:[theBlobKey storageType] error:&myError]; diff --git a/repo/XAttrSet.h b/repo/XAttrSet.h new file mode 100644 index 0000000..facc384 --- /dev/null +++ b/repo/XAttrSet.h @@ -0,0 +1,23 @@ +// +// XAttrSet.h +// Backup +// +// Created by Stefan Reitshamer on 4/27/09. +// Copyright 2009 PhotoMinds LLC. All rights reserved. +// + + +#import "BufferedInputStream.h" + +@interface XAttrSet : NSObject { + NSMutableDictionary *xattrs; + NSString *path; +} +- (id)initWithPath:(NSString *)thePath error:(NSError **)error; +- (id)initWithBufferedInputStream:(BufferedInputStream *)is error:(NSError **)error; +- (NSData *)toData; +- (NSUInteger)count; +- (unsigned long long)dataLength; +- (NSArray *)names; +- (BOOL)applyToFile:(NSString *)path error:(NSError **)error; +@end diff --git a/repo/XAttrSet.m b/repo/XAttrSet.m new file mode 100644 index 0000000..3ad17f3 --- /dev/null +++ b/repo/XAttrSet.m @@ -0,0 +1,204 @@ +// +// XAttrSet.m +// Backup +// +// Created by Stefan Reitshamer on 4/27/09. +// Copyright 2009 PhotoMinds LLC. All rights reserved. +// + +#include +#include +#import "XAttrSet.h" +#import "StringIO.h" +#import "DataIO.h" +#import "IntegerIO.h" +#import "DataInputStream.h" + + +#import "Streams.h" +#import "NSError_extra.h" +#import "BufferedInputStream.h" +#import "NSData-Gzip.h" + +#define HEADER_LENGTH (12) + +@interface XAttrSet (internal) +- (BOOL)loadFromPath:(NSString *)thePath error:(NSError **)error; +- (BOOL)loadFromInputStream:(BufferedInputStream *)is error:(NSError **)error; +@end + +@implementation XAttrSet +- (id)initWithPath:(NSString *)thePath error:(NSError **)error { + if (self = [super init]) { + xattrs = [[NSMutableDictionary alloc] init]; + NSError *myError = nil; + if (![self loadFromPath:thePath error:&myError]) { + if ([myError isErrorWithDomain:@"UnixErrorDomain" code:EPERM]) { + HSLogDebug(@"%@ doesn't support extended attributes; skipping", thePath); + } else { + if (error != NULL) { + *error = myError; + } + [self release]; + return nil; + } + } + path = [thePath retain]; + } + return self; +} +- (id)initWithBufferedInputStream:(BufferedInputStream *)is error:(NSError **)error { + if (self = [super init]) { + xattrs = [[NSMutableDictionary alloc] init]; + if (![self loadFromInputStream:is error:error]) { + [self release]; + self = nil; + } + } + return self; +} +- (void)dealloc { + [xattrs release]; + [path release]; + [super dealloc]; +} +- (NSData *)toData { + NSMutableData *mutableData = [[[NSMutableData alloc] init] autorelease]; + [mutableData appendBytes:"XAttrSetV002" length:HEADER_LENGTH]; + uint64_t count = (uint64_t)[xattrs count]; + [IntegerIO writeUInt64:count to:mutableData]; + for (NSString *name in [xattrs allKeys]) { + [StringIO write:name to:mutableData]; + [DataIO write:[xattrs objectForKey:name] to:mutableData]; + } + return mutableData; +} +- (NSUInteger)count { + return [xattrs count]; +} +- (unsigned long long)dataLength { + unsigned long long total = 0; + for (NSString *key in [xattrs allKeys]) { + NSData *value = [xattrs objectForKey:key]; + total += [value length]; + } + return total; +} +- (NSArray *)names { + return [xattrs allKeys]; +} +- (BOOL)applyToFile:(NSString *)thePath error:(NSError **)error { + XAttrSet *current = [[[XAttrSet alloc] initWithPath:thePath error:error] autorelease]; + if (!current) { + return NO; + } + const char *pathChars = [thePath fileSystemRepresentation]; + for (NSString *name in [current names]) { + if (removexattr(pathChars, [name UTF8String], XATTR_NOFOLLOW) == -1) { + int errnum = errno; + HSLogError(@"removexattr(%@, %@) error %d: %s", thePath, name, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to remove extended attribute %@ from %@: %s", name, thePath, strerror(errnum)); + return NO; + } + } + for (NSString *key in [xattrs allKeys]) { + NSData *value = [xattrs objectForKey:key]; + if (setxattr(pathChars, + [key UTF8String], + [value bytes], + [value length], + 0, + XATTR_NOFOLLOW) == -1) { + int errnum = errno; + HSLogError(@"setxattr(%@, %@) error %d: %s", thePath, key, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to set extended attribute %@ on %@: %s", key, thePath, strerror(errnum)); + return NO; + } + } + return YES; +} +@end + +@implementation XAttrSet (internal) +- (BOOL)loadFromPath:(NSString *)thePath error:(NSError **)error { + struct stat st; + if (lstat([thePath fileSystemRepresentation], &st) == -1) { + int errnum = errno; + HSLogError(@"lstat(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"xattr lstat(%@): %s", thePath, strerror(errnum)); + return NO; + } + if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)) { + const char *cpath = [thePath fileSystemRepresentation]; + ssize_t xattrsize = listxattr(cpath, NULL, 0, XATTR_NOFOLLOW); + if (xattrsize == -1) { + int errnum = errno; + HSLogError(@"listxattr(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to list extended attributes of %@: %s", thePath, strerror(errnum)); + return NO; + } + if (xattrsize > 0) { + char *xattrbuf = (char *)malloc(xattrsize); + xattrsize = listxattr(cpath, xattrbuf, xattrsize, XATTR_NOFOLLOW); + if (xattrsize == -1) { + int errnum = errno; + HSLogError(@"listxattr(%@) error %d: %s", thePath, errnum, strerror(errnum)); + SETNSERROR(@"UnixErrorDomain", errnum, @"failed to list extended attributes of %@: %s", thePath, strerror(errnum)); + free(xattrbuf); + return NO; + } + for (char *name = xattrbuf; name < (xattrbuf + xattrsize); name += strlen(name) + 1) { + NSString *theName = [NSString stringWithUTF8String:name]; + ssize_t valuesize = getxattr(cpath, name, NULL, 0, 0, XATTR_NOFOLLOW); + if (valuesize == -1) { + int errnum = errno; + HSLogError(@"skipping xattrs %s of %s: error %d: %s", name, cpath, errnum, strerror(errnum)); + } else if (valuesize > 0) { + void *value = malloc(valuesize); + if (getxattr(cpath, name, value, valuesize, 0, XATTR_NOFOLLOW) == -1) { + int errnum = errno; + HSLogError(@"skipping xattrs %s of %s: error %d: %s", name, cpath, errnum, strerror(errnum)); + } else { + [xattrs setObject:[NSData dataWithBytes:value length:valuesize] forKey:theName]; + } + free(value); + } else { + [xattrs setObject:[NSData data] forKey:theName]; + } + } + free(xattrbuf); + } + } + return YES; +} +- (BOOL)loadFromInputStream:(BufferedInputStream *)is error:(NSError **)error { + BOOL ret = NO; + unsigned char *buf = (unsigned char *)malloc(HEADER_LENGTH); + if (![is readExactly:HEADER_LENGTH into:buf error:error]) { + goto load_error; + } + if (strncmp((const char *)buf, "XAttrSetV002", HEADER_LENGTH)) { + SETNSERROR(@"XAttrSetErrorDomain", ERROR_INVALID_OBJECT_VERSION, @"invalid XAttrSet header"); + goto load_error; + } + uint64_t count; + if (![IntegerIO readUInt64:&count from:is error:error]) { + goto load_error; + } + for (uint64_t i = 0; i < count; i++) { + NSString *name; + if (![StringIO read:&name from:is error:error]) { + goto load_error; + } + NSData *value; + if (![DataIO read:&value from:is error:error]) { + goto load_error; + } + [xattrs setObject:value forKey:name]; + } + ret = YES; +load_error: + free(buf); + return ret; +} +@end diff --git a/s3restore/S3Restorer.h b/s3restore/S3Restorer.h new file mode 100644 index 0000000..11394a7 --- /dev/null +++ b/s3restore/S3Restorer.h @@ -0,0 +1,43 @@ +// +// S3Restorer.h +// Arq +// +// Created by Stefan Reitshamer on 5/28/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "StorageType.h" +#import "Restorer.h" +#import "TargetConnection.h" +@protocol S3RestorerDelegate; +@class S3RestorerParamSet; +@class Repo; +@class Commit; +@class Tree; +@class BlobKey; + + +@interface S3Restorer : NSObject { + S3RestorerParamSet *paramSet; + id delegate; + + NSString *skipFilesRoot; + + NSMutableArray *calculateItems; + NSMutableArray *restoreItems; + NSMutableDictionary *hardlinks; + + Repo *repo; + Commit *commit; + NSString *commitDescription; + Tree *rootTree; + + unsigned long long bytesTransferred; + unsigned long long totalBytesToTransfer; + + unsigned long long writtenToCurrentFile; +} +- (id)initWithParamSet:(S3RestorerParamSet *)theParamSet + delegate:(id )theDelegate; + +@end diff --git a/s3restore/S3Restorer.m b/s3restore/S3Restorer.m new file mode 100644 index 0000000..54a199b --- /dev/null +++ b/s3restore/S3Restorer.m @@ -0,0 +1,297 @@ +// +// S3Restorer.m +// Arq +// +// Created by Stefan Reitshamer on 5/28/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "S3Restorer.h" +#import "S3RestorerDelegate.h" +#import "ArqSalt.h" +#import "Repo.h" +#import "S3AuthorizationProvider.h" +#import "S3Service.h" +#import "Tree.h" +#import "Node.h" +#import "FileOutputStream.h" +#import "Commit.h" +#import "BlobKey.h" +#import "NSFileManager_extra.h" +#import "NSData-GZip.h" +#import "BufferedOutputStream.h" +#import "OSStatusDescription.h" +#import "FileAttributes.h" +#import "FileACL.h" +#import "DataInputStream.h" +#import "XAttrSet.h" +#import "FileInputStream.h" +#import "SHA1Hash.h" +#import "S3RestorerParamSet.h" +#import "RestoreItem.h" +#import "UserLibrary_Arq.h" +#import "CalculateItem.h" + + +@implementation S3Restorer +- (id)initWithParamSet:(S3RestorerParamSet *)theParamSet + delegate:(id )theDelegate { + if (self = [super init]) { + paramSet = [theParamSet retain]; + delegate = theDelegate; // Don't retain it. + + skipFilesRoot = [[[UserLibrary arqUserLibraryPath] stringByAppendingFormat:@"/RestoreJobSkipFiles/%f", [NSDate timeIntervalSinceReferenceDate]] retain]; + + calculateItems = [[NSMutableArray alloc] init]; + restoreItems = [[NSMutableArray alloc] init]; + hardlinks = [[NSMutableDictionary alloc] init]; + + [self run]; + } + return self; +} +- (void)dealloc { + [paramSet release]; + + [skipFilesRoot release]; + + [calculateItems release]; + [restoreItems release]; + [hardlinks release]; + [repo release]; + [super dealloc]; +} +- (void)run { + NSError *myError = nil; + if (![self run:&myError]) { + [delegate s3RestorerDidFail:myError]; + } else { + [delegate s3RestorerDidSucceed]; + } +} +- (BOOL)requestBlobKey:(BlobKey *)theBlobKey error:(NSError **)error { + // Not relevant for S3. + return YES; +} +- (NSNumber *)isObjectAvailableForBlobKey:(BlobKey *)theBlobKey error:(NSError **)error { + return [NSNumber numberWithBool:YES]; +} +- (NSNumber *)sizeOfBlob:(BlobKey *)theBlobKey error:(NSError **)error { + unsigned long long size = 0; + NSNumber *contains = [repo containsBlobForBlobKey:theBlobKey dataSize:&size error:error]; + if (contains == nil) { + return nil; + } + if (![contains boolValue]) { + SETNSERROR([self errorDomain], ERROR_NOT_FOUND, @"size of blob %@ not found because blob not found", theBlobKey); + return NO; + } + return [NSNumber numberWithUnsignedLongLong:size]; +} +- (NSData *)dataForBlobKey:(BlobKey *)theBlobKey error:(NSError **)error { + // Because of a bug in Arq pre-4.4, Arq might have created Buckets for non-AWS S3-compatible destinations with a storage type of StorageTypeS3Glacier. + // So, we could be here and theBlobKey's storage type could be StorageTypeS3Glacier, which is OK because the Repo will just put "glacier/" in the path + // and restoring will work fine. + + NSData *ret = [repo dataForBlobKey:theBlobKey error:error]; + if (ret == nil) { + return nil; + } + if (![self addToBytesTransferred:[ret length] error:error]) { + return nil; + } + return ret; +} +- (BOOL)shouldSkipFile:(NSString *)thePath { + NSString *skipFilePath = [skipFilesRoot stringByAppendingString:thePath]; + return [[NSFileManager defaultManager] fileExistsAtPath:skipFilePath]; +} +- (BOOL)useTargetUIDAndGID { + return paramSet.useTargetUIDAndGID; +} +- (uid_t)targetUID { + return paramSet.targetUID; +} +- (gid_t)targetGID { + return paramSet.targetGID; +} + + +#pragma mark TargetConnectionDelegate +- (BOOL)targetConnectionShouldRetryOnTransientError:(NSError **)error { + return YES; +} + + +#pragma mark internal +- (NSString *)errorDomain { + return @"S3RestorerErrorDomain"; +} + +- (BOOL)run:(NSError **)error { + if (![self setUp:error]) { + return NO; + } + + if ([delegate s3RestorerMessageDidChange:@"Calculating sizes"]) { + SETNSERROR([self errorDomain], ERROR_ABORT_REQUESTED, @"cancel requested"); + return NO; + } + if (![self calculateSizes:error]) { + return NO; + } + + if ([delegate s3RestorerMessageDidChange:[NSString stringWithFormat:@"Restoring %@ from %@ to %@", paramSet.rootItemName, commitDescription, paramSet.destinationPath]]) { + SETNSERROR([self errorDomain], ERROR_ABORT_REQUESTED, @"cancel requested"); + return NO; + } + + NSAutoreleasePool *pool = nil; + BOOL ret = YES; + while ([restoreItems count] > 0) { + [pool drain]; + pool = [[NSAutoreleasePool alloc] init]; + + RestoreItem *restoreItem = [restoreItems objectAtIndex:0]; + NSError *restoreError = nil; + if (![restoreItem restoreWithHardlinks:hardlinks restorer:self error:&restoreError]) { + if ([restoreError isErrorWithDomain:[self errorDomain] code:ERROR_ABORT_REQUESTED]) { + if (error != NULL) { + *error = restoreError; + } + ret = NO; + break; + } else { + [delegate s3RestorerErrorMessage:[restoreError localizedDescription] didOccurForPath:[restoreItem path]]; + } + } + NSArray *nextItems = [restoreItem nextItemsWithRepo:repo error:error]; + if (nextItems == nil) { + ret = NO; + break; + } + [restoreItems removeObjectAtIndex:0]; + if ([nextItems count] > 0) { + [restoreItems insertObjects:nextItems atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [nextItems count])]]; + } + } + if (!ret && error != NULL) { + [*error retain]; + } + [pool drain]; + if (!ret && error != NULL) { + [*error autorelease]; + } + + return ret; +} +- (BOOL)setUp:(NSError **)error { + repo = [[Repo alloc] initWithBucket:[paramSet bucket] + encryptionPassword:[paramSet encryptionPassword] + targetUID:[paramSet targetUID] + targetGID:[paramSet targetGID] + loadExistingMutablePackFiles:NO + targetConnectionDelegate:self + repoDelegate:nil + error:error]; + if (repo == nil) { + return NO; + } + commit = [[repo commitForBlobKey:paramSet.commitBlobKey error:error] retain]; + if (commit == nil) { + return NO; + } + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setDateStyle:NSDateFormatterMediumStyle]; + [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; + commitDescription = [[dateFormatter stringFromDate:[commit creationDate]] retain]; + + rootTree = [[repo treeForBlobKey:paramSet.treeBlobKey error:error] retain]; + if (rootTree == nil) { + return NO; + } + + unsigned long long total = 0; + if (paramSet.nodeName != nil) { + // Individual file. + Node *node = [rootTree childNodeWithName:paramSet.nodeName]; + if ([[rootTree childNodeNames] isEqualToArray:[NSArray arrayWithObject:@"."]]) { + // The single-file case. + node = [rootTree childNodeWithName:@"."]; + } + NSAssert(node != nil, @"node may not be nil"); + total = [node uncompressedDataSize]; + [calculateItems addObject:[[[CalculateItem alloc] initWithPath:paramSet.destinationPath node:node] autorelease]]; + [restoreItems addObject:[[[RestoreItem alloc] initWithPath:paramSet.destinationPath tree:rootTree node:node] autorelease]]; + } else { + // Tree. + total = [rootTree aggregateUncompressedDataSize]; + [calculateItems addObject:[[[CalculateItem alloc] initWithPath:paramSet.destinationPath tree:rootTree] autorelease]]; + [restoreItems addObject:[[[RestoreItem alloc] initWithPath:paramSet.destinationPath tree:rootTree] autorelease]]; + } + if (![self addToTotalBytesToTransfer:total error:error]) { + return NO; + } + + return YES; +} +- (BOOL)calculateSizes:(NSError **)error { + BOOL ret = YES; + NSAutoreleasePool *pool = nil; + while ([calculateItems count] > 0) { + [pool drain]; + pool = [[NSAutoreleasePool alloc] init]; + + CalculateItem *item = [calculateItems objectAtIndex:0]; + if (![item calculateWithRepo:repo restorer:self error:error]) { + ret = NO; + break; + } + for (NSString *path in [item filesToSkip]) { + [self skipFile:path]; + } + [calculateItems removeObjectAtIndex:0]; + NSArray *nextItems = [item nextItems]; + if ([nextItems count] > 0) { + [calculateItems insertObjects:nextItems atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [nextItems count])]]; + } + } + if (!ret && error != NULL) { + [*error retain]; + } + [pool drain]; + if (!ret && error != NULL) { + [*error autorelease]; + } + + return ret; +} +- (void)skipFile:(NSString *)thePath { + NSString *skipFilePath = [skipFilesRoot stringByAppendingString:thePath]; + NSError *myError = nil; + if (![[NSFileManager defaultManager] ensureParentPathExistsForPath:skipFilePath targetUID:paramSet.targetUID targetGID:paramSet.targetGID error:&myError]) { + HSLogError(@"error creating parent dir for %@: %@", skipFilePath, myError); + return; + } + if (![[NSFileManager defaultManager] touchFileAtPath:skipFilePath targetUID:paramSet.targetUID targetGID:paramSet.targetGID error:&myError]) { + HSLogError(@"error touching %@: %@", skipFilePath, myError); + } + HSLogDebug(@"skip file %@", thePath); +} +- (BOOL)addToBytesTransferred:(unsigned long long)length error:(NSError **)error { + bytesTransferred += length; + if ([delegate s3RestorerBytesTransferredDidChange:[NSNumber numberWithUnsignedLongLong:bytesTransferred]]) { + SETNSERROR([self errorDomain], ERROR_ABORT_REQUESTED, @"cancel requested"); + return NO; + } + return YES; +} +- (BOOL)addToTotalBytesToTransfer:(unsigned long long)length error:(NSError **)error { + totalBytesToTransfer += length; + if ([delegate s3RestorerTotalBytesToTransferDidChange:[NSNumber numberWithUnsignedLongLong:totalBytesToTransfer]]) { + SETNSERROR([self errorDomain], ERROR_ABORT_REQUESTED, @"cancel requested"); + return NO; + } + return YES; +} +@end diff --git a/s3restore/S3RestorerDelegate.h b/s3restore/S3RestorerDelegate.h new file mode 100644 index 0000000..5f6d41e --- /dev/null +++ b/s3restore/S3RestorerDelegate.h @@ -0,0 +1,22 @@ +// +// S3RestorerDelegate.h +// Arq +// +// Created by Stefan Reitshamer on 5/28/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +@protocol S3RestorerDelegate + +// Methods return YES if cancel is requested. + +- (BOOL)s3RestorerMessageDidChange:(NSString *)message; + +- (BOOL)s3RestorerBytesTransferredDidChange:(NSNumber *)theTransferred; +- (BOOL)s3RestorerTotalBytesToTransferDidChange:(NSNumber *)theTotal; + +- (BOOL)s3RestorerErrorMessage:(NSString *)theErrorMessage didOccurForPath:(NSString *)thePath; + +- (BOOL)s3RestorerDidSucceed; +- (BOOL)s3RestorerDidFail:(NSError *)error; +@end diff --git a/s3restore/S3RestorerParamSet.h b/s3restore/S3RestorerParamSet.h new file mode 100644 index 0000000..20fb163 --- /dev/null +++ b/s3restore/S3RestorerParamSet.h @@ -0,0 +1,59 @@ +// +// S3RestorerParamSet.h +// Arq +// +// Created by Stefan Reitshamer on 5/28/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "StorageType.h" +@class Bucket; +@class BufferedInputStream; +@class BufferedOutputStream; +@class AWSRegion; +@class BlobKey; + +@interface S3RestorerParamSet : NSObject { + Bucket *bucket; + NSString *encryptionPassword; + BlobKey *commitBlobKey; + NSString *rootItemName; + int treeVersion; + BOOL treeIsCompressed; + BlobKey *treeBlobKey; + NSString *nodeName; + uid_t targetUID; + gid_t targetGID; + BOOL useTargetUIDAndGID; + NSString *destinationPath; + int logLevel; +} + +- (id)initWithBucket:(Bucket *)theBucket + encryptionPassword:(NSString *)theEncryptionPassword + commitBlobKey:(BlobKey *)theCommitBlobKey + rootItemName:(NSString *)theRootItemName + treeVersion:(int32_t)theTreeVersion + treeIsCompressed:(BOOL)theTreeIsCompressed + treeBlobKey:(BlobKey *)theTreeBlobKey + nodeName:(NSString *)theNodeName + targetUID:(uid_t)theTargetUID + targetGID:(gid_t)theTargetGID + useTargetUIDAndGID:(BOOL)theUseTargetUIDAndGID + destinationPath:(NSString *)theDestination + logLevel:(int)theLogLevel; + +@property (readonly, retain) NSString *arqs3restorerPath; +@property (readonly, retain) Bucket *bucket; +@property (readonly, retain) NSString *encryptionPassword; +@property (readonly, retain) BlobKey *commitBlobKey; +@property (readonly, retain) NSString *rootItemName; +@property (readonly, retain) BlobKey *treeBlobKey; +@property (readonly, retain) NSString *nodeName; +@property (readonly) uid_t targetUID; +@property (readonly) gid_t targetGID; +@property (readonly) BOOL useTargetUIDAndGID; +@property (readonly, retain) NSString *destinationPath; +@property (readonly) int logLevel; + +@end diff --git a/s3restore/S3RestorerParamSet.m b/s3restore/S3RestorerParamSet.m new file mode 100644 index 0000000..454f9c5 --- /dev/null +++ b/s3restore/S3RestorerParamSet.m @@ -0,0 +1,75 @@ +// +// S3RestorerParamSet.m +// Arq +// +// Created by Stefan Reitshamer on 5/28/13. +// Copyright (c) 2013 Stefan Reitshamer. All rights reserved. +// + +#import "S3RestorerParamSet.h" +#import "AWSRegion.h" +#import "BlobKey.h" +#import "StringIO.h" +#import "IntegerIO.h" +#import "BlobKeyIO.h" +#import "BooleanIO.h" +#import "DataIO.h" +#import "Tree.h" +#import "Bucket.h" + + +@implementation S3RestorerParamSet +@synthesize bucket; +@synthesize encryptionPassword; +@synthesize commitBlobKey; +@synthesize rootItemName; +@synthesize treeBlobKey; +@synthesize nodeName; +@synthesize targetUID; +@synthesize targetGID; +@synthesize useTargetUIDAndGID; +@synthesize destinationPath; +@synthesize logLevel; + + +- (id)initWithBucket:(Bucket *)theBucket + encryptionPassword:(NSString *)theEncryptionPassword + commitBlobKey:(BlobKey *)theCommitBlobKey + rootItemName:(NSString *)theRootItemName + treeVersion:(int32_t)theTreeVersion + treeIsCompressed:(BOOL)theTreeIsCompressed + treeBlobKey:(BlobKey *)theTreeBlobKey + nodeName:(NSString *)theNodeName + targetUID:(uid_t)theTargetUID + targetGID:(gid_t)theTargetGID + useTargetUIDAndGID:(BOOL)theUseTargetUIDAndGID + destinationPath:(NSString *)theDestination + logLevel:(int)theLogLevel { + if (self = [super init]) { + bucket = [theBucket retain]; + encryptionPassword = [theEncryptionPassword retain]; + commitBlobKey = [theCommitBlobKey retain]; + rootItemName = [theRootItemName retain]; + treeVersion = theTreeVersion; + treeIsCompressed = theTreeIsCompressed; + treeBlobKey = [theTreeBlobKey retain]; + nodeName = [theNodeName retain]; + targetUID = theTargetUID; + targetGID = theTargetGID; + useTargetUIDAndGID = theUseTargetUIDAndGID; + destinationPath = [theDestination retain]; + logLevel = theLogLevel; + } + return self; +} +- (void)dealloc { + [bucket release]; + [encryptionPassword release]; + [commitBlobKey release]; + [rootItemName release]; + [treeBlobKey release]; + [nodeName release]; + [destinationPath release]; + [super dealloc]; +} +@end