Ben Copsey

Added gzip for request bodies

Comments / code tweaks
@@ -44,12 +44,15 @@ extern NSString* const NetworkRequestErrorDomain; @@ -44,12 +44,15 @@ extern NSString* const NetworkRequestErrorDomain;
44 // A queue delegate that should *ALSO* be notified of delegate message (used by ASINetworkQueue) 44 // A queue delegate that should *ALSO* be notified of delegate message (used by ASINetworkQueue)
45 id queue; 45 id queue;
46 46
47 - // HTTP method to use (GET / POST / PUT / DELETE). Defaults to GET 47 + // HTTP method to use (GET / POST / PUT / DELETE / HEAD). Defaults to GET
48 NSString *requestMethod; 48 NSString *requestMethod;
49 49
50 // Request body - only used when the whole body is stored in memory (shouldStreamPostDataFromDisk is false) 50 // Request body - only used when the whole body is stored in memory (shouldStreamPostDataFromDisk is false)
51 NSMutableData *postBody; 51 NSMutableData *postBody;
52 52
  53 + // gzipped request body used when shouldCompressRequestBody is YES
  54 + NSData *compressedPostBody;
  55 +
53 // When true, post body will be streamed from a file on disk, rather than loaded into memory at once (useful for large uploads) 56 // When true, post body will be streamed from a file on disk, rather than loaded into memory at once (useful for large uploads)
54 // Automatically set to true in ASIFormDataRequests when using setFile:forKey: 57 // Automatically set to true in ASIFormDataRequests when using setFile:forKey:
55 BOOL shouldStreamPostDataFromDisk; 58 BOOL shouldStreamPostDataFromDisk;
@@ -58,6 +61,9 @@ extern NSString* const NetworkRequestErrorDomain; @@ -58,6 +61,9 @@ extern NSString* const NetworkRequestErrorDomain;
58 // You can set this yourself - useful if you want to PUT a file from local disk 61 // You can set this yourself - useful if you want to PUT a file from local disk
59 NSString *postBodyFilePath; 62 NSString *postBodyFilePath;
60 63
  64 + // Path to a temporary file used to store a deflated post body (when shouldCompressPostBody is YES)
  65 + NSString *compressedPostBodyFilePath;
  66 +
61 // 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) 67 // 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)
62 BOOL didCreateTemporaryPostDataFile; 68 BOOL didCreateTemporaryPostDataFile;
63 69
@@ -91,6 +97,10 @@ extern NSString* const NetworkRequestErrorDomain; @@ -91,6 +97,10 @@ extern NSString* const NetworkRequestErrorDomain;
91 // If allowCompressedResponse is true, requests will inform the server they can accept compressed data, and will automatically decompress gzipped responses. Default is true. 97 // If allowCompressedResponse is true, requests will inform the server they can accept compressed data, and will automatically decompress gzipped responses. Default is true.
92 BOOL allowCompressedResponse; 98 BOOL allowCompressedResponse;
93 99
  100 + // If shouldCompressRequestBody is true, the request body will be gzipped. Default is false.
  101 + // You will probably need to enable this feature on your webserver to make this work. Tested with apache only.
  102 + BOOL shouldCompressRequestBody;
  103 +
94 // When downloadDestinationPath is set, the result of this request will be downloaded to the file at this location 104 // When downloadDestinationPath is set, the result of this request will be downloaded to the file at this location
95 // If downloadDestinationPath is not set, download data will be stored in memory 105 // If downloadDestinationPath is not set, download data will be stored in memory
96 NSString *downloadDestinationPath; 106 NSString *downloadDestinationPath;
@@ -203,11 +213,18 @@ extern NSString* const NetworkRequestErrorDomain; @@ -203,11 +213,18 @@ extern NSString* const NetworkRequestErrorDomain;
203 // Prevents the body of the post being built more than once (largely for subclasses) 213 // Prevents the body of the post being built more than once (largely for subclasses)
204 BOOL haveBuiltPostBody; 214 BOOL haveBuiltPostBody;
205 215
  216 + // Used internally, may reflect the size of the internal used by CFNetwork
  217 + // POST / PUT operations with body sizes greater than uploadBufferSize will not timeout unless more than uploadBufferSize bytes have been sent
  218 + // Likely to be 32KB on iPhone 3.0, 128KB on Mac OS X Leopard and iPhone 2.2.x
206 unsigned long long uploadBufferSize; 219 unsigned long long uploadBufferSize;
207 220
  221 + // Text encoding for responses that do not send a Content-Type with a charset value. Defaults to NSISOLatin1StringEncoding
