Added the ability to stream the postBody from local disk, drastically cutting me…
…mory use for large posts
Showing
4 changed files
with
114 additions
and
34 deletions
| @@ -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]; |
-
Please register or login to post a comment