S3: Added COPY requests
S3: Added more convenience constructors (HEAD/DELETE) S3: More tests, tweaks
Showing
5 changed files
with
127 additions
and
36 deletions
| @@ -23,7 +23,12 @@ | @@ -23,7 +23,12 @@ | ||
| 23 | ASIS3BucketObject *currentObject; | 23 | ASIS3BucketObject *currentObject; |
| 24 | NSMutableArray *objects; | 24 | NSMutableArray *objects; |
| 25 | 25 | ||
| 26 | - | 26 | + // Options for filtering list requests |
| 27 | + // See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTBucketGET.html | ||
| 28 | + NSString *listPrefix; | ||
| 29 | + NSString *listMarker; | ||
| 30 | + int listMaxResults; | ||
| 31 | + NSString *listDelimiter; | ||
| 27 | } | 32 | } |
| 28 | // Create a list request | 33 | // Create a list request |
| 29 | + (id)listRequestWithBucket:(NSString *)bucket; | 34 | + (id)listRequestWithBucket:(NSString *)bucket; |
| @@ -38,9 +38,13 @@ static NSDateFormatter *dateFormatter = nil; | @@ -38,9 +38,13 @@ static NSDateFormatter *dateFormatter = nil; | ||
| 38 | 38 | ||
| 39 | - (void)dealloc | 39 | - (void)dealloc |
| 40 | { | 40 | { |
| 41 | - [currentElement release]; | ||
| 42 | [currentObject release]; | 41 | [currentObject release]; |
| 42 | + [currentElement release]; | ||
| 43 | + [currentContent release]; | ||
| 43 | [objects release]; | 44 | [objects release]; |
| 45 | + [prefix release]; | ||
| 46 | + [marker release]; | ||
| 47 | + [delimiter release]; | ||
| 44 | [super dealloc]; | 48 | [super dealloc]; |
| 45 | } | 49 | } |
| 46 | 50 | ||
| @@ -120,10 +124,6 @@ static NSDateFormatter *dateFormatter = nil; | @@ -120,10 +124,6 @@ static NSDateFormatter *dateFormatter = nil; | ||
| 120 | [self setCurrentContent:[[self currentContent] stringByAppendingString:string]]; | 124 | [self setCurrentContent:[[self currentContent] stringByAppendingString:string]]; |
| 121 | } | 125 | } |
| 122 | 126 | ||
| 123 | -- (void)parserDidEndDocument:(NSXMLParser *)parser | ||
| 124 | -{ | ||
| 125 | -} | ||
| 126 | - | ||
| 127 | 127 | ||
| 128 | @synthesize currentContent; | 128 | @synthesize currentContent; |
| 129 | @synthesize currentElement; | 129 | @synthesize currentElement; |
| @@ -46,12 +46,9 @@ typedef enum _ASIS3ErrorType { | @@ -46,12 +46,9 @@ typedef enum _ASIS3ErrorType { | ||
| 46 | // The access policy to use when PUTting a file (see the string constants at the top of this header) | 46 | // The access policy to use when PUTting a file (see the string constants at the top of this header) |
| 47 | NSString *accessPolicy; | 47 | NSString *accessPolicy; |
| 48 | 48 | ||
| 49 | - // Options for filtering list requests | 49 | + // The bucket + path of the object to be copied (used with COPYRequestFromBucket:path:toBucket:path:) |
| 50 | - // See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTBucketGET.html | 50 | + NSString *sourceBucket; |
| 51 | - NSString *listPrefix; | 51 | + NSString *sourcePath; |
| 52 | - NSString *listMarker; | ||
| 53 | - int listMaxResults; | ||
| 54 | - NSString *listDelimiter; | ||
| 55 | 52 | ||
| 56 | // Internally used while parsing errors | 53 | // Internally used while parsing errors |
| 57 | NSString *currentErrorString; | 54 | NSString *currentErrorString; |
| @@ -66,6 +63,17 @@ typedef enum _ASIS3ErrorType { | @@ -66,6 +63,17 @@ typedef enum _ASIS3ErrorType { | ||
| 66 | // Create a PUT request using the file at filePath as the body | 63 | // Create a PUT request using the file at filePath as the body |
| 67 | + (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket path:(NSString *)path; | 64 | + (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket path:(NSString *)path; |
| 68 | 65 | ||
| 66 | +// Create a DELETE request for the object at path | ||
| 67 | ++ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path; | ||
| 68 | + | ||
| 69 | +// Create a PUT request to copy an object from one location to another | ||
| 70 | +// Clang will complain because it thinks this method should return an object with +1 retain :( | ||
| 71 | ++ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path; | ||
| 72 | + | ||
| 73 | +// Creates a HEAD request for the object at path | ||
| 74 | ++ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path; | ||
| 75 | + | ||
| 76 | + | ||
| 69 | // Generates the request headers S3 needs | 77 | // Generates the request headers S3 needs |
| 70 | // Automatically called before the request begins in startRequest | 78 | // Automatically called before the request begins in startRequest |
| 71 | - (void)generateS3Headers; | 79 | - (void)generateS3Headers; |
| @@ -94,5 +102,6 @@ typedef enum _ASIS3ErrorType { | @@ -94,5 +102,6 @@ typedef enum _ASIS3ErrorType { | ||
| 94 | @property (retain) NSString *accessKey; | 102 | @property (retain) NSString *accessKey; |
| 95 | @property (retain) NSString *secretAccessKey; | 103 | @property (retain) NSString *secretAccessKey; |
| 96 | @property (retain) NSString *accessPolicy; | 104 | @property (retain) NSString *accessPolicy; |
| 97 | - | 105 | +@property (retain) NSString *sourceBucket; |
| 106 | +@property (retain) NSString *sourcePath; | ||
| 98 | @end | 107 | @end |
| @@ -26,20 +26,11 @@ static NSString *sharedSecretAccessKey = nil; | @@ -26,20 +26,11 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 26 | 26 | ||
| 27 | @implementation ASIS3Request | 27 | @implementation ASIS3Request |
| 28 | 28 | ||
| 29 | -- (void)dealloc | 29 | +#pragma mark Constructors |
| 30 | -{ | ||
| 31 | - [bucket release]; | ||
| 32 | - [path release]; | ||
| 33 | - [dateString release]; | ||
| 34 | - [mimeType release]; | ||
| 35 | - [accessKey release]; | ||
| 36 | - [secretAccessKey release]; | ||
| 37 | - [super dealloc]; | ||
| 38 | -} | ||
| 39 | 30 | ||
| 40 | + (id)requestWithBucket:(NSString *)bucket path:(NSString *)path | 31 | + (id)requestWithBucket:(NSString *)bucket path:(NSString *)path |
| 41 | { | 32 | { |
| 42 | - ASIS3Request *request = [[[ASIS3Request alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease]; | 33 | + ASIS3Request *request = [[[ASIS3Request alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease]; |
| 43 | [request setBucket:bucket]; | 34 | [request setBucket:bucket]; |
| 44 | [request setPath:path]; | 35 | [request setPath:path]; |
| 45 | return request; | 36 | return request; |
| @@ -55,6 +46,43 @@ static NSString *sharedSecretAccessKey = nil; | @@ -55,6 +46,43 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 55 | return request; | 46 | return request; |
| 56 | } | 47 | } |
| 57 | 48 | ||
| 49 | ++ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path | ||
| 50 | +{ | ||
| 51 | + ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path]; | ||
| 52 | + [request setRequestMethod:@"DELETE"]; | ||
| 53 | + return request; | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | ++ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path | ||
| 57 | +{ | ||
| 58 | + ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path]; | ||
| 59 | + [request setRequestMethod:@"PUT"]; | ||
| 60 | + [request setSourceBucket:sourceBucket]; | ||
| 61 | + [request setSourcePath:sourcePath]; | ||
| 62 | + return request; | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | ++ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path | ||
| 66 | +{ | ||
| 67 | + ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path]; | ||
| 68 | + [request setRequestMethod:@"HEAD"]; | ||
| 69 | + return request; | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +- (void)dealloc | ||
| 73 | +{ | ||
| 74 | + [bucket release]; | ||
| 75 | + [path release]; | ||
| 76 | + [dateString release]; | ||
| 77 | + [mimeType release]; | ||
| 78 | + [accessKey release]; | ||
| 79 | + [secretAccessKey release]; | ||
| 80 | + [sourcePath release]; | ||
| 81 | + [sourceBucket release]; | ||
| 82 | + [super dealloc]; | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | + | ||
| 58 | - (void)setDate:(NSDate *)date | 86 | - (void)setDate:(NSDate *)date |
| 59 | { | 87 | { |
| 60 | NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; | 88 | NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; |
| @@ -88,15 +116,23 @@ static NSString *sharedSecretAccessKey = nil; | @@ -88,15 +116,23 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 88 | NSString *canonicalizedResource = [NSString stringWithFormat:@"/%@%@",[self bucket],[self path]]; | 116 | NSString *canonicalizedResource = [NSString stringWithFormat:@"/%@%@",[self bucket],[self path]]; |
| 89 | 117 | ||
| 90 | // Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private) | 118 | // Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private) |
| 119 | + NSMutableDictionary *amzHeaders = [[[NSMutableDictionary alloc] init] autorelease]; | ||
| 91 | NSString *canonicalizedAmzHeaders = @""; | 120 | NSString *canonicalizedAmzHeaders = @""; |
| 92 | if ([self accessPolicy]) { | 121 | if ([self accessPolicy]) { |
| 93 | - [self addRequestHeader:@"x-amz-acl" value:[self accessPolicy]]; | 122 | + [amzHeaders setObject:[self accessPolicy] forKey:@"x-amz-acl"]; |
| 94 | - canonicalizedAmzHeaders = [NSString stringWithFormat:@"x-amz-acl:%@\n",[self accessPolicy]]; | 123 | + } |
| 124 | + if ([self sourcePath]) { | ||
| 125 | + [amzHeaders setObject:[[self sourceBucket] stringByAppendingString:[self sourcePath]] forKey:@"x-amz-copy-source"]; | ||
| 95 | } | 126 | } |
| 127 | + for (NSString *key in [amzHeaders keyEnumerator]) { | ||
| 128 | + canonicalizedAmzHeaders = [NSString stringWithFormat:@"%@%@:%@\n",canonicalizedAmzHeaders,[key lowercaseString],[amzHeaders objectForKey:key]]; | ||
| 129 | + [self addRequestHeader:key value:[amzHeaders objectForKey:key]]; | ||
| 130 | + } | ||
| 131 | + | ||
| 96 | 132 | ||
| 97 | // Jump through hoops while eating hot food | 133 | // Jump through hoops while eating hot food |
| 98 | NSString *stringToSign; | 134 | NSString *stringToSign; |
| 99 | - if ([[self requestMethod] isEqualToString:@"PUT"]) { | 135 | + if ([[self requestMethod] isEqualToString:@"PUT"] && ![self sourcePath]) { |
| 100 | [self addRequestHeader:@"Content-Type" value:[self mimeType]]; | 136 | [self addRequestHeader:@"Content-Type" value:[self mimeType]]; |
| 101 | stringToSign = [NSString stringWithFormat:@"PUT\n\n%@\n%@\n%@%@",[self mimeType],dateString,canonicalizedAmzHeaders,canonicalizedResource]; | 137 | stringToSign = [NSString stringWithFormat:@"PUT\n\n%@\n%@\n%@%@",[self mimeType],dateString,canonicalizedAmzHeaders,canonicalizedResource]; |
| 102 | } else { | 138 | } else { |
| @@ -115,6 +151,11 @@ static NSString *sharedSecretAccessKey = nil; | @@ -115,6 +151,11 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 115 | 151 | ||
| 116 | - (void)requestFinished | 152 | - (void)requestFinished |
| 117 | { | 153 | { |
| 154 | + // COPY requests return a 200 whether they succeed or fail, so we need to look at the XML to see if we were successful. | ||
| 155 | + if ([self responseStatusCode] == 200 && [self sourcePath] && [self sourceBucket]) { | ||
| 156 | + [self parseError]; | ||
| 157 | + return; | ||
| 158 | + } | ||
| 118 | if ([self responseStatusCode] < 207) { | 159 | if ([self responseStatusCode] < 207) { |
| 119 | [super requestFinished]; | 160 | [super requestFinished]; |
| 120 | return; | 161 | return; |
| @@ -157,14 +198,6 @@ static NSString *sharedSecretAccessKey = nil; | @@ -157,14 +198,6 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 157 | [self setCurrentErrorString:[[self currentErrorString] stringByAppendingString:string]]; | 198 | [self setCurrentErrorString:[[self currentErrorString] stringByAppendingString:string]]; |
| 158 | } | 199 | } |
| 159 | 200 | ||
| 160 | -- (void)parserDidEndDocument:(NSXMLParser *)parser | ||
| 161 | -{ | ||
| 162 | - // We've got to the end of the XML error, without encountering a <Message></Message>, I don't think this should happen, but anyway | ||
| 163 | - if (![self error]) { | ||
| 164 | - [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"An unspecified S3 error ocurred",NSLocalizedDescriptionKey,nil]]]; | ||
| 165 | - } | ||
| 166 | -} | ||
| 167 | - | ||
| 168 | 201 | ||
| 169 | #pragma mark Shared access keys | 202 | #pragma mark Shared access keys |
| 170 | 203 | ||
| @@ -284,5 +317,6 @@ static NSString *sharedSecretAccessKey = nil; | @@ -284,5 +317,6 @@ static NSString *sharedSecretAccessKey = nil; | ||
| 284 | @synthesize secretAccessKey; | 317 | @synthesize secretAccessKey; |
| 285 | @synthesize accessPolicy; | 318 | @synthesize accessPolicy; |
| 286 | @synthesize currentErrorString; | 319 | @synthesize currentErrorString; |
| 287 | - | 320 | +@synthesize sourceBucket; |
| 321 | +@synthesize sourcePath; | ||
| 288 | @end | 322 | @end |
| @@ -142,6 +142,30 @@ static NSString *bucket = @""; | @@ -142,6 +142,30 @@ static NSString *bucket = @""; | ||
| 142 | success = [[request responseString] isEqualToString:@"This is my content"]; | 142 | success = [[request responseString] isEqualToString:@"This is my content"]; |
| 143 | GHAssertTrue(success,@"Failed to GET the correct data from S3"); | 143 | GHAssertTrue(success,@"Failed to GET the correct data from S3"); |
| 144 | 144 | ||
| 145 | + // COPY the file | ||
| 146 | + request = [ASIS3Request COPYRequestFromBucket:bucket path:path toBucket:bucket path:@"/test-copy"]; | ||
| 147 | + [request setSecretAccessKey:secretAccessKey]; | ||
| 148 | + [request setAccessKey:accessKey]; | ||
| 149 | + [request start]; | ||
| 150 | + GHAssertNil([request error],@"Failed to COPY a file"); | ||
| 151 | + | ||
| 152 | + // GET the copy | ||
| 153 | + request = [ASIS3Request requestWithBucket:bucket path:@"/test-copy"]; | ||
| 154 | + [request setSecretAccessKey:secretAccessKey]; | ||
| 155 | + [request setAccessKey:accessKey]; | ||
| 156 | + [request start]; | ||
| 157 | + success = [[request responseString] isEqualToString:@"This is my content"]; | ||
| 158 | + GHAssertTrue(success,@"Failed to GET the correct data from S3"); | ||
| 159 | + | ||
| 160 | + | ||
| 161 | + // HEAD the copy | ||
| 162 | + request = [ASIS3Request HEADRequestWithBucket:bucket path:@"/test-copy"]; | ||
| 163 | + [request setSecretAccessKey:secretAccessKey]; | ||
| 164 | + [request setAccessKey:accessKey]; | ||
| 165 | + [request start]; | ||
| 166 | + success = [[request responseString] isEqualToString:@""]; | ||
| 167 | + GHAssertTrue(success,@"Got a response body for a HEAD request"); | ||
| 168 | + | ||
| 145 | // Get a list of files | 169 | // Get a list of files |
| 146 | ASIS3ListRequest *listRequest = [ASIS3ListRequest listRequestWithBucket:bucket]; | 170 | ASIS3ListRequest *listRequest = [ASIS3ListRequest listRequestWithBucket:bucket]; |
| 147 | [listRequest setPrefix:@"test"]; | 171 | [listRequest setPrefix:@"test"]; |
| @@ -160,6 +184,25 @@ static NSString *bucket = @""; | @@ -160,6 +184,25 @@ static NSString *bucket = @""; | ||
| 160 | [request start]; | 184 | [request start]; |
| 161 | success = [[request responseString] isEqualToString:@""]; | 185 | success = [[request responseString] isEqualToString:@""]; |
| 162 | GHAssertTrue(success,@"Failed to DELETE the file from S3"); | 186 | GHAssertTrue(success,@"Failed to DELETE the file from S3"); |
| 187 | + | ||
| 188 | + // (Also DELETE the copy we made) | ||
| 189 | + request = [ASIS3Request requestWithBucket:bucket path:@"/test-copy"]; | ||
| 190 | + [request setSecretAccessKey:secretAccessKey]; | ||
| 191 | + [request setRequestMethod:@"DELETE"]; | ||
| 192 | + [request setAccessKey:accessKey]; | ||
| 193 | + [request start]; | ||
| 194 | + success = [[request responseString] isEqualToString:@""]; | ||
| 195 | + GHAssertTrue(success,@"Failed to DELETE the copy from S3"); | ||
| 196 | + | ||
| 197 | + // Attempt to COPY the file, even though it is no longer there | ||
| 198 | + request = [ASIS3Request COPYRequestFromBucket:bucket path:path toBucket:bucket path:@"/test-copy"]; | ||
| 199 | + [request setSecretAccessKey:secretAccessKey]; | ||
| 200 | + [request setAccessKey:accessKey]; | ||
| 201 | + [request start]; | ||
| 202 | + GHAssertNotNil([request error],@"Failed generate an error for what should have been a failed COPY"); | ||
| 203 | + | ||
| 204 | + success = [[[request error] localizedDescription] isEqualToString:@"The specified key does not exist."]; | ||
| 205 | + GHAssertTrue(success, @"Got the wrong error message"); | ||
| 163 | } | 206 | } |
| 164 | 207 | ||
| 165 | - (void)testListRequest | 208 | - (void)testListRequest |
-
Please register or login to post a comment