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