Ben Copsey

S3: Added COPY requests

S3: Added more convenience constructors (HEAD/DELETE)
S3: More tests, tweaks
... ... @@ -23,7 +23,12 @@
ASIS3BucketObject *currentObject;
NSMutableArray *objects;
// Options for filtering list requests
// See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTBucketGET.html
NSString *listPrefix;
NSString *listMarker;
int listMaxResults;
NSString *listDelimiter;
}
// Create a list request
+ (id)listRequestWithBucket:(NSString *)bucket;
... ...
... ... @@ -38,9 +38,13 @@ static NSDateFormatter *dateFormatter = nil;
- (void)dealloc
{
[currentElement release];
[currentObject release];
[currentElement release];
[currentContent release];
[objects release];
[prefix release];
[marker release];
[delimiter release];
[super dealloc];
}
... ... @@ -120,10 +124,6 @@ static NSDateFormatter *dateFormatter = nil;
[self setCurrentContent:[[self currentContent] stringByAppendingString:string]];
}
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
}
@synthesize currentContent;
@synthesize currentElement;
... ...
... ... @@ -46,12 +46,9 @@ typedef enum _ASIS3ErrorType {
// The access policy to use when PUTting a file (see the string constants at the top of this header)
NSString *accessPolicy;
// Options for filtering list requests
// See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTBucketGET.html
NSString *listPrefix;
NSString *listMarker;
int listMaxResults;
NSString *listDelimiter;
// The bucket + path of the object to be copied (used with COPYRequestFromBucket:path:toBucket:path:)
NSString *sourceBucket;
NSString *sourcePath;
// Internally used while parsing errors
NSString *currentErrorString;
... ... @@ -66,6 +63,17 @@ typedef enum _ASIS3ErrorType {
// Create a PUT request using the file at filePath as the body
+ (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket path:(NSString *)path;
// Create a DELETE request for the object at path
+ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path;
// Create a PUT request to copy an object from one location to another
// Clang will complain because it thinks this method should return an object with +1 retain :(
+ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path;
// Creates a HEAD request for the object at path
+ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path;
// Generates the request headers S3 needs
// Automatically called before the request begins in startRequest
- (void)generateS3Headers;
... ... @@ -94,5 +102,6 @@ typedef enum _ASIS3ErrorType {
@property (retain) NSString *accessKey;
@property (retain) NSString *secretAccessKey;
@property (retain) NSString *accessPolicy;
@property (retain) NSString *sourceBucket;
@property (retain) NSString *sourcePath;
@end
... ...
... ... @@ -26,20 +26,11 @@ static NSString *sharedSecretAccessKey = nil;
@implementation ASIS3Request
- (void)dealloc
{
[bucket release];
[path release];
[dateString release];
[mimeType release];
[accessKey release];
[secretAccessKey release];
[super dealloc];
}
#pragma mark Constructors
+ (id)requestWithBucket:(NSString *)bucket path:(NSString *)path
{
ASIS3Request *request = [[[ASIS3Request alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease];
ASIS3Request *request = [[[ASIS3Request alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease];
[request setBucket:bucket];
[request setPath:path];
return request;
... ... @@ -55,6 +46,43 @@ static NSString *sharedSecretAccessKey = nil;
return request;
}
+ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path
{
ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path];
[request setRequestMethod:@"DELETE"];
return request;
}
+ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path
{
ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path];
[request setRequestMethod:@"PUT"];
[request setSourceBucket:sourceBucket];
[request setSourcePath:sourcePath];
return request;
}
+ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path
{
ASIS3Request *request = [ASIS3Request requestWithBucket:bucket path:path];
[request setRequestMethod:@"HEAD"];
return request;
}
- (void)dealloc
{
[bucket release];
[path release];
[dateString release];
[mimeType release];
[accessKey release];
[secretAccessKey release];
[sourcePath release];
[sourceBucket release];
[super dealloc];
}
- (void)setDate:(NSDate *)date
{
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
... ... @@ -88,15 +116,23 @@ static NSString *sharedSecretAccessKey = nil;
NSString *canonicalizedResource = [NSString stringWithFormat:@"/%@%@",[self bucket],[self path]];
// Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private)
NSMutableDictionary *amzHeaders = [[[NSMutableDictionary alloc] init] autorelease];
NSString *canonicalizedAmzHeaders = @"";
if ([self accessPolicy]) {
[self addRequestHeader:@"x-amz-acl" value:[self accessPolicy]];
canonicalizedAmzHeaders = [NSString stringWithFormat:@"x-amz-acl:%@\n",[self accessPolicy]];
[amzHeaders setObject:[self accessPolicy] forKey:@"x-amz-acl"];
}
if ([self sourcePath]) {
[amzHeaders setObject:[[self sourceBucket] stringByAppendingString:[self sourcePath]] forKey:@"x-amz-copy-source"];
}
for (NSString *key in [amzHeaders keyEnumerator]) {
canonicalizedAmzHeaders = [NSString stringWithFormat:@"%@%@:%@\n",canonicalizedAmzHeaders,[key lowercaseString],[amzHeaders objectForKey:key]];
[self addRequestHeader:key value:[amzHeaders objectForKey:key]];
}
// Jump through hoops while eating hot food
NSString *stringToSign;
if ([[self requestMethod] isEqualToString:@"PUT"]) {
if ([[self requestMethod] isEqualToString:@"PUT"] && ![self sourcePath]) {
[self addRequestHeader:@"Content-Type" value:[self mimeType]];
stringToSign = [NSString stringWithFormat:@"PUT\n\n%@\n%@\n%@%@",[self mimeType],dateString,canonicalizedAmzHeaders,canonicalizedResource];
} else {
... ... @@ -115,6 +151,11 @@ static NSString *sharedSecretAccessKey = nil;
- (void)requestFinished
{
// COPY requests return a 200 whether they succeed or fail, so we need to look at the XML to see if we were successful.
if ([self responseStatusCode] == 200 && [self sourcePath] && [self sourceBucket]) {
[self parseError];
return;
}
if ([self responseStatusCode] < 207) {
[super requestFinished];
return;
... ... @@ -157,14 +198,6 @@ static NSString *sharedSecretAccessKey = nil;
[self setCurrentErrorString:[[self currentErrorString] stringByAppendingString:string]];
}
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
// We've got to the end of the XML error, without encountering a <Message></Message>, I don't think this should happen, but anyway
if (![self error]) {
[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"An unspecified S3 error ocurred",NSLocalizedDescriptionKey,nil]]];
}
}
#pragma mark Shared access keys
... ... @@ -284,5 +317,6 @@ static NSString *sharedSecretAccessKey = nil;
@synthesize secretAccessKey;
@synthesize accessPolicy;
@synthesize currentErrorString;
@synthesize sourceBucket;
@synthesize sourcePath;
@end
... ...
... ... @@ -142,6 +142,30 @@ static NSString *bucket = @"";
success = [[request responseString] isEqualToString:@"This is my content"];
GHAssertTrue(success,@"Failed to GET the correct data from S3");
// COPY the file
request = [ASIS3Request COPYRequestFromBucket:bucket path:path toBucket:bucket path:@"/test-copy"];
[request setSecretAccessKey:secretAccessKey];
[request setAccessKey:accessKey];
[request start];
GHAssertNil([request error],@"Failed to COPY a file");
// GET the copy
request = [ASIS3Request requestWithBucket:bucket path:@"/test-copy"];
[request setSecretAccessKey:secretAccessKey];
[request setAccessKey:accessKey];
[request start];
success = [[request responseString] isEqualToString:@"This is my content"];
GHAssertTrue(success,@"Failed to GET the correct data from S3");
// HEAD the copy
request = [ASIS3Request HEADRequestWithBucket:bucket path:@"/test-copy"];
[request setSecretAccessKey:secretAccessKey];
[request setAccessKey:accessKey];
[request start];
success = [[request responseString] isEqualToString:@""];
GHAssertTrue(success,@"Got a response body for a HEAD request");
// Get a list of files
ASIS3ListRequest *listRequest = [ASIS3ListRequest listRequestWithBucket:bucket];
[listRequest setPrefix:@"test"];
... ... @@ -160,6 +184,25 @@ static NSString *bucket = @"";
[request start];
success = [[request responseString] isEqualToString:@""];
GHAssertTrue(success,@"Failed to DELETE the file from S3");
// (Also DELETE the copy we made)
request = [ASIS3Request requestWithBucket:bucket path:@"/test-copy"];
[request setSecretAccessKey:secretAccessKey];
[request setRequestMethod:@"DELETE"];
[request setAccessKey:accessKey];
[request start];
success = [[request responseString] isEqualToString:@""];
GHAssertTrue(success,@"Failed to DELETE the copy from S3");
// Attempt to COPY the file, even though it is no longer there
request = [ASIS3Request COPYRequestFromBucket:bucket path:path toBucket:bucket path:@"/test-copy"];
[request setSecretAccessKey:secretAccessKey];
[request setAccessKey:accessKey];
[request start];
GHAssertNotNil([request error],@"Failed generate an error for what should have been a failed COPY");
success = [[[request error] localizedDescription] isEqualToString:@"The specified key does not exist."];
GHAssertTrue(success, @"Got the wrong error message");
}
- (void)testListRequest
... ...