208 NSStringEncoding defaultResponseEncoding; 222 NSStringEncoding defaultResponseEncoding;
  223 +
  224 + // The text encoding of the response, will be defaultResponseEncoding if the server didn't specify. Can't be set.
209 NSStringEncoding responseEncoding; 225 NSStringEncoding responseEncoding;
210 226
  227 + // Tells ASIHTTPRequest not to delete partial downloads, and allows it to use an existing file to resume a download. Defaults to NO.
211 BOOL allowResumeForFileDownloads; 228 BOOL allowResumeForFileDownloads;
212 229
213 // Custom user information assosiated with the request 230 // Custom user information assosiated with the request
@@ -360,7 +377,7 @@ extern NSString* const NetworkRequestErrorDomain; @@ -360,7 +377,7 @@ extern NSString* const NetworkRequestErrorDomain;
360 // Dump all session data (authentication and cookies) 377 // Dump all session data (authentication and cookies)
361 + (void)clearSession; 378 + (void)clearSession;
362 379
363 -#pragma mark gzip compression 380 +#pragma mark gzip decompression
364 381
365 // Uncompress gzipped data with zlib 382 // Uncompress gzipped data with zlib
366 + (NSData *)uncompressZippedData:(NSData*)compressedData; 383 + (NSData *)uncompressZippedData:(NSData*)compressedData;
@@ -369,6 +386,15 @@ extern NSString* const NetworkRequestErrorDomain; @@ -369,6 +386,15 @@ extern NSString* const NetworkRequestErrorDomain;
369 + (int)uncompressZippedDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath; 386 + (int)uncompressZippedDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath;
370 + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest; 387 + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest;
371 388
  389 +#pragma mark gzip compression
  390 +
  391 +// Compress data with gzip using zlib
  392 ++ (NSData *)compressData:(NSData*)uncompressedData;
  393 +
  394 +// gzip compress data from a file, saving to another file, used for uploading when shouldCompressRequestBody is true
  395 ++ (int)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath;
  396 ++ (int)compressDataFromSource:(FILE *)source toDestination:(FILE *)dest;
  397 +
372 @property (retain) NSString *username; 398 @property (retain) NSString *username;
373 @property (retain) NSString *password; 399 @property (retain) NSString *password;
374 @property (retain) NSString *domain; 400 @property (retain) NSString *domain;
@@ -417,4 +443,5 @@ extern NSString* const NetworkRequestErrorDomain; @@ -417,4 +443,5 @@ extern NSString* const NetworkRequestErrorDomain;
417 @property (assign, readonly) unsigned long long partialDownloadSize; 443 @property (assign, readonly) unsigned long long partialDownloadSize;
418 @property (assign) BOOL shouldRedirect; 444 @property (assign) BOOL shouldRedirect;
419 @property (assign) BOOL validatesSecureCertificate; 445 @property (assign) BOOL validatesSecureCertificate;
  446 +@property (assign) BOOL shouldCompressRequestBody;
420 @end 447 @end
@@ -70,6 +70,8 @@ static NSError *ASITooMuchRedirectionError; @@ -70,6 +70,8 @@ static NSError *ASITooMuchRedirectionError;
70 @property (assign, nonatomic) BOOL updatedProgress; 70 @property (assign, nonatomic) BOOL updatedProgress;
71 @property (assign, nonatomic) BOOL needsRedirect; 71 @property (assign, nonatomic) BOOL needsRedirect;
72 @property (assign, nonatomic) int redirectCount; 72 @property (assign, nonatomic) int redirectCount;
  73 + @property (retain, nonatomic) NSData *compressedPostBody;
  74 + @property (retain, nonatomic) NSString *compressedPostBodyFilePath;
73 @end 75 @end
74 76
75 @implementation ASIHTTPRequest 77 @implementation ASIHTTPRequest
@@ -133,6 +135,7 @@ static NSError *ASITooMuchRedirectionError; @@ -133,6 +135,7 @@ static NSError *ASITooMuchRedirectionError;
133 [userInfo release]; 135 [userInfo release];
134 [mainRequest release]; 136 [mainRequest release];
135 [postBody release]; 137 [postBody release];
  138 + [compressedPostBody release];
