Ben Copsey

Minor refactor / cleanup on S3 API:

Have all ASIS3Request subclasses parse the xml response in request finished - cuts out quite a bit of duplication
Service and bucket requests now correctly parse error message xml
Fixes to query string generation for bucket requests
Keep track of the current element stack so we can correctly parse common prefixes
Fix a couple of leaked objects
Add new tests for issues raised by Tom Andersen in his patch + email
@@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
23 23
24 24
25 // Automatically set on build 25 // Automatically set on build
26 -NSString *ASIHTTPRequestVersion = @"v1.6.1-6 2010-04-12"; 26 +NSString *ASIHTTPRequestVersion = @"v1.6.1-8 2010-04-12";
27 27
28 NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain"; 28 NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain";
29 29
@@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
20 20
21 - (void)dealloc 21 - (void)dealloc
22 { 22 {
  23 + [bucket release];
23 [key release]; 24 [key release];
24 [lastModified release]; 25 [lastModified release];
25 [ETag release]; 26 [ETag release];
@@ -29,12 +29,15 @@ @@ -29,12 +29,15 @@
29 NSString *delimiter; 29 NSString *delimiter;
30 30
31 // Internally used while parsing the response 31 // Internally used while parsing the response
32 - NSString *currentContent;  
33 - NSString *currentElement;  
34 ASIS3BucketObject *currentObject; 32 ASIS3BucketObject *currentObject;
  33 +
  34 + // Returns an array of ASIS3BucketObjects created from the XML response
35 NSMutableArray *objects; 35 NSMutableArray *objects;
36 36
37 - NSMutableArray* foundFolders; 37 + // Will be populated with a list of 'folders' when a delimiter is set
  38 + NSMutableArray *commonPrefixes;
  39 +
  40 + // Will be true if this request did not return all the results matching the query (use maxResultCount to configure the number of results to return)
38 BOOL isTruncated; 41 BOOL isTruncated;
39 } 42 }
40 43
@@ -57,12 +60,6 @@ @@ -57,12 +60,6 @@
57 // Use for deleting buckets - they must be empty for this to succeed 60 // Use for deleting buckets - they must be empty for this to succeed
58 + (id)DELETERequestWithBucket:(NSString *)bucket; 61 + (id)DELETERequestWithBucket:(NSString *)bucket;
59 62
60 -// Returns an array of ASIS3BucketObjects created from the XML response  
61 -- (NSArray *)bucketObjects;  
62 -  
63 -// prefixes are like folders - get them by setting a delimiter string (usually always @"/")  
64 --(NSArray*)commonPrefixes;  
65 -  
66 //Builds a query string out of the list parameters we supplied 63 //Builds a query string out of the list parameters we supplied
67 - (void)createQueryString; 64 - (void)createQueryString;
68 65
@@ -72,5 +69,7 @@ @@ -72,5 +69,7 @@
72 @property (retain) NSString *marker; 69 @property (retain) NSString *marker;
73 @property (assign) int maxResultCount; 70 @property (assign) int maxResultCount;
74 @property (retain) NSString *delimiter; 71 @property (retain) NSString *delimiter;
75 -@property (readonly) BOOL isTruncated; 72 +@property (retain, readonly) NSMutableArray *objects;
  73 +@property (retain, readonly) NSMutableArray *commonPrefixes;
  74 +@property (assign, readonly) BOOL isTruncated;
76 @end 75 @end
@@ -12,16 +12,24 @@ @@ -12,16 +12,24 @@
12 12
13 // Private stuff 13 // Private stuff
14 @interface ASIS3BucketRequest () 14 @interface ASIS3BucketRequest ()
15 -@property (retain, nonatomic) NSString *currentContent;  
16 -@property (retain, nonatomic) NSString *currentElement;  
17 @property (retain, nonatomic) ASIS3BucketObject *currentObject; 15 @property (retain, nonatomic) ASIS3BucketObject *currentObject;
18 -@property (retain, nonatomic) NSMutableArray *objects; 16 +@property (retain, nonatomic) NSString *currentXMLElementContent;
19 -@property (retain, nonatomic) NSMutableArray *foundFolders; 17 +@property (retain, nonatomic) NSMutableArray *currentXMLElementStack;
20 -@property (readwrite) BOOL isTruncated; 18 +@property (retain) NSMutableArray *objects;
  19 +@property (retain) NSMutableArray *commonPrefixes;
  20 +@property (assign) BOOL isTruncated;
21 @end 21 @end
22 22
23 @implementation ASIS3BucketRequest 23 @implementation ASIS3BucketRequest
24 24
  25 +- (id)initWithURL:(NSURL *)newURL
  26 +{
  27 + self = [super initWithURL:newURL];
  28 + [self setObjects:[[[NSMutableArray alloc] init] autorelease]];
  29 + [self setCommonPrefixes:[[[NSMutableArray alloc] init] autorelease]];
  30 + return self;
  31 +}
  32 +
