Ben Copsey

Start work on test for file-streamed request body

Improve comments
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 // asi-http-request 3 // asi-http-request
4 // 4 //
5 // Created by Ben Copsey on 07/11/2008. 5 // Created by Ben Copsey on 07/11/2008.
6 -// Copyright 2008 All-Seeing Interactive. All rights reserved. 6 +// Copyright 2008-2009 All-Seeing Interactive. All rights reserved.
7 // 7 //
8 8
9 #import "ASIHTTPRequest.h" 9 #import "ASIHTTPRequest.h"
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 // asi-http-request 3 // asi-http-request
4 // 4 //
5 // Created by Ben Copsey on 07/11/2008. 5 // Created by Ben Copsey on 07/11/2008.
6 -// Copyright 2008 All-Seeing Interactive. All rights reserved. 6 +// Copyright 2008-2009 All-Seeing Interactive. All rights reserved.
7 // 7 //
8 8
9 #import "ASIFormDataRequest.h" 9 #import "ASIFormDataRequest.h"
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 // ASIHTTPRequest.h 2 // ASIHTTPRequest.h
3 // 3 //
4 // Created by Ben Copsey on 04/10/2007. 4 // Created by Ben Copsey on 04/10/2007.
5 -// Copyright 2007-2008 All-Seeing Interactive. All rights reserved. 5 +// Copyright 2007-2009 All-Seeing Interactive. All rights reserved.
6 // 6 //
7 // A guide to the main features is available at: 7 // A guide to the main features is available at:
8 // http://allseeing-i.com/ASIHTTPRequest 8 // http://allseeing-i.com/ASIHTTPRequest
@@ -40,9 +40,26 @@ typedef enum _ASINetworkErrorType { @@ -40,9 +40,26 @@ typedef enum _ASINetworkErrorType {
40 // HTTP method to use (GET / POST / PUT / DELETE). Defaults to GET 40 // HTTP method to use (GET / POST / PUT / DELETE). Defaults to GET
41 NSString *requestMethod; 41 NSString *requestMethod;
42 42
43 - // Request body 43 + // Request body - only used when the whole body is stored in memory (shouldStreamPostDataFromDisk is false)
44 NSMutableData *postBody; 44 NSMutableData *postBody;
45 45
  46 + // When true, post body will be streamed from a file on disk, rather than loaded into memory at once (useful for large uploads)
  47 + // Automatically set to true in ASIFormDataRequests when using setFile:forKey:
  48 + BOOL shouldStreamPostDataFromDisk;
  49 +
  50 + // Path to file used to store post body (when shouldStreamPostDataFromDisk is true)
  51 + // You can set this yourself - useful if you want to PUT a file from local disk
  52 + NSString *postBodyFilePath;
  53 +
  54 + // Set to true when ASIHTTPRequest automatically created a temporary file containing the request body (when true, the file at postBodyFilePath will be deleted at the end of the request)
  55 + BOOL didCreateTemporaryPostDataFile;
  56 +
  57 + // Used when writing to the post body when shouldStreamPostDataFromDisk is true (via appendPostData: or appendPostDataFromFile:)
  58 + NSOutputStream *postBodyWriteStream;
  59 +
  60 + // Used for reading from the post body when sending the request
  61 + NSInputStream *postBodyReadStream;
  62 +
46 // Dictionary for custom HTTP request headers 63 // Dictionary for custom HTTP request headers
47 NSMutableDictionary *requestHeaders; 64 NSMutableDictionary *requestHeaders;
48 65
@@ -192,10 +209,7 @@ typedef enum _ASINetworkErrorType { @@ -192,10 +209,7 @@ typedef enum _ASINetworkErrorType {
192 // Custom user information assosiated with the request 209 // Custom user information assosiated with the request
193 NSDictionary *userInfo; 210 NSDictionary *userInfo;
194 211
195 - NSString *postBodyFilePath; 212 +
196 - NSOutputStream *postBodyWriteStream;  
197 - NSInputStream *postBodyReadStream;  
198 - BOOL shouldStreamPostDataFromDisk;  
199 } 213 }
200 214
201 #pragma mark init / dealloc 215 #pragma mark init / dealloc
@@ -210,6 +224,7 @@ typedef enum _ASINetworkErrorType { @@ -210,6 +224,7 @@ typedef enum _ASINetworkErrorType {
210 224
211 - (void)buildPostBody; 225 - (void)buildPostBody;
212 226
  227 +// Called to add data to the post body. Will append to postBody when shouldStreamPostDataFromDisk is false, or write to postBodyWriteStream when true
213 - (void)appendPostData:(NSData *)data; 228 - (void)appendPostData:(NSData *)data;
214 - (void)appendPostDataFromFile:(NSString *)file; 229 - (void)appendPostDataFromFile:(NSString *)file;
215 230
@@ -239,6 +254,10 @@ typedef enum _ASINetworkErrorType { @@ -239,6 +254,10 @@ typedef enum _ASINetworkErrorType {
239 // No need to call this if the request succeeds - it is removed automatically 254 // No need to call this if the request succeeds - it is removed automatically
240 - (void)removeTemporaryDownloadFile; 255 - (void)removeTemporaryDownloadFile;
241 256
  257 +// Call to remove the file used as the request body
  258 +// No need to call this if the request succeeds and you didn't specify postBodyFilePath manually - it is removed automatically
  259 +- (void)removePostDataFile;
  260 +
242 #pragma mark upload/download progress 261 #pragma mark upload/download progress
243 262
244 // Called on main thread to update progress delegates 263 // Called on main thread to update progress delegates
@@ -371,4 +390,5 @@ typedef enum _ASINetworkErrorType { @@ -371,4 +390,5 @@ typedef enum _ASINetworkErrorType {
371 @property (retain) NSOutputStream *postBodyWriteStream; 390 @property (retain) NSOutputStream *postBodyWriteStream;
372 @property (retain) NSInputStream *postBodyReadStream; 391 @property (retain) NSInputStream *postBodyReadStream;
373 @property (assign) BOOL shouldStreamPostDataFromDisk; 392 @property (assign) BOOL shouldStreamPostDataFromDisk;
  393 +@property (assign) BOOL didCreateTemporaryPostDataFile;
374 @end 394 @end
@@ -56,6 +56,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -56,6 +56,7 @@ static NSError *ASIUnableToCreateRequestError;
56 [super initialize]; 56 [super initialize];
57 } 57 }
58 58
  59 +
59 - (id)initWithURL:(NSURL *)newURL 60 - (id)initWithURL:(NSURL *)newURL
60 { 61 {
61 self = [super init]; 62 self = [super init];
@@ -76,6 +77,11 @@ static NSError *ASIUnableToCreateRequestError; @@ -76,6 +77,11 @@ static NSError *ASIUnableToCreateRequestError;
76 requestAuthentication = NULL; 77 requestAuthentication = NULL;
77 haveBuiltPostBody = NO; 78 haveBuiltPostBody = NO;
78 request = NULL; 79 request = NULL;
  80 + [self setDidCreateTemporaryPostDataFile:NO];
  81 + [self setPostBodyFilePath:nil];
  82 + [self setPostBodyWriteStream:nil];
  83 + [self setPostBodyReadStream:nil];
  84 + [self setShouldStreamPostDataFromDisk:NO];
79 [self setAllowCompressedResponse:YES]; 85 [self setAllowCompressedResponse:YES];
80 [self setDefaultResponseEncoding:NSISOLatin1StringEncoding]; 86 [self setDefaultResponseEncoding:NSISOLatin1StringEncoding];
81 [self setUploadBufferSize:0]; 87 [self setUploadBufferSize:0];
@@ -130,6 +136,9 @@ static NSError *ASIUnableToCreateRequestError; @@ -130,6 +136,9 @@ static NSError *ASIUnableToCreateRequestError;
130 [requestMethod release]; 136 [requestMethod release];
131 [cancelledLock release]; 137 [cancelledLock release];
132 [authenticationMethod release]; 138 [authenticationMethod release];
  139 + [postBodyFilePath release];
  140 + [postBodyWriteStream release];
  141 + [postBodyReadStream release];
133 [super dealloc]; 142 [super dealloc];
134 } 143 }
135 144
@@ -145,34 +154,38 @@ static NSError *ASIUnableToCreateRequestError; @@ -145,34 +154,38 @@ static NSError *ASIUnableToCreateRequestError;
145 } 154 }
146 155
147 156
148 -// Subclasses should override this method if they need to create POST content for this request  
149 // This function will be called either just before a request starts, or when postLength is needed, whichever comes first 157 // This function will be called either just before a request starts, or when postLength is needed, whichever comes first
150 -// postLength must be set by the time this function is complete - calling setPostBody: will do this for you 158 +// postLength must be set by the time this function is complete
151 - (void)buildPostBody 159 - (void)buildPostBody
152 { 160 {
153 - if ([self postBodyFilePath] && [self postBodyWriteStream]) { 161 + // Are we submitting the request body from a file on disk
154 - [[self postBodyWriteStream] close]; 162 + if ([self postBodyFilePath]) {
155 - [self setPostBodyWriteStream:nil]; 163 +
156 - [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self postBodyFilePath] traverseLink:NO] fileSize]]; 164 + // If we were writing to the post body via appendPostData or appendPostDataFromFile, close the write tream
157 - [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]]; 165 + if ([self postBodyWriteStream]) {
158 - if (postBody && postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) { 166 + [[self postBodyWriteStream] close];
159 - [self setRequestMethod:@"POST"]; 167 + [self setPostBodyWriteStream:nil];
160 } 168 }
  169 + [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self postBodyFilePath] traverseLink:NO] fileSize]];
  170 +
  171 + // Otherwise, we have an in-memory request body
161 } else { 172 } else {
162 [self setPostLength:[postBody length]]; 173 [self setPostLength:[postBody length]];
163 - [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]]; 174 + }
164 - if (postBody && postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) { 175 + [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]];
165 - [self setRequestMethod:@"POST"]; 176 + if (postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) {
166 - } 177 + [self setRequestMethod:@"POST"];
167 } 178 }
168 haveBuiltPostBody = YES; 179 haveBuiltPostBody = YES;
169 } 180 }
170 181
  182 +// Sets up storage for the post body
