Ben Copsey

Added the ability to stream the postBody from local disk, drastically cutting me…

…mory use for large posts
@@ -44,10 +44,7 @@ @@ -44,10 +44,7 @@
44 if (!fileData) { 44 if (!fileData) {
45 fileData = [[NSMutableDictionary alloc] init]; 45 fileData = [[NSMutableDictionary alloc] init];
46 } 46 }
47 - NSMutableDictionary *file = [[[NSMutableDictionary alloc] init] autorelease]; 47 + [fileData setValue:filePath forKey:key];
48 - [file setObject:[NSData dataWithContentsOfFile:filePath options:NSUncachedRead error:NULL] forKey:@"data"];  
49 - [file setObject:[filePath lastPathComponent] forKey:@"filename"];  
50 - [fileData setValue:file forKey:key];  
51 [self setRequestMethod:@"POST"]; 48 [self setRequestMethod:@"POST"];
52 } 49 }
53 50
@@ -69,15 +66,17 @@ @@ -69,15 +66,17 @@
69 [super buildPostBody]; 66 [super buildPostBody];
70 return; 67 return;
71 } 68 }
  69 + if ([fileData count] > 0) {
  70 + [self setShouldStreamPostDataFromDisk:YES];
  71 + }
72 72
73 - NSMutableData *body = [[[NSMutableData alloc] init] autorelease];  
74 73
75 // Set your own boundary string only if really obsessive. We don't bother to check if post data contains the boundary, since it's pretty unlikely that it does. 74 // Set your own boundary string only if really obsessive. We don't bother to check if post data contains the boundary, since it's pretty unlikely that it does.
76 NSString *stringBoundary = @"0xKhTmLbOuNdArY"; 75 NSString *stringBoundary = @"0xKhTmLbOuNdArY";
77 76
78 [self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",stringBoundary]]; 77 [self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",stringBoundary]];
79 78
80 - [body appendData:[[NSString stringWithFormat:@"--%@\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 79 + [self appendPostData:[[NSString stringWithFormat:@"--%@\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
81 80
82 // Adds post data 81 // Adds post data
83 NSData *endItemBoundary = [[NSString stringWithFormat:@"\r\n--%@\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]; 82 NSData *endItemBoundary = [[NSString stringWithFormat:@"\r\n--%@\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding];
@@ -85,11 +84,11 @@ @@ -85,11 +84,11 @@
85 NSString *key; 84 NSString *key;
86 int i=0; 85 int i=0;
87 while (key = [e nextObject]) { 86 while (key = [e nextObject]) {
88 - [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",key] dataUsingEncoding:NSUTF8StringEncoding]]; 87 + [self appendPostData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",key] dataUsingEncoding:NSUTF8StringEncoding]];
89 - [body appendData:[[postData objectForKey:key] dataUsingEncoding:NSUTF8StringEncoding]]; 88 + [self appendPostData:[[postData objectForKey:key] dataUsingEncoding:NSUTF8StringEncoding]];
90 i++; 89 i++;
91 if (i != [postData count] || [fileData count] > 0) { //Only add the boundary if this is not the last item in the post body 90 if (i != [postData count] || [fileData count] > 0) { //Only add the boundary if this is not the last item in the post body
92 - [body appendData:endItemBoundary]; 91 + [self appendPostData:endItemBoundary];
93 } 92 }
94 } 93 }
95 94
@@ -98,21 +97,18 @@ @@ -98,21 +97,18 @@
98 e = [fileData keyEnumerator]; 97 e = [fileData keyEnumerator];
99 i=0; 98 i=0;
100 while (key = [e nextObject]) { 99 while (key = [e nextObject]) {
101 - NSDictionary *fileInfo = [fileData objectForKey:key]; 100 + NSString *filePath = [fileData objectForKey:key];
102 - [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",key,[fileInfo objectForKey:@"filename"]] dataUsingEncoding:NSUTF8StringEncoding]]; 101 + [self appendPostData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",key,[filePath lastPathComponent]] dataUsingEncoding:NSUTF8StringEncoding]];
103 - [body appendData:contentTypeHeader]; 102 + [self appendPostData:contentTypeHeader];
104 - [body appendData: [fileInfo objectForKey:@"data"]]; 103 + [self appendPostDataFromFile:filePath];
105 i++; 104 i++;
106 // Only add the boundary if this is not the last item in the post body 105 // Only add the boundary if this is not the last item in the post body
107 if (i != [fileData count]) { 106 if (i != [fileData count]) {
108 - [body appendData:endItemBoundary]; 107 + [self appendPostData:endItemBoundary];
109 } 108 }
110 } 109 }
111 110
112 - [body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 111 + [self appendPostData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
113 -  
114 - [self setPostBody:body];  
115 -  
116 112
117 [super buildPostBody]; 113 [super buildPostBody];
118 } 114 }
@@ -41,7 +41,7 @@ typedef enum _ASINetworkErrorType { @@ -41,7 +41,7 @@ typedef enum _ASINetworkErrorType {
41 NSString *requestMethod; 41 NSString *requestMethod;
42 42
43 // Request body 43 // Request body
44 - NSData *postBody; 44 + NSMutableData *postBody;
45 45
46 // Dictionary for custom HTTP request headers 46 // Dictionary for custom HTTP request headers
47 NSMutableDictionary *requestHeaders; 47 NSMutableDictionary *requestHeaders;
@@ -191,6 +191,11 @@ typedef enum _ASINetworkErrorType { @@ -191,6 +191,11 @@ typedef enum _ASINetworkErrorType {
191 191
192 // Custom user information assosiated with the request 192 // Custom user information assosiated with the request
193 NSDictionary *userInfo; 193 NSDictionary *userInfo;
  194 +
  195 + NSString *postBodyFilePath;
  196 + NSOutputStream *postBodyWriteStream;
  197 + NSInputStream *postBodyReadStream;
  198 + BOOL shouldStreamPostDataFromDisk;
194 } 199 }
195 200
196 #pragma mark init / dealloc 201 #pragma mark init / dealloc
@@ -205,6 +210,9 @@ typedef enum _ASINetworkErrorType { @@ -205,6 +210,9 @@ typedef enum _ASINetworkErrorType {
205 210
206 - (void)buildPostBody; 211 - (void)buildPostBody;
207 212
  213 +- (void)appendPostData:(NSData *)data;
  214 +- (void)appendPostDataFromFile:(NSString *)file;
  215 +
208 #pragma mark get information about this request 216 #pragma mark get information about this request
209 217
210 // Returns the contents of the result as an NSString (not appropriate for binary data - used responseData instead) 218 // Returns the contents of the result as an NSString (not appropriate for binary data - used responseData instead)
@@ -344,7 +352,7 @@ typedef enum _ASINetworkErrorType { @@ -344,7 +352,7 @@ typedef enum _ASINetworkErrorType {
344 @property (retain) NSDate *lastActivityTime; 352 @property (retain) NSDate *lastActivityTime;
345 @property (assign) NSTimeInterval timeOutSeconds; 353 @property (assign) NSTimeInterval timeOutSeconds;
346 @property (retain) NSString *requestMethod; 354 @property (retain) NSString *requestMethod;
347 -@property (retain,setter=setPostBody:) NSData *postBody; 355 +@property (retain) NSMutableData *postBody;
348 @property (assign) unsigned long long contentLength; 356 @property (assign) unsigned long long contentLength;
349 @property (assign) unsigned long long partialDownloadSize; 357 @property (assign) unsigned long long partialDownloadSize;
350 @property (assign) unsigned long long postLength; 358 @property (assign) unsigned long long postLength;
@@ -359,4 +367,8 @@ typedef enum _ASINetworkErrorType { @@ -359,4 +367,8 @@ typedef enum _ASINetworkErrorType {
359 @property (assign) BOOL allowCompressedResponse; 367 @property (assign) BOOL allowCompressedResponse;
360 @property (assign) BOOL allowResumeForFileDownloads; 368 @property (assign) BOOL allowResumeForFileDownloads;
361 @property (retain) NSDictionary *userInfo; 369 @property (retain) NSDictionary *userInfo;
  370 +@property (retain) NSString *postBodyFilePath;
  371 +@property (retain) NSOutputStream *postBodyWriteStream;
  372 +@property (retain) NSInputStream *postBodyReadStream;
  373 +@property (assign) BOOL shouldStreamPostDataFromDisk;
362 @end 374 @end
@@ -144,23 +144,76 @@ static NSError *ASIUnableToCreateRequestError; @@ -144,23 +144,76 @@ static NSError *ASIUnableToCreateRequestError;
144 [requestHeaders setObject:value forKey:header]; 144 [requestHeaders setObject:value forKey:header];
145 } 145 }
146 146
147 --(void)setPostBody:(NSData *)body 147 +
  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
  150 +// postLength must be set by the time this function is complete - calling setPostBody: will do this for you
  151 +- (void)buildPostBody
148 { 152 {
149 - [postBody release]; 153 + if ([self postBodyFilePath] && [self postBodyWriteStream]) {
150 - postBody = [body retain]; 154 + [[self postBodyWriteStream] close];
151 - postLength = [postBody length]; 155 + [self setPostBodyWriteStream:nil];
  156 + [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self postBodyFilePath] traverseLink:NO] fileSize]];
  157 + [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]];
  158 + if (postBody && postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) {
  159 + [self setRequestMethod:@"POST"];
  160 + }
  161 + } else {
  162 + [self setPostLength:[postBody length]];
152 [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]]; 163 [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]];
153 if (postBody && postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) { 164 if (postBody && postLength > 0 && ![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) {
154 [self setRequestMethod:@"POST"]; 165 [self setRequestMethod:@"POST"];
155 } 166 }
  167 + }
  168 + haveBuiltPostBody = YES;
156 } 169 }
157 170
158 -// Subclasses should override this method if they need to create POST content for this request 171 +- (void)setupPostBody
159 -// This function will be called either just before a request starts, or when postLength is needed, whichever comes first  
160 -// postLength must be set by the time this function is complete - calling setPostBody: will do this for you  
161 -- (void)buildPostBody  
162 { 172 {
163 - haveBuiltPostBody = YES; 173 + if ([self shouldStreamPostDataFromDisk]) {
  174 + if (![self postBodyFilePath]) {
  175 + [self setPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
  176 + }
  177 + if (![self postBodyWriteStream]) {
  178 + [self setPostBodyWriteStream:[[[NSOutputStream alloc] initToFileAtPath:[self postBodyFilePath] append:NO] autorelease]];
  179 + [[self postBodyWriteStream] open];
  180 + }
  181 + } else {
  182 + if (![self postBody]) {
  183 + [self setPostBody:[[[NSMutableData alloc] init] autorelease]];
  184 + }
  185 + }
  186 +}
  187 +
  188 +- (void)appendPostData:(NSData *)data
  189 +{
  190 + [self setupPostBody];
  191 + if ([self shouldStreamPostDataFromDisk]) {
  192 + [[self postBodyWriteStream] write:[data bytes] maxLength:[data length]];
  193 + } else {
  194 + [[self postBody] appendData:data];
  195 + }
  196 +}
  197 +
  198 +- (void)appendPostDataFromFile:(NSString *)file
  199 +{
  200 + [self setupPostBody];
  201 + NSInputStream *stream = [[[NSInputStream alloc] initWithFileAtPath:file] autorelease];
  202 + [stream open];
  203 +
  204 + NSMutableData *d;
  205 + while ([stream hasBytesAvailable]) {
  206 + d = [[NSMutableData alloc] initWithLength:256*1024];
  207 + int bytesRead = [stream read:[d mutableBytes] maxLength:256*1024];
  208 + if ([self shouldStreamPostDataFromDisk]) {
  209 + [[self postBodyWriteStream] write:[d mutableBytes] maxLength:bytesRead];
  210 + } else {
  211 + NSLog(@"foo");
  212 + [[self postBody] appendData:[NSData dataWithBytes:[d mutableBytes] length:bytesRead]];
  213 + }
  214 + [d release];
  215 + }
  216 + [stream close];
164 } 217 }
165 218
166 #pragma mark get information about this request 219 #pragma mark get information about this request
@@ -300,7 +353,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -300,7 +353,7 @@ static NSError *ASIUnableToCreateRequestError;
300 } 353 }
301 354
302 355
303 - // If this is a post request and we have data to send, add it to the request 356 + // If this is a post request and we have data in memory send, add it to the request
304 if ([self postBody]) { 357 if ([self postBody]) {
305 CFHTTPMessageSetBody(request, (CFDataRef)postBody); 358 CFHTTPMessageSetBody(request, (CFDataRef)postBody);
306 } 359 }
@@ -339,7 +392,13 @@ static NSError *ASIUnableToCreateRequestError; @@ -339,7 +392,13 @@ static NSError *ASIUnableToCreateRequestError;
339 [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]]; 392 [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]];
340 } 393 }
341 // Create the stream for the request. 394 // Create the stream for the request.
342 - readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,readStream); 395 + if ([self shouldStreamPostDataFromDisk] && [self postBodyFilePath] && [[NSFileManager defaultManager] fileExistsAtPath:[self postBodyFilePath]]) {
  396 + [self setPostBodyReadStream:[[[NSInputStream alloc] initWithFileAtPath:[self postBodyFilePath]] autorelease]];
  397 + [[self postBodyReadStream] open];
  398 + readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,(CFReadStreamRef)[self postBodyReadStream]);
  399 + } else {
  400 + readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
  401 + }