25 + (id)requestWithBucket:(NSString *)bucket 33 + (id)requestWithBucket:(NSString *)bucket
26 { 34 {
27 ASIS3ObjectRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com",bucket]]] autorelease]; 35 ASIS3ObjectRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com",bucket]]] autorelease];
@@ -56,10 +64,8 @@ @@ -56,10 +64,8 @@
56 - (void)dealloc 64 - (void)dealloc
57 { 65 {
58 [currentObject release]; 66 [currentObject release];
59 - [currentElement release];  
60 - [currentContent release];  
61 [objects release]; 67 [objects release];
62 - [foundFolders release]; 68 + [commonPrefixes release];
63 [prefix release]; 69 [prefix release];
64 [marker release]; 70 [marker release];
65 [delimiter release]; 71 [delimiter release];
@@ -83,19 +89,20 @@ @@ -83,19 +89,20 @@
83 [queryParts addObject:[NSString stringWithFormat:@"prefix=%@",[[self prefix] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; 89 [queryParts addObject:[NSString stringWithFormat:@"prefix=%@",[[self prefix] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
84 } 90 }
85 if ([self marker]) { 91 if ([self marker]) {
86 - [queryParts addObject:[NSString stringWithFormat:@"marker=%@",[[self marker] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; 92 + [queryParts addObject:[NSString stringWithFormat:@"key-marker=%@",[[self marker] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
87 } 93 }
88 if ([self delimiter]) { 94 if ([self delimiter]) {
89 [queryParts addObject:[NSString stringWithFormat:@"delimiter=%@",[[self delimiter] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; 95 [queryParts addObject:[NSString stringWithFormat:@"delimiter=%@",[[self delimiter] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
90 } 96 }
91 if ([self maxResultCount] > 0) { 97 if ([self maxResultCount] > 0) {
92 - [queryParts addObject:[NSString stringWithFormat:@"delimiter=%hi",[self maxResultCount]]]; 98 + [queryParts addObject:[NSString stringWithFormat:@"max-keys=%hi",[self maxResultCount]]];
93 } 99 }
94 if ([queryParts count]) 100 if ([queryParts count])
95 { 101 {
96 NSString* template = @"%@?%@"; 102 NSString* template = @"%@?%@";
97 - if ([self.subResource length] > 0) 103 + if ([[self subResource] length] > 0) {
98 - template = @"%@&%@"; 104 + template = @"%@&%@";
  105 + }
99 [self setURL:[NSURL URLWithString:[NSString stringWithFormat:template,[[self url] absoluteString],[queryParts componentsJoinedByString:@"&"]]]]; 106 [self setURL:[NSURL URLWithString:[NSString stringWithFormat:template,[[self url] absoluteString],[queryParts componentsJoinedByString:@"&"]]]];
100 } 107 }
101 } 108 }
@@ -106,44 +113,12 @@ @@ -106,44 +113,12 @@
106 [super main]; 113 [super main];
107 } 114 }
108 115
109 --(void)makeBucketsAndPrefixes;  
110 -{  
111 - [self setObjects:[[[NSMutableArray alloc] init] autorelease]];  
112 - [self setFoundFolders:[[[NSMutableArray alloc] init] autorelease]];  
113 - NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:[self responseData]] autorelease];  
114 - [parser setDelegate:self];  
115 - [parser setShouldProcessNamespaces:NO];  
116 - [parser setShouldReportNamespacePrefixes:NO];  
117 - [parser setShouldResolveExternalEntities:NO];  
118 - [parser parse];  
119 -}  
120 -  
121 -- (NSArray *)bucketObjects  
122 -{  
123 - if ([self objects]) {  
124 - return [self objects];  
125 - }  
126 - [self makeBucketsAndPrefixes];  
127 - return [self objects];  
128 -}  
129 -  
130 --(NSArray*)commonPrefixes;  
131 -{  
132 - if ([self foundFolders]) {  
133 - return [self foundFolders];  
134 - }  
135 - [self makeBucketsAndPrefixes];  
136 - return [self foundFolders];  
137 -}  
138 -  
139 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 116 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
140 { 117 {
141 - [self setCurrentElement:elementName];  
142 -  
143 if ([elementName isEqualToString:@"Contents"]) { 118 if ([elementName isEqualToString:@"Contents"]) {
144 [self setCurrentObject:[ASIS3BucketObject objectWithBucket:[self bucket]]]; 119 [self setCurrentObject:[ASIS3BucketObject objectWithBucket:[self bucket]]];
145 } 120 }
146 - [self setCurrentContent:@""]; 121 + [super parser:parser didStartElement:elementName namespaceURI:namespaceURI qualifiedName:qName attributes:attributeDict];
147 } 122 }
148 123
149 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName 124 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
@@ -152,27 +127,27 @@ @@ -152,27 +127,27 @@
152 [objects addObject:currentObject]; 127 [objects addObject:currentObject];
153 [self setCurrentObject:nil]; 128 [self setCurrentObject:nil];
154 } else if ([elementName isEqualToString:@"Key"]) { 129 } else if ([elementName isEqualToString:@"Key"]) {
155 - [[self currentObject] setKey:[self currentContent]]; 130 + [[self currentObject] setKey:[self currentXMLElementContent]];
156 } else if ([elementName isEqualToString:@"LastModified"]) { 131 } else if ([elementName isEqualToString:@"LastModified"]) {
157 - [[self currentObject] setLastModified:[[ASIS3Request dateFormatter] dateFromString:[self currentContent]]]; 132 + [[self currentObject] setLastModified:[[ASIS3Request dateFormatter] dateFromString:[self currentXMLElementContent]]];
158 } else if ([elementName isEqualToString:@"ETag"]) { 133 } else if ([elementName isEqualToString:@"ETag"]) {
159 - [[self currentObject] setETag:[self currentContent]]; 134 + [[self currentObject] setETag:[self currentXMLElementContent]];
160 } else if ([elementName isEqualToString:@"Size"]) { 135 } else if ([elementName isEqualToString:@"Size"]) {
161 - [[self currentObject] setSize:(unsigned long long)[[self currentContent] longLongValue]]; 136 + [[self currentObject] setSize:(unsigned long long)[[self currentXMLElementContent] longLongValue]];
162 } else if ([elementName isEqualToString:@"ID"]) { 137 } else if ([elementName isEqualToString:@"ID"]) {
163 - [[self currentObject] setOwnerID:[self currentContent]]; 138 + [[self currentObject] setOwnerID:[self currentXMLElementContent]];
164 } else if ([elementName isEqualToString:@"DisplayName"]) { 139 } else if ([elementName isEqualToString:@"DisplayName"]) {
165 - [[self currentObject] setOwnerName:[self currentContent]]; 140 + [[self currentObject] setOwnerName:[self currentXMLElementContent]];
166 - } else if ([elementName isEqualToString:@"Prefix"]) { 141 + } else if ([elementName isEqualToString:@"Prefix"] && [[self currentXMLElementStack] count] > 2 && [[[self currentXMLElementStack] objectAtIndex:[[self currentXMLElementStack] count]-2] isEqualToString:@"CommonPrefixes"]) {
167 - [foundFolders addObject:[NSString stringWithString:[self currentContent]]]; 142 + [[self commonPrefixes] addObject:[self currentXMLElementContent]];
168 } else if ([elementName isEqualToString:@"IsTruncated"]) { 143 } else if ([elementName isEqualToString:@"IsTruncated"]) {
169 - self.isTruncated = [[NSString stringWithString:[self currentContent]] boolValue]; 144 + [self setIsTruncated:[[self currentXMLElementContent] isEqualToString:@"true"]];
  145 + } else {
  146 + // Let ASIS3Request look for error messages
  147 + [super parser:parser didEndElement:elementName namespaceURI:namespaceURI qualifiedName:qName];
170 } 148 }
171 } 149 }
172 -- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string 150 +
173 -{  
174 - [self setCurrentContent:[[self currentContent] stringByAppendingString:string]];  
175 -}  
176 151
177 #pragma mark NSCopying 152 #pragma mark NSCopying
178 153
@@ -188,17 +163,19 @@ @@ -188,17 +163,19 @@
188 return newRequest; 163 return newRequest;
189 } 164 }
190 165
  166 +
  167 +
  168 +
191 @synthesize bucket; 169 @synthesize bucket;
192 @synthesize subResource; 170 @synthesize subResource;
193 -@synthesize currentContent;  
194 -@synthesize currentElement;  
195 @synthesize currentObject; 171 @synthesize currentObject;
196 @synthesize objects; 172 @synthesize objects;
197 -@synthesize foundFolders; 173 +@synthesize commonPrefixes;
198 @synthesize prefix; 174 @synthesize prefix;
199 @synthesize marker; 175 @synthesize marker;
200 @synthesize maxResultCount; 176 @synthesize maxResultCount;
201 @synthesize delimiter; 177 @synthesize delimiter;
202 @synthesize isTruncated; 178 @synthesize isTruncated;
203 - 179 +@synthesize currentXMLElementContent;
  180 +@synthesize currentXMLElementStack;
204 @end 181 @end
@@ -112,16 +112,6 @@ @@ -112,16 +112,6 @@
112 } 112 }
113 } 113 }
114 114
115 -- (void)requestFinished  
116 -{  
117 - // COPY requests return a 200 whether they succeed or fail, so we need to look at the XML to see if we were successful.  
118 - if ([self responseStatusCode] == 200 && [self sourceKey] && [self sourceBucket]) {  
119 - [self parseResponseXML];  
120 - return;  
121 - }  
122 - [super requestFinished];  
123 -}  
124 -  
125 - (NSString *)canonicalizedResource 115 - (NSString *)canonicalizedResource
126 { 116 {
127 return [NSString stringWithFormat:@"/%@%@",[self bucket],[ASIS3Request stringByURLEncodingForS3Path:[self key]]]; 117 return [NSString stringWithFormat:@"/%@%@",[self bucket],[ASIS3Request stringByURLEncodingForS3Path:[self key]]];
@@ -147,6 +137,7 @@ @@ -147,6 +137,7 @@
147 } 137 }
148 138
149 139
  140 +
150 @synthesize bucket; 141 @synthesize bucket;
151 @synthesize key; 142 @synthesize key;
152 @synthesize sourceBucket; 143 @synthesize sourceBucket;
@@ -42,7 +42,8 @@ typedef enum _ASIS3ErrorType { @@ -42,7 +42,8 @@ typedef enum _ASIS3ErrorType {
42 NSString *accessPolicy; 42 NSString *accessPolicy;
43 43
44 // Internally used while parsing errors 44 // Internally used while parsing errors
45 - NSString *currentErrorString; 45 + NSString *currentXMLElementContent;
  46 + NSMutableArray *currentXMLElementStack;
46 } 47 }
47 48
48 // Uses the supplied date to create a Date header string 49 // Uses the supplied date to create a Date header string
@@ -77,7 +78,6 @@ typedef enum _ASIS3ErrorType { @@ -77,7 +78,6 @@ typedef enum _ASIS3ErrorType {
77 + (NSString *)stringByURLEncodingForS3Path:(NSString *)key; 78 + (NSString *)stringByURLEncodingForS3Path:(NSString *)key;
78 79
79 80
80 -  
81 @property (retain) NSString *dateString; 81 @property (retain) NSString *dateString;
82 @property (retain) NSString *accessKey; 82 @property (retain) NSString *accessKey;
83 @property (retain) NSString *secretAccessKey; 83 @property (retain) NSString *secretAccessKey;
@@ -19,12 +19,11 @@ static NSString *sharedSecretAccessKey = nil; @@ -19,12 +19,11 @@ static NSString *sharedSecretAccessKey = nil;
19 19
20 static NSDateFormatter *dateFormatter = nil; 20 static NSDateFormatter *dateFormatter = nil;
21 21
22 -  
23 -  
24 // Private stuff 22 // Private stuff
25 @interface ASIS3Request () 23 @interface ASIS3Request ()
26 + (NSData *)HMACSHA1withKey:(NSString *)key forString:(NSString *)string; 24 + (NSData *)HMACSHA1withKey:(NSString *)key forString:(NSString *)string;
27 - @property (retain, nonatomic) NSString *currentErrorString; 25 + @property (retain, nonatomic) NSString *currentXMLElementContent;
  26 + @property (retain, nonatomic) NSMutableArray *currentXMLElementStack;
28 @end 27 @end
29 28
30 @implementation ASIS3Request 29 @implementation ASIS3Request
@@ -40,6 +39,8 @@ static NSDateFormatter *dateFormatter = nil; @@ -40,6 +39,8 @@ static NSDateFormatter *dateFormatter = nil;
40 39
41 - (void)dealloc 40 - (void)dealloc
42 { 41 {
  42 + [currentXMLElementContent release];
  43 + [currentXMLElementStack release];
43 [dateString release]; 44 [dateString release];
44 [accessKey release]; 45 [accessKey release];
45 [secretAccessKey release]; 46 [secretAccessKey release];
@@ -123,11 +124,12 @@ static NSDateFormatter *dateFormatter = nil; @@ -123,11 +124,12 @@ static NSDateFormatter *dateFormatter = nil;
123 124
124 - (void)requestFinished 125 - (void)requestFinished
125 { 126 {
126 - if ([self responseStatusCode] < 207) { 127 + if ([[[self responseHeaders] objectForKey:@"Content-Type"] isEqualToString:@"application/xml"]) {
  128 + [self parseResponseXML];
  129 + }
  130 + if (![self error]) {
127 [super requestFinished]; 131 [super requestFinished];
128 - return;  
129 } 132 }
130 - [self parseResponseXML];  
131 } 133 }
132 134
133 #pragma mark Error XML parsing 135 #pragma mark Error XML parsing
@@ -135,6 +137,7 @@ static NSDateFormatter *dateFormatter = nil; @@ -135,6 +137,7 @@ static NSDateFormatter *dateFormatter = nil;
135 - (void)parseResponseXML 137 - (void)parseResponseXML
136 { 138 {
137 NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:[self responseData]] autorelease]; 139 NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:[self responseData]] autorelease];
  140 + [self setCurrentXMLElementStack:[NSMutableArray array]];
138 [parser setDelegate:self]; 141 [parser setDelegate:self];
139 [parser setShouldProcessNamespaces:NO]; 142 [parser setShouldProcessNamespaces:NO];
140 [parser setShouldReportNamespacePrefixes:NO]; 143 [parser setShouldReportNamespacePrefixes:NO];
@@ -150,16 +153,18 @@ static NSDateFormatter *dateFormatter = nil; @@ -150,16 +153,18 @@ static NSDateFormatter *dateFormatter = nil;
150 153
151 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 154 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
152 { 155 {
153 - [self setCurrentErrorString:@""]; 156 + [self setCurrentXMLElementContent:@""];
  157 + [[self currentXMLElementStack] addObject:elementName];
154 } 158 }
155 159
156 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName 160 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
157 { 161 {
  162 + [[self currentXMLElementStack] removeLastObject];
158 if ([elementName isEqualToString:@"Message"]) { 163 if ([elementName isEqualToString:@"Message"]) {
159 - [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[self currentErrorString],NSLocalizedDescriptionKey,nil]]]; 164 + [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[self currentXMLElementContent],NSLocalizedDescriptionKey,nil]]];
160 // Handle S3 connection expiry errors 165 // Handle S3 connection expiry errors
161 } else if ([elementName isEqualToString:@"Code"]) { 166 } else if ([elementName isEqualToString:@"Code"]) {
162 - if ([[self currentErrorString] isEqualToString:@"RequestTimeout"]) { 167 + if ([[self currentXMLElementContent] isEqualToString:@"RequestTimeout"]) {
163 if ([self retryUsingNewConnection]) { 168 if ([self retryUsingNewConnection]) {
164 [parser abortParsing]; 169 [parser abortParsing];
165 return; 170 return;
@@ -170,7 +175,7 @@ static NSDateFormatter *dateFormatter = nil; @@ -170,7 +175,7 @@ static NSDateFormatter *dateFormatter = nil;
170 175
171 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string 176 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
172 { 177 {
173 - [self setCurrentErrorString:[[self currentErrorString] stringByAppendingString:string]]; 178 + [self setCurrentXMLElementContent:[[self currentXMLElementContent] stringByAppendingString:string]];
174 } 179 }
175 180
176 - (id)copyWithZone:(NSZone *)zone 181 - (id)copyWithZone:(NSZone *)zone
@@ -255,6 +260,7 @@ static NSDateFormatter *dateFormatter = nil; @@ -255,6 +260,7 @@ static NSDateFormatter *dateFormatter = nil;
255 @synthesize dateString; 260 @synthesize dateString;
256 @synthesize accessKey; 261 @synthesize accessKey;
257 @synthesize secretAccessKey; 262 @synthesize secretAccessKey;
258 -@synthesize currentErrorString; 263 +@synthesize currentXMLElementContent;
  264 +@synthesize currentXMLElementStack;
259 @synthesize accessPolicy; 265 @synthesize accessPolicy;
260 @end 266 @end
@@ -15,19 +15,17 @@ @@ -15,19 +15,17 @@
15 @interface ASIS3ServiceRequest : ASIS3Request { 15 @interface ASIS3ServiceRequest : ASIS3Request {
16 16
17 // Internally used while parsing the response 17 // Internally used while parsing the response
18 - NSString *currentContent;  
19 - NSString *currentElement;  
20 ASIS3Bucket *currentBucket; 18 ASIS3Bucket *currentBucket;
21 - NSMutableArray *buckets;  
22 NSString *ownerName; 19 NSString *ownerName;
23 NSString *ownerID; 20 NSString *ownerID;
  21 +
  22 + // A list of the buckets stored on S3 for this account
  23 + NSMutableArray *buckets;
24 } 24 }
25 25
26 // Perform a GET request on the S3 service 26 // Perform a GET request on the S3 service
27 // This will fetch a list of the buckets attached to the S3 account 27 // This will fetch a list of the buckets attached to the S3 account
28 + (id)serviceRequest; 28 + (id)serviceRequest;
29 29
30 -// Parse the XML response from S3, and return an array of ASIS3Bucket objects 30 +@property (retain, readonly) NSMutableArray *buckets;
31 -- (NSArray *)allBuckets;  
32 -  
33 @end 31 @end
@@ -11,12 +11,13 @@ @@ -11,12 +11,13 @@
11 11
12 // Private stuff 12 // Private stuff
13 @interface ASIS3ServiceRequest () 13 @interface ASIS3ServiceRequest ()
14 -@property (retain, nonatomic) NSMutableArray *buckets; 14 +@property (retain) NSMutableArray *buckets;
15 -@property (retain, nonatomic) NSString *currentContent;  
16 -@property (retain, nonatomic) NSString *currentElement;  
17 @property (retain, nonatomic) ASIS3Bucket *currentBucket; 15 @property (retain, nonatomic) ASIS3Bucket *currentBucket;
18 @property (retain, nonatomic) NSString *ownerID; 16 @property (retain, nonatomic) NSString *ownerID;
19 @property (retain, nonatomic) NSString *ownerName; 17 @property (retain, nonatomic) NSString *ownerName;
  18 +
  19 +@property (retain, nonatomic) NSMutableArray *currentXMLElementStack;
  20 +@property (retain, nonatomic) NSString *currentXMLElementContent;
20 @end 21 @end
21 22
22 @implementation ASIS3ServiceRequest 23 @implementation ASIS3ServiceRequest
@@ -26,40 +27,30 @@ @@ -26,40 +27,30 @@
26 return [[[self alloc] initWithURL:[NSURL URLWithString:@"http://s3.amazonaws.com"]] autorelease]; 27 return [[[self alloc] initWithURL:[NSURL URLWithString:@"http://s3.amazonaws.com"]] autorelease];
27 } 28 }
28 29
  30 +
  31 +- (id)initWithURL:(NSURL *)newURL
  32 +{
  33 + self = [super initWithURL:newURL];
  34 + [self setBuckets:[[[NSMutableArray alloc] init] autorelease]];
  35 + return self;
  36 +}
  37 +
  38 +
29 - (void)dealloc 39 - (void)dealloc
30 { 40 {
31 [buckets release]; 41 [buckets release];
32 - [currentContent release];  
33 - [currentElement release];  
34 [currentBucket release]; 42 [currentBucket release];
35 [ownerID release]; 43 [ownerID release];
36 [ownerName release]; 44 [ownerName release];
37 [super dealloc]; 45 [super dealloc];
38 } 46 }
39 47
40 -- (NSArray *)allBuckets  
41 -{  
42 - if ([self buckets]) {  
43 - return [self buckets];  
44 - }  
45 - [self setBuckets:[[[NSMutableArray alloc] init] autorelease]];  
46 - NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:[self responseData]] autorelease];  
47 - [parser setDelegate:self];  
48 - [parser setShouldProcessNamespaces:NO];  
49 - [parser setShouldReportNamespacePrefixes:NO];  
50 - [parser setShouldResolveExternalEntities:NO];  
51 - [parser parse];  
52 - return [self buckets];  
53 -}  
54 -  
55 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 48 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
56 { 49 {
57 - [self setCurrentElement:elementName];  
58 -  
59 if ([elementName isEqualToString:@"Bucket"]) { 50 if ([elementName isEqualToString:@"Bucket"]) {
60 [self setCurrentBucket:[ASIS3Bucket bucketWithOwnerID:[self ownerID] ownerName:[self ownerName]]]; 51 [self setCurrentBucket:[ASIS3Bucket bucketWithOwnerID:[self ownerID] ownerName:[self ownerName]]];
61 } 52 }
62 - [self setCurrentContent:@""]; 53 + [super parser:parser didStartElement:elementName namespaceURI:namespaceURI qualifiedName:qName attributes:attributeDict];
63 } 54 }
64 55
65 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName 56 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
@@ -68,24 +59,22 @@ @@ -68,24 +59,22 @@
68 [[self buckets] addObject:[self currentBucket]]; 59 [[self buckets] addObject:[self currentBucket]];
69 [self setCurrentBucket:nil]; 60 [self setCurrentBucket:nil];
70 } else if ([elementName isEqualToString:@"Name"]) { 61 } else if ([elementName isEqualToString:@"Name"]) {
71 - [[self currentBucket] setName:[self currentContent]]; 62 + [[self currentBucket] setName:[self currentXMLElementContent]];
72 } else if ([elementName isEqualToString:@"CreationDate"]) { 63 } else if ([elementName isEqualToString:@"CreationDate"]) {
73 - [[self currentBucket] setCreationDate:[[ASIS3Request dateFormatter] dateFromString:[self currentContent]]]; 64 + [[self currentBucket] setCreationDate:[[ASIS3Request dateFormatter] dateFromString:[self currentXMLElementContent]]];
74 } else if ([elementName isEqualToString:@"ID"]) { 65 } else if ([elementName isEqualToString:@"ID"]) {
75 - [self setOwnerID:[self currentContent]]; 66 + [self setOwnerID:[self currentXMLElementContent]];
76 } else if ([elementName isEqualToString:@"DisplayName"]) { 67 } else if ([elementName isEqualToString:@"DisplayName"]) {
77 - [self setOwnerName:[self currentContent]]; 68 + [self setOwnerName:[self currentXMLElementContent]];
  69 + } else {
  70 + // Let ASIS3Request look for error messages
  71 + [super parser:parser didEndElement:elementName namespaceURI:namespaceURI qualifiedName:qName];
78 } 72 }
79 } 73 }
80 74
81 -- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string  
82 -{  
83 - [self setCurrentContent:[[self currentContent] stringByAppendingString:string]];  
84 -}  
85 -  
86 @synthesize buckets; 75 @synthesize buckets;
87 -@synthesize currentContent; 76 +@synthesize currentXMLElementContent;
88 -@synthesize currentElement; 77 +@synthesize currentXMLElementStack;
89 @synthesize currentBucket; 78 @synthesize currentBucket;
90 @synthesize ownerID; 79 @synthesize ownerID;
91 @synthesize ownerName; 80 @synthesize ownerName;
@@ -118,8 +118,6 @@ static NSString *bucket = @""; @@ -118,8 +118,6 @@ static NSString *bucket = @"";
118 118
119 - (void)testFailure 119 - (void)testFailure
120 { 120 {
121 - [self createTestBucket];  
122 -  
123 // Needs expanding to cover more failure states - this is just a test to ensure Amazon's error description is being added to the error 121 // Needs expanding to cover more failure states - this is just a test to ensure Amazon's error description is being added to the error
124 122
125 // We're actually going to try with the Amazon example details, but the request will fail because the date is old 123 // We're actually going to try with the Amazon example details, but the request will fail because the date is old
@@ -141,6 +139,33 @@ static NSString *bucket = @""; @@ -141,6 +139,33 @@ static NSString *bucket = @"";
141 success = ([[[request error] localizedDescription] isEqualToString:@"The difference between the request time and the current time is too large."]); 139 success = ([[[request error] localizedDescription] isEqualToString:@"The difference between the request time and the current time is too large."]);
142 GHAssertTrue(success,@"Generated error had the wrong description"); 140 GHAssertTrue(success,@"Generated error had the wrong description");
143 141
  142 + // Ensure a bucket request will correctly parse an error from S3
  143 + request = [ASIS3BucketRequest requestWithBucket:exampleBucket];
  144 + [request setDateString:dateString];
  145 + [request setSecretAccessKey:exampleSecretAccessKey];
  146 + [request setAccessKey:exampleAccessKey];
  147 + [request startSynchronous];
  148 + GHAssertNotNil([request error],@"Failed to generate an error when the request was not correctly signed");
  149 +
  150 + success = ([[request error] code] == ASIS3ResponseErrorType);
  151 + GHAssertTrue(success,@"Generated error had the wrong error code");
  152 +
  153 + success = ([[[request error] localizedDescription] isEqualToString:@"The difference between the request time and the current time is too large."]);
  154 + GHAssertTrue(success,@"Generated error had the wrong description");
  155 +
  156 + // Ensure a service request will correctly parse an error from S3
  157 + request = [ASIS3ServiceRequest serviceRequest];
  158 + [request setDateString:dateString];
  159 + [request setSecretAccessKey:exampleSecretAccessKey];
  160 + [request setAccessKey:exampleAccessKey];
  161 + [request startSynchronous];
  162 + GHAssertNotNil([request error],@"Failed to generate an error when the request was not correctly signed");
  163 +
  164 + success = ([[request error] code] == ASIS3ResponseErrorType);
  165 + GHAssertTrue(success,@"Generated error had the wrong error code");
  166 +
  167 + success = ([[[request error] localizedDescription] isEqualToString:@"The difference between the request time and the current time is too large."]);
  168 + GHAssertTrue(success,@"Generated error had the wrong description");
144 } 169 }
145 170
146 - (void)createTestBucket 171 - (void)createTestBucket
@@ -176,7 +201,7 @@ static NSString *bucket = @""; @@ -176,7 +201,7 @@ static NSString *bucket = @"";
176 GHAssertNil([serviceRequest error],@"Failed to fetch the list of buckets from S3"); 201 GHAssertNil([serviceRequest error],@"Failed to fetch the list of buckets from S3");
177 202
178 BOOL foundBucket = NO; 203 BOOL foundBucket = NO;
179 - for (ASIS3Bucket *theBucket in [serviceRequest allBuckets]) { 204 + for (ASIS3Bucket *theBucket in [serviceRequest buckets]) {
180 if ([[theBucket name] isEqualToString:bucket]) { 205 if ([[theBucket name] isEqualToString:bucket]) {
181 foundBucket = YES; 206 foundBucket = YES;
182 break; 207 break;
@@ -236,7 +261,7 @@ static NSString *bucket = @""; @@ -236,7 +261,7 @@ static NSString *bucket = @"";
236 [listRequest setAccessKey:accessKey]; 261 [listRequest setAccessKey:accessKey];
237 [listRequest startSynchronous]; 262 [listRequest startSynchronous];
238 GHAssertNil([listRequest error],@"Failed to download a list from S3"); 263 GHAssertNil([listRequest error],@"Failed to download a list from S3");
239 - success = [[listRequest bucketObjects] count]; 264 + success = [[listRequest objects] count];
240 GHAssertTrue(success,@"The file didn't show up in the list"); 265 GHAssertTrue(success,@"The file didn't show up in the list");
241 266
242 // Test again with a prefix query 267 // Test again with a prefix query
@@ -246,7 +271,7 @@ static NSString *bucket = @""; @@ -246,7 +271,7 @@ static NSString *bucket = @"";
246 [listRequest setAccessKey:accessKey]; 271 [listRequest setAccessKey:accessKey];
247 [listRequest startSynchronous]; 272 [listRequest startSynchronous];
248 GHAssertNil([listRequest error],@"Failed to download a list from S3"); 273 GHAssertNil([listRequest error],@"Failed to download a list from S3");
249 - success = [[listRequest bucketObjects] count]; 274 + success = [[listRequest objects] count];
250 GHAssertTrue(success,@"The file didn't show up in the list"); 275 GHAssertTrue(success,@"The file didn't show up in the list");
251 276
252 // DELETE the file 277 // DELETE the file
@@ -309,7 +334,7 @@ static NSString *bucket = @""; @@ -309,7 +334,7 @@ static NSString *bucket = @"";
309 [bucketRequest setSecretAccessKey:secretAccessKey]; 334 [bucketRequest setSecretAccessKey:secretAccessKey];
310 [bucketRequest setAccessKey:accessKey]; 335 [bucketRequest setAccessKey:accessKey];
311 [bucketRequest startSynchronous]; 336 [bucketRequest startSynchronous];
312 - GHAssertNil([bucketRequest error],@"Failed to create a bucket"); 337 + GHAssertNil([bucketRequest error],@"Failed to delete a bucket");
313 338
314 339
315 } 340 }
@@ -387,14 +412,52 @@ static NSString *bucket = @""; @@ -387,14 +412,52 @@ static NSString *bucket = @"";
387 GHAssertNil([request error],@"Give up on list request test - failed to upload a file"); 412 GHAssertNil([request error],@"Give up on list request test - failed to upload a file");
388 } 413 }
389 414
390 - // Now get a list of the files 415 + // Test common prefixes
391 ASIS3BucketRequest *listRequest = [ASIS3BucketRequest requestWithBucket:bucket]; 416 ASIS3BucketRequest *listRequest = [ASIS3BucketRequest requestWithBucket:bucket];
  417 + [listRequest setSecretAccessKey:secretAccessKey];
  418 + [listRequest setAccessKey:accessKey];
  419 + [listRequest setDelimiter:@"/"];
  420 + [listRequest startSynchronous];
  421 + GHAssertNil([listRequest error],@"Failed to download a list from S3");
  422 + success = NO;
  423 + for (NSString *prefix in [listRequest commonPrefixes]) {
  424 + if ([prefix isEqualToString:@"test-file/"]) {
  425 + success = YES;
  426 + }
  427 + }
  428 + GHAssertTrue(success,@"Failed to obtain a list of common prefixes");
  429 +
  430 +
  431 + // Test truncation
  432 + listRequest = [ASIS3BucketRequest requestWithBucket:bucket];
  433 + [listRequest setSecretAccessKey:secretAccessKey];
  434 + [listRequest setAccessKey:accessKey];
  435 + [listRequest setMaxResultCount:1];
  436 + [listRequest startSynchronous];
  437 + GHAssertTrue([listRequest isTruncated],@"Failed to identify what should be a truncated list of results");
  438 +
  439 + // Test urls are built correctly when requesting a subresource
  440 + listRequest = [ASIS3BucketRequest requestWithBucket:bucket subResource:@"acl"];
  441 + [listRequest setSecretAccessKey:secretAccessKey];
  442 + [listRequest setAccessKey:accessKey];
  443 + [listRequest setDelimiter:@"/"];
  444 + [listRequest setPrefix:@"foo"];
  445 + [listRequest setMarker:@"bar"];
  446 + [listRequest setMaxResultCount:5];
  447 + [listRequest createQueryString];
  448 + NSString *expectedURL = [NSString stringWithFormat:@"http://%@.s3.amazonaws.com/?acl&prefix=foo&key-marker=bar&delimiter=/&max-keys=5",bucket];
  449 + success = ([[[listRequest url] absoluteString] isEqualToString:expectedURL]);
  450 + GHAssertTrue(success,@"Generated the wrong url when requesting a subresource");
  451 +
  452 +
  453 + // Now get a list of the files
  454 + listRequest = [ASIS3BucketRequest requestWithBucket:bucket];