136 [requestCredentials release]; 139 [requestCredentials release];
137 [error release]; 140 [error release];
138 [requestHeaders release]; 141 [requestHeaders release];
@@ -154,6 +157,7 @@ static NSError *ASITooMuchRedirectionError; @@ -154,6 +157,7 @@ static NSError *ASITooMuchRedirectionError;
154 [cancelledLock release]; 157 [cancelledLock release];
155 [authenticationMethod release]; 158 [authenticationMethod release];
156 [postBodyFilePath release]; 159 [postBodyFilePath release];
  160 + [compressedPostBodyFilePath release];
157 [postBodyWriteStream release]; 161 [postBodyWriteStream release];
158 [postBodyReadStream release]; 162 [postBodyReadStream release];
159 [super dealloc]; 163 [super dealloc];
@@ -177,16 +181,28 @@ static NSError *ASITooMuchRedirectionError; @@ -177,16 +181,28 @@ static NSError *ASITooMuchRedirectionError;
177 // Are we submitting the request body from a file on disk 181 // Are we submitting the request body from a file on disk
178 if ([self postBodyFilePath]) { 182 if ([self postBodyFilePath]) {
179 183
180 - // If we were writing to the post body via appendPostData or appendPostDataFromFile, close the write tream 184 + // If we were writing to the post body via appendPostData or appendPostDataFromFile, close the write stream
181 if ([self postBodyWriteStream]) { 185 if ([self postBodyWriteStream]) {
182 [[self postBodyWriteStream] close]; 186 [[self postBodyWriteStream] close];
183 [self setPostBodyWriteStream:nil]; 187 [self setPostBodyWriteStream:nil];
184 } 188 }
  189 +
  190 + if ([self shouldCompressRequestBody]) {
  191 + [self setCompressedPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
  192 + [ASIHTTPRequest compressDataFromFile:[self postBodyFilePath] toFile:[self compressedPostBodyFilePath]];
  193 + [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self compressedPostBodyFilePath] traverseLink:NO] fileSize]];
  194 + } else {
185 [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self postBodyFilePath] traverseLink:NO] fileSize]]; 195 [self setPostLength:[[[NSFileManager defaultManager] fileAttributesAtPath:[self postBodyFilePath] traverseLink:NO] fileSize]];
  196 + }
186 197
187 // Otherwise, we have an in-memory request body 198 // Otherwise, we have an in-memory request body
188 } else { 199 } else {
189 - [self setPostLength:[postBody length]]; 200 + if ([self shouldCompressRequestBody]) {
  201 + [self setCompressedPostBody:[ASIHTTPRequest compressData:[self postBody]]];
  202 + [self setPostLength:[[self compressedPostBody] length]];
  203 + } else {
  204 + [self setPostLength:[[self postBody] length]];
  205 + }
190 } 206 }
191 207
192 if ([self postLength] > 0) 208 if ([self postLength] > 0)
@@ -194,7 +210,7 @@ static NSError *ASITooMuchRedirectionError; @@ -194,7 +210,7 @@ static NSError *ASITooMuchRedirectionError;
194 if (![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) { 210 if (![requestMethod isEqualToString:@"POST"] && ![requestMethod isEqualToString:@"PUT"]) {
195 [self setRequestMethod:@"POST"]; 211 [self setRequestMethod:@"POST"];
196 } 212 }
197 - [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",postLength]]; 213 + [self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",[self postLength]]];
198 } 214 }
199 [self setHaveBuiltPostBody:YES]; 215 [self setHaveBuiltPostBody:YES];
200 } 216 }
@@ -374,6 +390,11 @@ static NSError *ASITooMuchRedirectionError; @@ -374,6 +390,11 @@ static NSError *ASITooMuchRedirectionError;
374 [self addRequestHeader:@"Accept-Encoding" value:@"gzip"]; 390 [self addRequestHeader:@"Accept-Encoding" value:@"gzip"];
375 } 391 }
376 392
  393 + // Configure a compressed request body
  394 + if ([self shouldCompressRequestBody]) {
  395 + [self addRequestHeader:@"Content-Encoding" value:@"gzip"];
  396 + }
  397 +