343 if (!readStream) { 402 if (!readStream) {
344 [cancelledLock unlock]; 403 [cancelledLock unlock];
345 [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIInternalErrorWhileBuildingRequestType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create read stream",NSLocalizedDescriptionKey,nil]]]; 404 [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIInternalErrorWhileBuildingRequestType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create read stream",NSLocalizedDescriptionKey,nil]]];
@@ -1484,4 +1543,8 @@ static NSError *ASIUnableToCreateRequestError; @@ -1484,4 +1543,8 @@ static NSError *ASIUnableToCreateRequestError;
1484 @synthesize allowCompressedResponse; 1543 @synthesize allowCompressedResponse;
1485 @synthesize allowResumeForFileDownloads; 1544 @synthesize allowResumeForFileDownloads;
1486 @synthesize userInfo; 1545 @synthesize userInfo;
  1546 +@synthesize postBodyFilePath;
  1547 +@synthesize postBodyWriteStream;
  1548 +@synthesize postBodyReadStream;
  1549 +@synthesize shouldStreamPostDataFromDisk;
1487 @end 1550 @end
@@ -196,10 +196,19 @@ @@ -196,10 +196,19 @@
196 196
197 - (IBAction)postWithProgress:(id)sender 197 - (IBAction)postWithProgress:(id)sender
198 { 198 {
199 - //Create a 1mb file 199 + //Create a 10MB file
200 - NSMutableData *data = [NSMutableData dataWithLength:1024*1024]; 200 + NSMutableData *data = [NSMutableData dataWithLength:1024];
201 NSString *path = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"bigfile"]; 201 NSString *path = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"bigfile"];
202 - [data writeToFile:path atomically:NO]; 202 +
  203 + NSOutputStream *stream = [[[NSOutputStream alloc] initToFileAtPath:path append:NO] autorelease];
  204 + [stream open];
  205 + int i;
  206 + for (i=0; i<1024*10; i++) {
  207 + [stream write:[data mutableBytes] maxLength:[data length]];
  208 + }
  209 +
  210 + [stream close];
  211 +
203 212
204 [networkQueue cancelAllOperations]; 213 [networkQueue cancelAllOperations];
205 [networkQueue setShowAccurateProgress:YES]; 214 [networkQueue setShowAccurateProgress:YES];