392 [listRequest setPrefix:@"test-file"]; 455 [listRequest setPrefix:@"test-file"];
393 [listRequest setSecretAccessKey:secretAccessKey]; 456 [listRequest setSecretAccessKey:secretAccessKey];
394 [listRequest setAccessKey:accessKey]; 457 [listRequest setAccessKey:accessKey];
395 [listRequest startSynchronous]; 458 [listRequest startSynchronous];
396 GHAssertNil([listRequest error],@"Failed to download a list from S3"); 459 GHAssertNil([listRequest error],@"Failed to download a list from S3");
397 - success = ([[listRequest bucketObjects] count] == 5); 460 + success = ([[listRequest objects] count] == 5);
398 GHAssertTrue(success,@"List did not contain all files"); 461 GHAssertTrue(success,@"List did not contain all files");
399 462
400 // Please don't use an autoreleased operation queue with waitUntilAllOperationsAreFinished in your own code unless you're writing a test like this one 463 // Please don't use an autoreleased operation queue with waitUntilAllOperationsAreFinished in your own code unless you're writing a test like this one
@@ -405,7 +468,7 @@ static NSString *bucket = @""; @@ -405,7 +468,7 @@ static NSString *bucket = @"";
405 [queue setRequestDidFinishSelector:@selector(GETRequestDone:)]; 468 [queue setRequestDidFinishSelector:@selector(GETRequestDone:)];
406 [queue setRequestDidFailSelector:@selector(GETRequestFailed:)]; 469 [queue setRequestDidFailSelector:@selector(GETRequestFailed:)];
407 [queue setDelegate:self]; 470 [queue setDelegate:self];
408 - for (ASIS3BucketObject *object in [listRequest bucketObjects]) { 471 + for (ASIS3BucketObject *object in [listRequest objects]) {
409 ASIS3ObjectRequest *request = [object GETRequest]; 472 ASIS3ObjectRequest *request = [object GETRequest];
410 [request setAccessKey:accessKey]; 473 [request setAccessKey:accessKey];
411 [request setSecretAccessKey:secretAccessKey]; 474 [request setSecretAccessKey:secretAccessKey];
@@ -421,7 +484,7 @@ static NSString *bucket = @""; @@ -421,7 +484,7 @@ static NSString *bucket = @"";
421 [queue setDelegate:self]; 484 [queue setDelegate:self];
422 i=0; 485 i=0;
423 // For each one, we'll just upload the same content again 486 // For each one, we'll just upload the same content again
424 - for (ASIS3BucketObject *object in [listRequest bucketObjects]) { 487 + for (ASIS3BucketObject *object in [listRequest objects]) {
425 NSString *oldFilePath = [[self filePathForTemporaryTestFiles] stringByAppendingPathComponent:[NSString stringWithFormat:@"%hi.txt",i]];; 488 NSString *oldFilePath = [[self filePathForTemporaryTestFiles] stringByAppendingPathComponent:[NSString stringWithFormat:@"%hi.txt",i]];;
426 ASIS3Request *request = [object PUTRequestWithFile:oldFilePath]; 489 ASIS3Request *request = [object PUTRequestWithFile:oldFilePath];
427 [request setAccessKey:accessKey]; 490 [request setAccessKey:accessKey];
@@ -439,7 +502,7 @@ static NSString *bucket = @""; @@ -439,7 +502,7 @@ static NSString *bucket = @"";
439 [queue setDelegate:self]; 502 [queue setDelegate:self];
440 i=0; 503 i=0;
441 504
442 - for (ASIS3BucketObject *object in [listRequest bucketObjects]) { 505 + for (ASIS3BucketObject *object in [listRequest objects]) {
443 ASIS3ObjectRequest *request = [object DELETERequest]; 506 ASIS3ObjectRequest *request = [object DELETERequest];
444 [request setAccessKey:accessKey]; 507 [request setAccessKey:accessKey];
445 [request setSecretAccessKey:secretAccessKey]; 508 [request setSecretAccessKey:secretAccessKey];
@@ -456,7 +519,7 @@ static NSString *bucket = @""; @@ -456,7 +519,7 @@ static NSString *bucket = @"";
456 [listRequest setAccessKey:accessKey]; 519 [listRequest setAccessKey:accessKey];
457 [listRequest startSynchronous]; 520 [listRequest startSynchronous];
458 GHAssertNil([listRequest error],@"Failed to download a list from S3"); 521 GHAssertNil([listRequest error],@"Failed to download a list from S3");
459 - success = ([[listRequest bucketObjects] count] == 0); 522 + success = ([[listRequest objects] count] == 0);
460 GHAssertTrue(success,@"List contained files that should have been deleted"); 523 GHAssertTrue(success,@"List contained files that should have been deleted");
461 524
462 } 525 }
@@ -685,6 +748,8 @@ static NSString *bucket = @""; @@ -685,6 +748,8 @@ static NSString *bucket = @"";
685 [bucketObject2 release]; 748 [bucketObject2 release];
686 } 749 }
687 750
  751 +
  752 +
688 @synthesize networkQueue; 753 @synthesize networkQueue;
689 754
690 @end 755 @end