377 // Should this request resume an existing download? 398 // Should this request resume an existing download?
378 if ([self allowResumeForFileDownloads] && [self downloadDestinationPath] && [self temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[self temporaryFileDownloadPath]]) { 399 if ([self allowResumeForFileDownloads] && [self downloadDestinationPath] && [self temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[self temporaryFileDownloadPath]]) {
379 [self setPartialDownloadSize:[[[NSFileManager defaultManager] fileAttributesAtPath:[self temporaryFileDownloadPath] traverseLink:NO] fileSize]]; 400 [self setPartialDownloadSize:[[[NSFileManager defaultManager] fileAttributesAtPath:[self temporaryFileDownloadPath] traverseLink:NO] fileSize]];
@@ -394,9 +415,11 @@ static NSError *ASITooMuchRedirectionError; @@ -394,9 +415,11 @@ static NSError *ASITooMuchRedirectionError;
394 CFHTTPMessageSetHeaderFieldValue(request, (CFStringRef)header, (CFStringRef)[requestHeaders objectForKey:header]); 415 CFHTTPMessageSetHeaderFieldValue(request, (CFStringRef)header, (CFStringRef)[requestHeaders objectForKey:header]);
395 } 416 }
396 417
397 - // If this is a post request and we have data in memory send, add it to the request 418 + // If this is a post/put request and we store the request body in memory, add it to the request
398 - if ([self postBody]) { 419 + if ([self shouldCompressRequestBody] && [self compressedPostBody]) {
399 - CFHTTPMessageSetBody(request, (CFDataRef)postBody); 420 + CFHTTPMessageSetBody(request, (CFDataRef)[self compressedPostBody]);
  421 + } else if ([self postBody]) {
  422 + CFHTTPMessageSetBody(request, (CFDataRef)[self postBody]);
400 } 423 }
401 424
402 [self loadRequest]; 425 [self loadRequest];
@@ -435,8 +458,13 @@ static NSError *ASITooMuchRedirectionError; @@ -435,8 +458,13 @@ static NSError *ASITooMuchRedirectionError;
435 } 458 }
436 // Create the stream for the request. 459 // Create the stream for the request.
437 if ([self shouldStreamPostDataFromDisk] && [self postBodyFilePath] && [[NSFileManager defaultManager] fileExistsAtPath:[self postBodyFilePath]]) { 460 if ([self shouldStreamPostDataFromDisk] && [self postBodyFilePath] && [[NSFileManager defaultManager] fileExistsAtPath:[self postBodyFilePath]]) {
  461 +
  462 + // Are we gzipping the request body?
  463 + if ([self compressedPostBodyFilePath] && [[NSFileManager defaultManager] fileExistsAtPath:[self compressedPostBodyFilePath]]) {
  464 + [self setPostBodyReadStream:[[[NSInputStream alloc] initWithFileAtPath:[self compressedPostBodyFilePath]] autorelease]];
  465 + } else {
438 [self setPostBodyReadStream:[[[NSInputStream alloc] initWithFileAtPath:[self postBodyFilePath]] autorelease]]; 466 [self setPostBodyReadStream:[[[NSInputStream alloc] initWithFileAtPath:[self postBodyFilePath]] autorelease]];
439 - [[self postBodyReadStream] open]; 467 + }
440 readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,(CFReadStreamRef)[self postBodyReadStream]); 468 readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,(CFReadStreamRef)[self postBodyReadStream]);
441 } else { 469 } else {
442 readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request); 470 readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
@@ -626,14 +654,22 @@ static NSError *ASITooMuchRedirectionError; @@ -626,14 +654,22 @@ static NSError *ASITooMuchRedirectionError;
626 654
627 - (void)removePostDataFile 655 - (void)removePostDataFile
628 { 656 {
629 - if (postBodyFilePath) { 657 + if ([self postBodyFilePath]) {
630 NSError *removeError = nil; 658 NSError *removeError = nil;
631 - [[NSFileManager defaultManager] removeItemAtPath:postBodyFilePath error:&removeError]; 659 + [[NSFileManager defaultManager] removeItemAtPath:[self postBodyFilePath] error:&removeError];
632 if (removeError) { 660 if (removeError) {
633 - [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]]]; 661 + [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIFileManagementError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Failed to delete file at %@ with error: %@",[self postBodyFilePath],removeError],NSLocalizedDescriptionKey,removeError,NSUnderlyingErrorKey,nil]]];
634 } 662 }
635 [self setPostBodyFilePath:nil]; 663 [self setPostBodyFilePath:nil];
636 } 664 }
  665 + if ([self compressedPostBodyFilePath]) {
  666 + NSError *removeError = nil;
  667 + [[NSFileManager defaultManager] removeItemAtPath:[self compressedPostBodyFilePath] error:&removeError];
  668 + if (removeError) {
  669 + [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIFileManagementError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Failed to delete file at %@ with error: %@",[self compressedPostBodyFilePath],removeError],NSLocalizedDescriptionKey,removeError,NSUnderlyingErrorKey,nil]]];
  670 + }
  671 + [self setCompressedPostBodyFilePath:nil];
  672 + }
637 } 673 }
638 674
639 675
@@ -708,7 +744,7 @@ static NSError *ASITooMuchRedirectionError; @@ -708,7 +744,7 @@ static NSError *ASITooMuchRedirectionError;
708 return; 744 return;
709 } 745 }
710 746
711 - // 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 2.2.1, 32KB on iPhone 3.0) 747 + // 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 Leopard and iPhone 2.2.1, 32KB on iPhone 3.0)
712 // If request body is less than the buffer size, byteCount will be the total size of the request body 748 // If request body is less than the buffer size, byteCount will be the total size of the request body
713 // We will remove this from any progress display, as kCFStreamPropertyHTTPRequestBytesWrittenCount does not tell us how much data has actually be written 749 // We will remove this from any progress display, as kCFStreamPropertyHTTPRequestBytesWrittenCount does not tell us how much data has actually be written
714 if (totalBytesSent > 0 && uploadBufferSize == 0 && totalBytesSent != postLength) { 750 if (totalBytesSent > 0 && uploadBufferSize == 0 && totalBytesSent != postLength) {
@@ -1258,6 +1294,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1258,6 +1294,7 @@ static NSError *ASITooMuchRedirectionError;
1258 1294
1259 - (void)handleNetworkEvent:(CFStreamEventType)type 1295 - (void)handleNetworkEvent:(CFStreamEventType)type
1260 { 1296 {
  1297 + NSLog(@"%hi",type);
1261 // Dispatch the stream events. 1298 // Dispatch the stream events.
1262 switch (type) { 1299 switch (type) {
1263 case kCFStreamEventHasBytesAvailable: 1300 case kCFStreamEventHasBytesAvailable:
@@ -1334,7 +1371,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1334,7 +1371,7 @@ static NSError *ASITooMuchRedirectionError;
1334 1371
1335 - (void)handleStreamComplete 1372 - (void)handleStreamComplete
1336 { 1373 {
1337 - //Try to read the headers (if this is a HEAD request handleBytesAvailable available may not be called) 1374 + //Try to read the headers (if this is a HEAD request handleBytesAvailable may not be called)
1338 if (![self responseHeaders]) { 1375 if (![self responseHeaders]) {
1339 if ([self readResponseHeadersReturningAuthenticationFailure]) { 1376 if ([self readResponseHeadersReturningAuthenticationFailure]) {
1340 [self attemptToApplyCredentialsAndResume]; 1377 [self attemptToApplyCredentialsAndResume];
@@ -1536,7 +1573,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1536,7 +1573,7 @@ static NSError *ASITooMuchRedirectionError;
1536 } 1573 }
1537 1574
1538 1575
1539 -#pragma mark gzip data handling 1576 +#pragma mark gzip decompression
1540 1577
1541 // 1578 //
1542 // Contributed by Shaun Harrison of Enormego, see: http://developers.enormego.com/view/asihttprequest_gzip 1579 // Contributed by Shaun Harrison of Enormego, see: http://developers.enormego.com/view/asihttprequest_gzip
@@ -1589,24 +1626,29 @@ static NSError *ASITooMuchRedirectionError; @@ -1589,24 +1626,29 @@ static NSError *ASITooMuchRedirectionError;
1589 } 1626 }
1590 } 1627 }
1591 1628
1592 - 1629 +// NOTE: To debug this method, turn off Data Formatters in Xcode or you'll crash on closeFile
1593 + (int)uncompressZippedDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath 1630 + (int)uncompressZippedDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath
1594 { 1631 {
1595 - // Get a FILE struct for the source file  
1596 - FILE *source = fdopen([[NSFileHandle fileHandleForReadingAtPath:sourcePath] fileDescriptor], "r");  
1597 -  
1598 // Create an empty file at the destination path 1632 // Create an empty file at the destination path
1599 [[NSFileManager defaultManager] createFileAtPath:destinationPath contents:[NSData data] attributes:nil]; 1633 [[NSFileManager defaultManager] createFileAtPath:destinationPath contents:[NSData data] attributes:nil];
1600 1634
  1635 + // Get a FILE struct for the source file
  1636 + NSFileHandle *inputFileHandle = [NSFileHandle fileHandleForReadingAtPath:sourcePath];
  1637 + FILE *source = fdopen([inputFileHandle fileDescriptor], "r");
  1638 +
1601 // Get a FILE struct for the destination path 1639 // Get a FILE struct for the destination path
1602 - FILE *dest = fdopen([[NSFileHandle fileHandleForWritingAtPath:destinationPath] fileDescriptor], "w"); 1640 + NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:destinationPath];
  1641 + FILE *dest = fdopen([outputFileHandle fileDescriptor], "w");
  1642 +
1603 1643
1604 // Uncompress data in source and save in destination 1644 // Uncompress data in source and save in destination
1605 int status = [ASIHTTPRequest uncompressZippedDataFromSource:source toDestination:dest]; 1645 int status = [ASIHTTPRequest uncompressZippedDataFromSource:source toDestination:dest];
1606 1646
1607 // Close the files 1647 // Close the files
1608 - fclose(source);  
1609 fclose(dest); 1648 fclose(dest);
  1649 + fclose(source);
  1650 + [inputFileHandle closeFile];
  1651 + [outputFileHandle closeFile];
1610 return status; 1652 return status;
1611 } 1653 }
1612 1654
@@ -1615,6 +1657,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1615,6 +1657,7 @@ static NSError *ASITooMuchRedirectionError;
1615 // http://www.zlib.net/zpipe.c 1657 // http://www.zlib.net/zpipe.c
1616 // 1658 //
1617 #define CHUNK 16384 1659 #define CHUNK 16384
  1660 +#define SET_BINARY_MODE(file)
1618 + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest 1661 + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest
1619 { 1662 {
1620 int ret; 1663 int ret;
@@ -1673,6 +1716,134 @@ static NSError *ASITooMuchRedirectionError; @@ -1673,6 +1716,134 @@ static NSError *ASITooMuchRedirectionError;
1673 return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; 1716 return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;
1674 } 1717 }
1675 1718
  1719 +
  1720 +#pragma mark gzip compression
  1721 +
  1722 +// Based on this from Robbie Hanson: http://deusty.blogspot.com/2007/07/gzip-compressiondecompression.html
  1723 +
  1724 ++ (NSData *)compressData:(NSData*)uncompressedData
  1725 +{
  1726 + if ([uncompressedData length] == 0) return uncompressedData;
  1727 +
  1728 + z_stream strm;
  1729 +
  1730 + strm.zalloc = Z_NULL;
  1731 + strm.zfree = Z_NULL;
  1732 + strm.opaque = Z_NULL;
  1733 + strm.total_out = 0;
  1734 + strm.next_in=(Bytef *)[uncompressedData bytes];
  1735 + strm.avail_in = [uncompressedData length];
  1736 +
  1737 + // Compresssion Levels:
  1738 + // Z_NO_COMPRESSION
  1739 + // Z_BEST_SPEED
  1740 + // Z_BEST_COMPRESSION
  1741 + // Z_DEFAULT_COMPRESSION
  1742 +
  1743 + if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
  1744 +
  1745 + NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
  1746 +
  1747 + do {
  1748 +
  1749 + if (strm.total_out >= [compressed length])
  1750 + [compressed increaseLengthBy: 16384];
  1751 +
  1752 + strm.next_out = [compressed mutableBytes] + strm.total_out;
  1753 + strm.avail_out = [compressed length] - strm.total_out;
  1754 +
  1755 + deflate(&strm, Z_FINISH);
  1756 +
  1757 + } while (strm.avail_out == 0);
  1758 +
  1759 + deflateEnd(&strm);
  1760 +
  1761 + [compressed setLength: strm.total_out];
  1762 + return [NSData dataWithData:compressed];
  1763 +}
  1764 +
  1765 +// NOTE: To debug this method, turn off Data Formatters in Xcode or you'll crash on closeFile
  1766 ++ (int)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinationPath
  1767 +{
  1768 + // Create an empty file at the destination path
  1769 + [[NSFileManager defaultManager] createFileAtPath:destinationPath contents:[NSData data] attributes:nil];
  1770 +
  1771 + // Get a FILE struct for the source file
  1772 + NSFileHandle *inputFileHandle = [NSFileHandle fileHandleForReadingAtPath:sourcePath];
  1773 + FILE *source = fdopen([inputFileHandle fileDescriptor], "r");
  1774 +
  1775 + // Get a FILE struct for the destination path
  1776 + NSFileHandle *outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:destinationPath];
  1777 + FILE *dest = fdopen([outputFileHandle fileDescriptor], "w");
  1778 +
  1779 + // compress data in source and save in destination
  1780 + int status = [ASIHTTPRequest compressDataFromSource:source toDestination:dest];
  1781 +
  1782 + // Close the files
  1783 + fclose(dest);
  1784 + fclose(source);
  1785 +
  1786 + // We have to close both of these explictly because CFReadStreamCreateForStreamedHTTPRequest() seems to go bonkers otherwise
  1787 + [inputFileHandle closeFile];
  1788 + [outputFileHandle closeFile];
  1789 +
  1790 + return status;
  1791 +}
  1792 +
  1793 +//
  1794 +// Also from the zlib sample code at http://www.zlib.net/zpipe.c
  1795 +//
  1796 ++ (int)compressDataFromSource:(FILE *)source toDestination:(FILE *)dest
  1797 +{
  1798 + int ret, flush;
  1799 + unsigned have;
  1800 + z_stream strm;
  1801 + unsigned char in[CHUNK];
  1802 + unsigned char out[CHUNK];
  1803 +
  1804 + /* allocate deflate state */
  1805 + strm.zalloc = Z_NULL;
  1806 + strm.zfree = Z_NULL;
  1807 + strm.opaque = Z_NULL;
  1808 + ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY);
  1809 + if (ret != Z_OK)
  1810 + return ret;
  1811 +
  1812 + /* compress until end of file */
  1813 + do {
  1814 + strm.avail_in = fread(in, 1, CHUNK, source);
  1815 + if (ferror(source)) {
  1816 + (void)deflateEnd(&strm);
  1817 + return Z_ERRNO;
  1818 + }
  1819 + flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
  1820 + strm.next_in = in;
  1821 +
  1822 + /* run deflate() on input until output buffer not full, finish
  1823 + compression if all of source has been read in */
  1824 + do {
  1825 + strm.avail_out = CHUNK;
  1826 + strm.next_out = out;
  1827 + ret = deflate(&strm, flush); /* no bad return value */
  1828 + assert(ret != Z_STREAM_ERROR); /* state not clobbered */
  1829 + have = CHUNK - strm.avail_out;
  1830 + if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
  1831 + (void)deflateEnd(&strm);
  1832 + return Z_ERRNO;
  1833 + }
  1834 + } while (strm.avail_out == 0);
  1835 + assert(strm.avail_in == 0); /* all input will be used */
  1836 +
  1837 + /* done when last data in file processed */
  1838 + } while (flush != Z_FINISH);
  1839 + assert(ret == Z_STREAM_END); /* stream will be complete */
  1840 +
  1841 + /* clean up and return */
  1842 + (void)deflateEnd(&strm);
  1843 + return Z_OK;
  1844 +}
  1845 +
  1846 +
1676 @synthesize username; 1847 @synthesize username;
1677 @synthesize password; 1848 @synthesize password;
1678 @synthesize domain; 1849 @synthesize domain;
@@ -1702,6 +1873,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1702,6 +1873,7 @@ static NSError *ASITooMuchRedirectionError;
1702 @synthesize timeOutSeconds; 1873 @synthesize timeOutSeconds;
1703 @synthesize requestMethod; 1874 @synthesize requestMethod;
1704 @synthesize postBody; 1875 @synthesize postBody;
  1876 +@synthesize compressedPostBody;
1705 @synthesize contentLength; 1877 @synthesize contentLength;
1706 @synthesize partialDownloadSize; 1878 @synthesize partialDownloadSize;
1707 @synthesize postLength; 1879 @synthesize postLength;
@@ -1717,6 +1889,7 @@ static NSError *ASITooMuchRedirectionError; @@ -1717,6 +1889,7 @@ static NSError *ASITooMuchRedirectionError;
1717 @synthesize allowResumeForFileDownloads; 1889 @synthesize allowResumeForFileDownloads;
1718 @synthesize userInfo; 1890 @synthesize userInfo;
1719 @synthesize postBodyFilePath; 1891 @synthesize postBodyFilePath;
  1892 +@synthesize compressedPostBodyFilePath;
1720 @synthesize postBodyWriteStream; 1893 @synthesize postBodyWriteStream;
1721 @synthesize postBodyReadStream; 1894 @synthesize postBodyReadStream;
1722 @synthesize shouldStreamPostDataFromDisk; 1895 @synthesize shouldStreamPostDataFromDisk;
@@ -1733,4 +1906,5 @@ static NSError *ASITooMuchRedirectionError; @@ -1733,4 +1906,5 @@ static NSError *ASITooMuchRedirectionError;
1733 @synthesize validatesSecureCertificate; 1906 @synthesize validatesSecureCertificate;
1734 @synthesize needsRedirect; 1907 @synthesize needsRedirect;
1735 @synthesize redirectCount; 1908 @synthesize redirectCount;
  1909 +@synthesize shouldCompressRequestBody;
1736 @end 1910 @end
@@ -37,5 +37,6 @@ @@ -37,5 +37,6 @@
37 - (void)testTooMuchRedirection; 37 - (void)testTooMuchRedirection;
38 - (void)testRedirectToNewDomain; 38 - (void)testRedirectToNewDomain;
39 - (void)test303Redirect; 39 - (void)test303Redirect;
  40 +- (void)testCompression;
40 41
41 @end 42 @end
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 #import "ASIHTTPRequest.h" 10 #import "ASIHTTPRequest.h"
11 #import "ASINSStringAdditions.h" 11 #import "ASINSStringAdditions.h"
12 #import "ASINetworkQueue.h" 12 #import "ASINetworkQueue.h"
13 - 13 +#import "ASIFormDataRequest.h"
14 14
15 @implementation ASIHTTPRequestTests 15 @implementation ASIHTTPRequestTests
16 16
@@ -680,6 +680,58 @@ @@ -680,6 +680,58 @@
680 GHAssertTrue(success,@"Failed to use GET on new URL"); 680 GHAssertTrue(success,@"Failed to use GET on new URL");
681 } 681 }
682 682
  683 +- (void)testCompression
  684 +{
  685 + NSString *content = @"This is the test content. This is the test content. This is the test content. This is the test content.";
  686 +
  687 + // Test in memory compression / decompression
  688 + NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
  689 + NSData *compressedData = [ASIHTTPRequest compressData:data];
  690 + NSData *uncompressedData = [ASIHTTPRequest uncompressZippedData:compressedData];
  691 + NSString *newContent = [[[NSString alloc] initWithBytes:[uncompressedData bytes] length:[uncompressedData length] encoding:NSUTF8StringEncoding] autorelease];
  692 +
  693 + BOOL success = [newContent isEqualToString:content];
  694 + GHAssertTrue(success,@"Failed compress or decompress the correct data");
  695 +
  696 + // Test file to file compression / decompression
  697 + NSString *basePath = [[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent];
  698 + NSString *sourcePath = [basePath stringByAppendingPathComponent:@"text.txt"];
  699 + NSString *destPath = [basePath stringByAppendingPathComponent:@"text.txt.compressed"];
  700 + NSString *newPath = [basePath stringByAppendingPathComponent:@"text2.txt"];
  701 +
  702 + [content writeToFile:sourcePath atomically:NO encoding:NSUTF8StringEncoding error:NULL];
  703 + [ASIHTTPRequest compressDataFromFile:sourcePath toFile:destPath];
  704 + [ASIHTTPRequest uncompressZippedDataFromFile:destPath toFile:newPath];
  705 + success = [[NSString stringWithContentsOfFile:newPath encoding:NSUTF8StringEncoding error:NULL] isEqualToString:content];
  706 + GHAssertTrue(success,@"Failed compress or decompress the correct data");
  707 +
  708 + // Test compressed body
  709 + // Body is deflated by ASIHTTPRequest, sent, inflated by the server, printed, deflated by mod_deflate, response is inflated by ASIHTTPRequest
  710 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/compressed_post_body"]];
  711 + [request setRequestMethod:@"PUT"];
  712 + [request setShouldCompressRequestBody:YES];
  713 + [request appendPostData:data];
  714 + [request start];
  715 +
  716 + success = [[request responseString] isEqualToString:content];
  717 + GHAssertTrue(success,@"Failed to compress the body, or server failed to decompress it");
  718 +
  719 +
  720 + request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/compressed_post_body"]];
  721 + [request setRequestMethod:@"PUT"];
  722 + [request setShouldCompressRequestBody:YES];
  723 + [request setShouldStreamPostDataFromDisk:YES];
  724 + [request setUploadProgressDelegate:self];
  725 + [request setPostBodyFilePath:sourcePath];
  726 +
  727 + [request start];
  728 +
  729 + success = [[request responseString] isEqualToString:content];
  730 + GHAssertTrue(success,@"Failed to compress the body, or server failed to decompress it");
  731 +
  732 +
  733 +
  734 +}
683 735
684 736
685 @end 737 @end