S3: Added COPY requests
S3: Added more convenience constructors (HEAD/DELETE) S3: More tests, tweaks
Showing
5 changed files
with
126 additions
and
35 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,16 +26,7 @@ static NSString *sharedSecretAccessKey = nil; | @@ -26,16 +26,7 @@ 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 | { |
@@ -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"]; | ||
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]]; | ||
95 | } | 130 | } |
96 | 131 | ||
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