171 - (void)setupPostBody 183 - (void)setupPostBody
172 { 184 {
173 if ([self shouldStreamPostDataFromDisk]) { 185 if ([self shouldStreamPostDataFromDisk]) {
174 if (![self postBodyFilePath]) { 186 if (![self postBodyFilePath]) {
175 [self setPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]]; 187 [self setPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
  188 + [self setDidCreateTemporaryPostDataFile:YES];
176 } 189 }
177 if (![self postBodyWriteStream]) { 190 if (![self postBodyWriteStream]) {
178 [self setPostBodyWriteStream:[[[NSOutputStream alloc] initToFileAtPath:[self postBodyFilePath] append:NO] autorelease]]; 191 [self setPostBodyWriteStream:[[[NSOutputStream alloc] initToFileAtPath:[self postBodyFilePath] append:NO] autorelease]];
@@ -234,7 +247,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -234,7 +247,7 @@ static NSError *ASIUnableToCreateRequestError;
234 } 247 }
235 248
236 249
237 -// Call this method to get the received data as an NSString. Don't use for Binary data! 250 +// Call this method to get the received data as an NSString. Don't use for binary data!
238 - (NSString *)responseString 251 - (NSString *)responseString
239 { 252 {
240 NSData *data = [self responseData]; 253 NSData *data = [self responseData];
@@ -341,7 +354,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -341,7 +354,7 @@ static NSError *ASIUnableToCreateRequestError;
341 // Add custom headers 354 // Add custom headers
342 NSDictionary *headers; 355 NSDictionary *headers;
343 356
344 - //Add headers from the main request if this is a HEAD request generated by an ASINetwork Queue 357 + //Add headers from the main request if this is a HEAD request generated by an ASINetworkQueue
345 if ([self mainRequest]) { 358 if ([self mainRequest]) {
346 headers = [mainRequest requestHeaders]; 359 headers = [mainRequest requestHeaders];
347 } else { 360 } else {
@@ -440,14 +453,11 @@ static NSError *ASIUnableToCreateRequestError; @@ -440,14 +453,11 @@ static NSError *ASIUnableToCreateRequestError;
440 } 453 }
441 } 454 }
442 455
443 -// Start the request 456 +// This is the 'main loop' for the request. Basically, it runs the runloop that our network stuff is attached to, and checks to see if we should cancel or timeout
444 - (void)loadRequest 457 - (void)loadRequest
445 { 458 {
446 -  
447 -  
448 [self startRequest]; 459 [self startRequest];
449 460
450 -  
451 // Record when the request started, so we can timeout if nothing happens 461 // Record when the request started, so we can timeout if nothing happens
452 [self setLastActivityTime:[NSDate date]]; 462 [self setLastActivityTime:[NSDate date]];
453 463
@@ -523,6 +533,11 @@ static NSError *ASIUnableToCreateRequestError; @@ -523,6 +533,11 @@ static NSError *ASIUnableToCreateRequestError;
523 } 533 }
524 } 534 }
525 535
  536 + // Clean up any temporary file used to store request body for streaming
  537 + if ([self didCreateTemporaryPostDataFile]) {
  538 + [self removePostDataFile];
  539 + }
  540 +
526 [self setResponseHeaders:nil]; 541 [self setResponseHeaders:nil];
527 [cancelledLock unlock]; 542 [cancelledLock unlock];
528 } 543 }
@@ -538,6 +553,15 @@ static NSError *ASIUnableToCreateRequestError; @@ -538,6 +553,15 @@ static NSError *ASIUnableToCreateRequestError;
538 } 553 }
539 } 554 }
540 555
  556 +- (void)removePostDataFile
  557 +{
  558 + NSError *removeError = nil;
  559 + [[NSFileManager defaultManager] removeItemAtPath:postBodyFilePath error:&removeError];
  560 + if (removeError) {
  561 + [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIFileManagementError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Failed to delete file at %@ with error: %@",postBodyFilePath,removeError],NSLocalizedDescriptionKey,removeError,NSUnderlyingErrorKey,nil]]];
  562 + }
  563 +}
  564 +
541 565
542 #pragma mark upload/download progress 566 #pragma mark upload/download progress
543 567
@@ -618,6 +642,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -618,6 +642,7 @@ static NSError *ASIUnableToCreateRequestError;
618 } 642 }
619 643
620 // If this is the first time we've written to the buffer, byteCount will be the size of the buffer (currently seems to be 128KB on both Mac and iPhone) 644 // If this is the first time we've written to the buffer, byteCount will be the size of the buffer (currently seems to be 128KB on both Mac and iPhone)
  645 + // If request body is less than 128KB, byteCount will be the total size of the request body
