// // ArqRestoreCommand.m // arq_restore // // Created by Stefan Reitshamer on 7/25/14. // // #import "ArqRestoreCommand.h" #import "Target.h" #import "AWSRegion.h" #import "BackupSet.h" #import "S3Service.h" #import "UserAndComputer.h" #import "Bucket.h" #import "Repo.h" #import "S3RestorerParamSet.h" #import "Tree.h" #import "Commit.h" #import "BlobKey.h" #import "S3Restorer.h" #import "S3GlacierRestorerParamSet.h" #import "S3GlacierRestorer.h" #import "GlacierRestorerParamSet.h" #import "GlacierRestorer.h" #import "S3AuthorizationProvider.h" @implementation ArqRestoreCommand - (void)dealloc { [target release]; [super dealloc]; } - (NSString *)errorDomain { return @"ArqRestoreCommandErrorDomain"; } - (BOOL)executeWithArgc:(int)argc argv:(const char **)argv error:(NSError **)error { NSMutableArray *args = [NSMutableArray array]; for (int i = 0; i < argc; i++) { [args addObject:[[[NSString alloc] initWithBytes:argv[i] length:strlen(argv[i]) encoding:NSUTF8StringEncoding] autorelease]]; } if ([args count] < 2) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments"); return NO; } int index = 1; if ([[args objectAtIndex:1] isEqualToString:@"-l"]) { if ([args count] < 4) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments"); return NO; } setHSLogLevel(hsLogLevelForName([args objectAtIndex:2])); index += 2; } NSString *cmd = [args objectAtIndex:index]; int targetParamsIndex = index + 1; if ([cmd isEqualToString:@"listcomputers"]) { // Valid command, but no additional args. } else if ([cmd isEqualToString:@"listfolders"]) { if ((argc - targetParamsIndex) < 2) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments for listfolders command"); return NO; } targetParamsIndex += 2; } else if ([cmd isEqualToString:@"restore"]) { if ((argc - targetParamsIndex) < 4) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing arguments"); return NO; } targetParamsIndex += 4; } else { SETNSERROR([self errorDomain], ERROR_USAGE, @"unknown command: %@", cmd); return NO; } if (targetParamsIndex >= argc) { SETNSERROR([self errorDomain], ERROR_USAGE, @"missing target type params"); return NO; } target = [[self targetForParams:[args subarrayWithRange:NSMakeRange(targetParamsIndex, argc - targetParamsIndex)] error:error] retain]; if (target == nil) { return NO; } if ([cmd isEqualToString:@"listcomputers"]) { if (![self listComputers:error]) { return NO; } } else if ([cmd isEqualToString:@"listfolders"]) { 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:index+1] bucketUUID:[args objectAtIndex:index+3] encryptionPassword:[args objectAtIndex:index+2] restoreBytesPerSecond:[args objectAtIndex:index+3] error:error]) { return NO; } } else { SETNSERROR([self errorDomain], ERROR_USAGE, @"unknown command: %@", cmd); return NO; } return YES; } #pragma mark internal - (Target *)targetForParams:(NSArray *)theParams error:(NSError **)error { NSString *theTargetType = [theParams objectAtIndex:0]; Target *ret = nil; if ([theTargetType isEqualToString:@"aws"]) { if ([theParams count] != 4) { SETNSERROR([self errorDomain], ERROR_USAGE, @"invalid aws parameters"); return nil; } NSString *theAccessKey = [theParams objectAtIndex:1]; NSString *theSecretKey = [theParams objectAtIndex:2]; NSString *theBucketName = [theParams objectAtIndex:3]; AWSRegion *awsRegion = [self awsRegionForAccessKey:theAccessKey secretKey:theSecretKey bucketName:theBucketName error:error]; if (awsRegion == nil) { return nil; } NSURL *s3Endpoint = [awsRegion s3EndpointWithSSL:YES]; int port = [[s3Endpoint port] intValue]; NSString *portString = @""; if (port != 0) { portString = [NSString stringWithFormat:@":%d", port]; } NSURL *targetEndpoint = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@@%@%@/%@", [s3Endpoint scheme], theAccessKey, [s3Endpoint host], portString, theBucketName]]; ret = [[[Target alloc] initWithEndpoint:targetEndpoint secret:theSecretKey passphrase:nil] autorelease]; } else if ([theTargetType isEqualToString:@"sftp"]) { if ([theParams count] != 6 && [theParams count] != 7) { SETNSERROR([self errorDomain], ERROR_USAGE, @"invalid sftp parameters"); return nil; } NSString *hostname = [theParams objectAtIndex:1]; int port = [[theParams objectAtIndex:2] intValue]; NSString *path = [theParams objectAtIndex:3]; NSString *username = [theParams objectAtIndex:4]; NSString *secret = [theParams objectAtIndex:5]; NSString *keyfilePassphrase = [theParams count] > 6 ? [theParams objectAtIndex:6] : nil; if (![path hasPrefix:@"/"]) { path = [@"/~/" stringByAppendingString:path]; } NSString *escapedPath = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)path, NULL, (CFStringRef)@"!*'();:@&=+$,?%#[]", kCFStringEncodingUTF8); NSString *escapedUsername = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)username, NULL, (CFStringRef)@"!*'();:@&=+$,?%#[]", kCFStringEncodingUTF8); NSURL *endpoint = [NSURL URLWithString:[NSString stringWithFormat:@"sftp://%@@%@:%d%@", escapedUsername, hostname, port, escapedPath]]; ret = [[[Target alloc] initWithEndpoint:endpoint secret:secret passphrase:keyfilePassphrase] autorelease]; } else if ([theTargetType isEqualToString:@"greenqloud"] || [theTargetType isEqualToString:@"dreamobjects"] || [theTargetType isEqualToString:@"googlecloudstorage"] || [theTargetType isEqualToString:@"s3compatible"]) { if ([theParams count] != 4) { SETNSERROR([self errorDomain], ERROR_USAGE, @"invalid %@ parameters", theTargetType); return nil; } NSString *theAccessKey = [theParams objectAtIndex:1]; NSString *theSecretKey = [theParams objectAtIndex:2]; NSString *theBucketName = [theParams objectAtIndex:3]; NSString *theHostname = nil; if ([theTargetType isEqualToString:@"greenqloud"]) { theHostname = @"s.greenqloud.com"; } else if ([theTargetType isEqualToString:@"dreamobjects"]) { theHostname = @"objects.dreamhost.com"; } else if ([theTargetType isEqualToString:@"googlecloudstorage"]) { theHostname = @"storage.googleapis.com"; } else { SETNSERROR([self errorDomain], ERROR_USAGE, @"no hostname for target type: %@", theTargetType); return nil; } NSURL *endpoint = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@@%@/%@", theAccessKey, theHostname, theBucketName]]; ret = [[[Target alloc] initWithEndpoint:endpoint secret:theSecretKey passphrase:nil] autorelease]; } else if ([theTargetType isEqualToString:@"googledrive"]) { if ([theParams count] != 3) { SETNSERROR([self errorDomain], ERROR_USAGE, @"invalid googledrive parameters"); return nil; } NSString *theRefreshToken = [theParams objectAtIndex:1]; NSString *thePath = [theParams objectAtIndex:2]; NSString *escapedPath = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)thePath, CFSTR("/"), CFSTR("@?=&+"), kCFStringEncodingUTF8); [escapedPath autorelease]; NSURL *endpoint = [NSURL URLWithString:[NSString stringWithFormat:@"googledrive://unknown_email_address@www.googleapis.com%@", escapedPath]]; ret = [[[Target alloc] initWithEndpoint:endpoint secret:theRefreshToken passphrase:nil] autorelease]; } else { SETNSERROR([self errorDomain], ERROR_USAGE, @"unknown target type: %@", theTargetType); return nil; } return ret; } - (AWSRegion *)awsRegionForAccessKey:(NSString *)theAccessKey secretKey:(NSString *)theSecretKey bucketName:(NSString *)theBucketName error:(NSError **)error { S3AuthorizationProvider *sap = [[[S3AuthorizationProvider alloc] initWithAccessKey:theAccessKey secretKey:theSecretKey] autorelease]; NSURL *endpoint = [[AWSRegion usEast1] s3EndpointWithSSL:YES]; S3Service *s3 = [[[S3Service alloc] initWithS3AuthorizationProvider:sap endpoint:endpoint useAmazonRRS:NO] autorelease]; NSString *location = [s3 locationOfS3Bucket:theBucketName targetConnectionDelegate:nil error:error]; if (location == nil) { return nil; } return [AWSRegion regionWithLocation:location]; } - (BOOL)listComputers:(NSError **)error { NSArray *expandedTargetList = [self expandedTargetList:error]; if (expandedTargetList == nil) { return NO; } NSMutableArray *ret = [NSMutableArray array]; for (Target *theTarget in expandedTargetList) { NSError *myError = nil; HSLogDebug(@"getting backup sets for %@", theTarget); NSArray *backupSets = [BackupSet allBackupSetsForTarget:theTarget targetConnectionDelegate:nil error:&myError]; if (backupSets == nil) { if ([myError isErrorWithDomain:[S3Service errorDomain] code:S3SERVICE_ERROR_AMAZON_ERROR] && [[[myError userInfo] objectForKey:@"HTTPStatusCode"] intValue] == 403) { HSLogError(@"access denied getting backup sets for %@", theTarget); } else { HSLogError(@"error getting backup sets for %@: %@", theTarget, myError); SETERRORFROMMYERROR; return nil; } } else { printf("target: %s\n", [[theTarget endpointDisplayName] UTF8String]); for (BackupSet *backupSet in backupSets) { printf("\tcomputer %s\n", [[backupSet computerUUID] UTF8String]); printf("\t\t%s (%s)\n", [[[backupSet userAndComputer] computerName] UTF8String], [[[backupSet userAndComputer] userName] UTF8String]); } } } return ret; } - (NSArray *)expandedTargetList:(NSError **)error { NSMutableArray *expandedTargetList = [NSMutableArray arrayWithObject:target]; // if ([target targetType] == kTargetAWS // || [target targetType] == kTargetDreamObjects // || [target targetType] == kTargetGoogleCloudStorage // || [target targetType] == kTargetGreenQloud // || [target targetType] == kTargetS3Compatible) { // NSError *myError = nil; // NSArray *targets = [self expandedTargetsForS3Target:target error:&myError]; // if (targets == nil) { // HSLogError(@"failed to expand target list for %@: %@", target, myError); // } else { // [expandedTargetList setArray:targets]; // HSLogDebug(@"expandedTargetList is now: %@", expandedTargetList); // } // } return expandedTargetList; } - (NSArray *)expandedTargetsForS3Target:(Target *)theTarget error:(NSError **)error { S3Service *s3 = [theTarget s3:error]; if (s3 == nil) { return nil; } NSArray *s3BucketNames = [s3 s3BucketNamesWithTargetConnectionDelegate:nil error:error]; if (s3BucketNames == nil) { return nil; } HSLogDebug(@"s3BucketNames for %@: %@", theTarget, s3BucketNames); NSURL *originalEndpoint = [theTarget endpoint]; NSMutableArray *ret = [NSMutableArray array]; for (NSString *s3BucketName in s3BucketNames) { NSURL *endpoint = nil; if ([theTarget targetType] == kTargetAWS) { NSString *location = [s3 locationOfS3Bucket:s3BucketName targetConnectionDelegate:nil error:error]; if (location == nil) { return nil; } AWSRegion *awsRegion = [AWSRegion regionWithLocation:location]; HSLogDebug(@"awsRegion for s3BucketName %@: %@", s3BucketName, location); NSURL *s3Endpoint = [awsRegion s3EndpointWithSSL:YES]; HSLogDebug(@"s3Endpoint: %@", s3Endpoint); endpoint = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@@%@/%@", [originalEndpoint user], [s3Endpoint host], s3BucketName]]; } else { endpoint = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@@%@/%@", [originalEndpoint scheme], [originalEndpoint user], [originalEndpoint host], s3BucketName]]; } HSLogDebug(@"endpoint: %@", endpoint); Target *theTarget = [[[Target alloc] initWithEndpoint:endpoint secret:[theTarget secret:NULL] passphrase:[theTarget passphrase:NULL]] autorelease]; [ret addObject:theTarget]; } return ret; } - (BOOL)listBucketsForComputerUUID:(NSString *)theComputerUUID encryptionPassword:(NSString *)theEncryptionPassword error:(NSError **)error { NSArray *buckets = [Bucket bucketsWithTarget:target computerUUID:theComputerUUID encryptionPassword:theEncryptionPassword targetConnectionDelegate:nil error:error]; if (buckets == nil) { return NO; } printf("target %s\n", [[target endpointDisplayName] UTF8String]); printf("computer %s\n", [theComputerUUID UTF8String]); for (Bucket *bucket in buckets) { printf("\tfolder %s\n", [[bucket localPath] UTF8String]); printf("\t\tuuid %s\n", [[bucket bucketUUID] UTF8String]); } return YES; } - (BOOL)restoreComputerUUID:(NSString *)theComputerUUID bucketUUID:(NSString *)theBucketUUID encryptionPassword:(NSString *)theEncryptionPassword restoreBytesPerSecond:(NSString *)theRestoreBytesPerSecond error:(NSError **)error { Bucket *myBucket = nil; NSArray *expandedTargetList = [self expandedTargetList:error]; if (expandedTargetList == nil) { return NO; } for (Target *theTarget in expandedTargetList) { NSArray *buckets = [Bucket bucketsWithTarget:theTarget computerUUID:theComputerUUID encryptionPassword:theEncryptionPassword targetConnectionDelegate:nil error:error]; if (buckets == nil) { return NO; } for (Bucket *bucket in buckets) { if ([[bucket bucketUUID] isEqualToString:theBucketUUID]) { myBucket = bucket; break; } } if (myBucket != nil) { break; } } if (myBucket == nil) { SETNSERROR([self errorDomain], ERROR_NOT_FOUND, @"folder %@ not found", theBucketUUID); return NO; } Repo *repo = [[[Repo alloc] initWithBucket:myBucket encryptionPassword:theEncryptionPassword targetUID:getuid() targetGID:getgid() loadExistingMutablePackFiles:NO targetConnectionDelegate:nil repoDelegate:nil error:error] autorelease]; if (repo == nil) { return NO; } BlobKey *commitBlobKey = [repo headBlobKey:error]; 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) { int bytesPerSecond = [theRestoreBytesPerSecond intValue]; if (bytesPerSecond == 0) { SETNSERROR([self errorDomain], -1, @"invalid bytes_per_second %@", theRestoreBytesPerSecond); return NO; } GlacierRestorerParamSet *paramSet = [[[GlacierRestorerParamSet alloc] initWithBucket:myBucket encryptionPassword:theEncryptionPassword downloadBytesPerSecond:bytesPerSecond 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]; [[[GlacierRestorer alloc] initWithGlacierRestorerParamSet:paramSet delegate:self] autorelease]; } else if ([myBucket storageType] == StorageTypeS3Glacier && isGlacierDestination) { int bytesPerSecond = [theRestoreBytesPerSecond intValue]; if (bytesPerSecond == 0) { SETNSERROR([self errorDomain], -1, @"invalid bytes_per_second %@", theRestoreBytesPerSecond); return NO; } S3GlacierRestorerParamSet *paramSet = [[[S3GlacierRestorerParamSet alloc] initWithBucket:myBucket encryptionPassword:theEncryptionPassword downloadBytesPerSecond:bytesPerSecond 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]; [[[S3GlacierRestorer alloc] initWithS3GlacierRestorerParamSet:paramSet delegate:self] autorelease]; } else { 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; } #pragma mark S3GlacierRestorerDelegate - (BOOL)s3GlacierRestorerMessageDidChange:(NSString *)message { printf("status: %s\n", [message UTF8String]); return NO; } - (BOOL)s3GlacierRestorerBytesRequestedDidChange:(NSNumber *)theRequested { return NO; } - (BOOL)s3GlacierRestorerTotalBytesToRequestDidChange:(NSNumber *)theMaxRequested { return NO; } - (BOOL)s3GlacierRestorerDidFinishRequesting { return NO; } - (BOOL)s3GlacierRestorerBytesTransferredDidChange:(NSNumber *)theTransferred { return NO; } - (BOOL)s3GlacierRestorerTotalBytesToTransferDidChange:(NSNumber *)theTotal { return NO; } - (BOOL)s3GlacierRestorerErrorMessage:(NSString *)theErrorMessage didOccurForPath:(NSString *)thePath { printf("%s error: %s\n", [thePath UTF8String], [theErrorMessage UTF8String]); return NO; } - (void)s3GlacierRestorerDidSucceed { } - (void)s3GlacierRestorerDidFail:(NSError *)error { printf("failed: %s\n", [[error localizedDescription] UTF8String]); } #pragma mark GlacierRestorerDelegate - (BOOL)glacierRestorerMessageDidChange:(NSString *)message { printf("status: %s\n", [message UTF8String]); return NO; } - (BOOL)glacierRestorerBytesRequestedDidChange:(NSNumber *)theRequested { return NO; } - (BOOL)glacierRestorerTotalBytesToRequestDidChange:(NSNumber *)theMaxRequested { return NO; } - (BOOL)glacierRestorerDidFinishRequesting { return NO; } - (BOOL)glacierRestorerBytesTransferredDidChange:(NSNumber *)theTransferred { return NO; } - (BOOL)glacierRestorerTotalBytesToTransferDidChange:(NSNumber *)theTotal { return NO; } - (BOOL)glacierRestorerErrorMessage:(NSString *)theErrorMessage didOccurForPath:(NSString *)thePath { printf("%s error: %s\n", [thePath UTF8String], [theErrorMessage UTF8String]); return NO; } - (BOOL)glacierRestorerDidSucceed { return NO; } - (BOOL)glacierRestorerDidFail:(NSError *)error { printf("failed: %s\n", [[error localizedDescription] UTF8String]); return NO; } @end