Ben Copsey

S3: Added COPY requests

S3: Added more convenience constructors (HEAD/DELETE)
S3: More tests, tweaks
@@ -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