621 // We will remove this from any progress display, as kCFStreamPropertyHTTPRequestBytesWrittenCount does not tell us how much data has actually be written 646 // We will remove this from any progress display, as kCFStreamPropertyHTTPRequestBytesWrittenCount does not tell us how much data has actually be written
622 if (totalBytesSent > 0 && uploadBufferSize == 0 && totalBytesSent != postLength) { 647 if (totalBytesSent > 0 && uploadBufferSize == 0 && totalBytesSent != postLength) {
623 [self setUploadBufferSize:totalBytesSent]; 648 [self setUploadBufferSize:totalBytesSent];
@@ -637,6 +662,10 @@ static NSError *ASIUnableToCreateRequestError; @@ -637,6 +662,10 @@ static NSError *ASIUnableToCreateRequestError;
637 662
638 [cancelledLock unlock]; 663 [cancelledLock unlock];
639 664
  665 + if (totalBytesSent == 0) {
  666 + return;
  667 + }
  668 +
640 669
641 if (uploadProgressDelegate) { 670 if (uploadProgressDelegate) {
642 671
@@ -1222,6 +1251,11 @@ static NSError *ASIUnableToCreateRequestError; @@ -1222,6 +1251,11 @@ static NSError *ASIUnableToCreateRequestError;
1222 1251
1223 NSError *fileError = nil; 1252 NSError *fileError = nil;
1224 1253
  1254 + // Delete up the request body temporary file, if it exists
  1255 + if (didCreateTemporaryPostDataFile) {
  1256 + [self removePostDataFile];
  1257 + }
  1258 +
1225 // Close the output stream as we're done writing to the file 1259 // Close the output stream as we're done writing to the file
1226 if (temporaryFileDownloadPath) { 1260 if (temporaryFileDownloadPath) {
1227 [outputStream close]; 1261 [outputStream close];
@@ -1547,4 +1581,5 @@ static NSError *ASIUnableToCreateRequestError; @@ -1547,4 +1581,5 @@ static NSError *ASIUnableToCreateRequestError;
1547 @synthesize postBodyWriteStream; 1581 @synthesize postBodyWriteStream;
1548 @synthesize postBodyReadStream; 1582 @synthesize postBodyReadStream;
1549 @synthesize shouldStreamPostDataFromDisk; 1583 @synthesize shouldStreamPostDataFromDisk;
  1584 +@synthesize didCreateTemporaryPostDataFile;
1550 @end 1585 @end
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 // asi-http-request 3 // asi-http-request
4 // 4 //
5 // Created by Ben Copsey on 07/11/2008. 5 // Created by Ben Copsey on 07/11/2008.
6 -// Copyright 2008 All-Seeing Interactive. All rights reserved. 6 +// Copyright 2008-2009 All-Seeing Interactive. All rights reserved.
7 // 7 //
8 8
9 9
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 // asi-http-request 3 // asi-http-request
4 // 4 //
5 // Created by Ben Copsey on 07/11/2008. 5 // Created by Ben Copsey on 07/11/2008.
6 -// Copyright 2008 All-Seeing Interactive. All rights reserved. 6 +// Copyright 2008-2009 All-Seeing Interactive. All rights reserved.
7 // 7 //
8 8
9 #import "ASINetworkQueue.h" 9 #import "ASINetworkQueue.h"
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 13
14 14
15 @implementation ASIHTTPRequestTests 15 @implementation ASIHTTPRequestTests
16 - 16 +/*
17 17
18 - (void)testBasicDownload 18 - (void)testBasicDownload
19 { 19 {
@@ -188,6 +188,8 @@ @@ -188,6 +188,8 @@
188 GHAssertTrue(success,@"Failed to properly increment download progress %f != 1.0",progress); 188 GHAssertTrue(success,@"Failed to properly increment download progress %f != 1.0",progress);
189 } 189 }
190 190
  191 +*/
  192 +
191 193
192 - (void)setProgress:(float)newProgress; 194 - (void)setProgress:(float)newProgress;
193 { 195 {
@@ -207,7 +209,49 @@ @@ -207,7 +209,49 @@
207 GHAssertTrue(success,@"Failed to properly increment upload progress %f != 1.0",progress); 209 GHAssertTrue(success,@"Failed to properly increment upload progress %f != 1.0",progress);
208 } 210 }
209 211
  212 +- (void)testPostBodyStreamedFromDisk
  213 +{
  214 + NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com/ignore"];
  215 + NSString *requestContentPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"testfile.txt"];
  216 + [[NSMutableData dataWithLength:1024*32] writeToFile:requestContentPath atomically:NO];
  217 +
  218 + // Test using a user-specified file as the request body (useful for PUT)
  219 + progress = 0;
  220 + ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease];
  221 + [request setRequestMethod:@"PUT"];
  222 + [request setShouldStreamPostDataFromDisk:YES];
  223 + [request setUploadProgressDelegate:self];
  224 + [request setPostBodyFilePath:requestContentPath];
  225 + //[request start];
  226 +
  227 +
  228 + //Wait for 1 second
  229 + //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
  230 +
  231 + BOOL success = (progress > 0.95);
  232 + //GHAssertTrue(success,@"Failed to properly increment upload progress %f != 1.0",progress);
  233 +
  234 +
  235 + // Test building a request body by appending data
  236 + progress = 0;
  237 + request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease];
  238 + [request setShouldStreamPostDataFromDisk:YES];
  239 + [request setUploadProgressDelegate:self];
  240 +
  241 + NSData *d = [@"Below this will be the file I am posting:\r\n" dataUsingEncoding:NSUTF8StringEncoding];
  242 + [request appendPostData:[NSData dataWithBytes:[d bytes] length:[d length]]];
  243 + [request appendPostDataFromFile:requestContentPath];
  244 + [request start];
  245 +
  246 +
  247 + //Wait for 1 second
  248 + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
  249 +
  250 + success = (progress > 0.95);
  251 + GHAssertTrue(success,@"Failed to properly increment upload progress %f != 1.0",progress);
  252 +}
210 253
  254 +/*
211 255
212 256
213 - (void)testCookies 257 - (void)testCookies
@@ -530,6 +574,6 @@ @@ -530,6 +574,6 @@
530 success = success = (progress > 0.95); 574 success = success = (progress > 0.95);
531 GHAssertTrue(success,@"Failed to correctly display increment progress for a partial download"); 575 GHAssertTrue(success,@"Failed to correctly display increment progress for a partial download");
532 } 576 }
533 - 577 +*/
534 578
535 @end 579 @end