Ben Copsey

Added the ability to resume large file downloads

@@ -179,6 +179,8 @@ typedef enum _ASINetworkErrorType { @@ -179,6 +179,8 @@ typedef enum _ASINetworkErrorType {
179 179
180 NSStringEncoding defaultResponseEncoding; 180 NSStringEncoding defaultResponseEncoding;
181 NSStringEncoding responseEncoding; 181 NSStringEncoding responseEncoding;
  182 +
  183 + BOOL allowResumeForFileDownloads;
182 } 184 }
183 185
184 #pragma mark init / dealloc 186 #pragma mark init / dealloc
@@ -311,7 +313,7 @@ typedef enum _ASINetworkErrorType { @@ -311,7 +313,7 @@ typedef enum _ASINetworkErrorType {
311 @property (assign) BOOL useKeychainPersistance; 313 @property (assign) BOOL useKeychainPersistance;
312 @property (assign) BOOL useSessionPersistance; 314 @property (assign) BOOL useSessionPersistance;
313 @property (retain) NSString *downloadDestinationPath; 315 @property (retain) NSString *downloadDestinationPath;
314 -@property (retain,readonly) NSString *temporaryFileDownloadPath; 316 +@property (retain) NSString *temporaryFileDownloadPath;
315 @property (assign) SEL didFinishSelector; 317 @property (assign) SEL didFinishSelector;
316 @property (assign) SEL didFailSelector; 318 @property (assign) SEL didFailSelector;
317 @property (retain,readonly) NSString *authenticationRealm; 319 @property (retain,readonly) NSString *authenticationRealm;
@@ -339,4 +341,5 @@ typedef enum _ASINetworkErrorType { @@ -339,4 +341,5 @@ typedef enum _ASINetworkErrorType {
339 @property (assign) NSStringEncoding defaultResponseEncoding; 341 @property (assign) NSStringEncoding defaultResponseEncoding;
340 @property (assign) NSStringEncoding responseEncoding; 342 @property (assign) NSStringEncoding responseEncoding;
341 @property (assign) BOOL allowCompressedResponse; 343 @property (assign) BOOL allowCompressedResponse;
  344 +@property (assign) BOOL allowResumeForFileDownloads;
342 @end 345 @end
@@ -81,9 +81,11 @@ static NSError *ASIUnableToCreateRequestError; @@ -81,9 +81,11 @@ static NSError *ASIUnableToCreateRequestError;
81 [self setUploadBufferSize:0]; 81 [self setUploadBufferSize:0];
82 [self setResponseHeaders:nil]; 82 [self setResponseHeaders:nil];
83 [self setTimeOutSeconds:10]; 83 [self setTimeOutSeconds:10];
  84 + [self setAllowResumeForFileDownloads:NO];
84 [self setUseKeychainPersistance:NO]; 85 [self setUseKeychainPersistance:NO];
85 [self setUseSessionPersistance:YES]; 86 [self setUseSessionPersistance:YES];
86 [self setUseCookiePersistance:YES]; 87 [self setUseCookiePersistance:YES];
  88 + [self setRawResponseData:nil];
87 [self setRequestCookies:[[[NSMutableArray alloc] init] autorelease]]; 89 [self setRequestCookies:[[[NSMutableArray alloc] init] autorelease]];
88 [self setDidFinishSelector:@selector(requestFinished:)]; 90 [self setDidFinishSelector:@selector(requestFinished:)];
89 [self setDidFailSelector:@selector(requestFailed:)]; 91 [self setDidFailSelector:@selector(requestFailed:)];
@@ -272,6 +274,12 @@ static NSError *ASIUnableToCreateRequestError; @@ -272,6 +274,12 @@ static NSError *ASIUnableToCreateRequestError;
272 [self addRequestHeader:@"Accept-Encoding" value:@"gzip"]; 274 [self addRequestHeader:@"Accept-Encoding" value:@"gzip"];
273 } 275 }
274 276
  277 + // Should this request resume an existing download?
  278 + if ([self allowResumeForFileDownloads] && [self downloadDestinationPath] && [self temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[self temporaryFileDownloadPath]]) {
  279 + unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:[self temporaryFileDownloadPath] traverseLink:NO] fileSize];
  280 + [self addRequestHeader:@"Range" value:[NSString stringWithFormat:@"bytes=%llu-",downloadedSoFar]];
  281 + }
  282 +
275 // Add custom headers 283 // Add custom headers
276 NSDictionary *headers; 284 NSDictionary *headers;
277 285
@@ -322,8 +330,9 @@ static NSError *ASIUnableToCreateRequestError; @@ -322,8 +330,9 @@ static NSError *ASIUnableToCreateRequestError;
322 contentLength = 0; 330 contentLength = 0;
323 } 331 }
324 [self setResponseHeaders:nil]; 332 [self setResponseHeaders:nil];
  333 + if (![self downloadDestinationPath]) {
325 [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]]; 334 [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]];
326 - 335 + }
327 // Create the stream for the request. 336 // Create the stream for the request.
328 readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,readStream); 337 readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,readStream);
329 if (!readStream) { 338 if (!readStream) {
@@ -431,11 +440,15 @@ static NSError *ASIUnableToCreateRequestError; @@ -431,11 +440,15 @@ static NSError *ASIUnableToCreateRequestError;
431 if (rawResponseData) { 440 if (rawResponseData) {
432 [self setRawResponseData:nil]; 441 [self setRawResponseData:nil];
433 442
434 - // If we were downloading to a file, let's remove it 443 + // If we were downloading to a file
435 } else if (temporaryFileDownloadPath) { 444 } else if (temporaryFileDownloadPath) {
436 [outputStream close]; 445 [outputStream close];
  446 +
  447 + // If we haven't said we might want to resume, let's remove the temporary file too
  448 + if (![self allowResumeForFileDownloads]) {
437 [[NSFileManager defaultManager] removeItemAtPath:temporaryFileDownloadPath error:NULL]; 449 [[NSFileManager defaultManager] removeItemAtPath:temporaryFileDownloadPath error:NULL];
438 } 450 }
  451 + }
439 452
440 [self setResponseHeaders:nil]; 453 [self setResponseHeaders:nil];
441 [cancelledLock unlock]; 454 [cancelledLock unlock];
@@ -755,6 +768,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -755,6 +768,7 @@ static NSError *ASIUnableToCreateRequestError;
755 BOOL isAuthenticationChallenge = NO; 768 BOOL isAuthenticationChallenge = NO;
756 CFHTTPMessageRef headers = (CFHTTPMessageRef)CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader); 769 CFHTTPMessageRef headers = (CFHTTPMessageRef)CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
757 if (CFHTTPMessageIsHeaderComplete(headers)) { 770 if (CFHTTPMessageIsHeaderComplete(headers)) {
  771 +
758 CFDictionaryRef headerFields = CFHTTPMessageCopyAllHeaderFields(headers); 772 CFDictionaryRef headerFields = CFHTTPMessageCopyAllHeaderFields(headers);
759 [self setResponseHeaders:(NSDictionary *)headerFields]; 773 [self setResponseHeaders:(NSDictionary *)headerFields];
760 CFRelease(headerFields); 774 CFRelease(headerFields);
@@ -1083,9 +1097,14 @@ static NSError *ASIUnableToCreateRequestError; @@ -1083,9 +1097,14 @@ static NSError *ASIUnableToCreateRequestError;
1083 // Are we downloading to a file? 1097 // Are we downloading to a file?
1084 if (downloadDestinationPath) { 1098 if (downloadDestinationPath) {
1085 if (!outputStream) { 1099 if (!outputStream) {
1086 - [temporaryFileDownloadPath release]; 1100 + BOOL append = NO;
1087 - temporaryFileDownloadPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]] retain]; 1101 + if (![self temporaryFileDownloadPath]) {
1088 - outputStream = [[NSOutputStream alloc] initToFileAtPath:temporaryFileDownloadPath append:NO]; 1102 + [self setTemporaryFileDownloadPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
  1103 + } else if ([self allowResumeForFileDownloads]) {
  1104 + append = YES;
  1105 + }
  1106 +
  1107 + outputStream = [[NSOutputStream alloc] initToFileAtPath:temporaryFileDownloadPath append:append];
1089 [outputStream open]; 1108 [outputStream open];
1090 } 1109 }
1091 [outputStream write:buffer maxLength:bytesRead]; 1110 [outputStream write:buffer maxLength:bytesRead];
@@ -1442,4 +1461,5 @@ static NSError *ASIUnableToCreateRequestError; @@ -1442,4 +1461,5 @@ static NSError *ASIUnableToCreateRequestError;
1442 @synthesize defaultResponseEncoding; 1461 @synthesize defaultResponseEncoding;
1443 @synthesize responseEncoding; 1462 @synthesize responseEncoding;
1444 @synthesize allowCompressedResponse; 1463 @synthesize allowCompressedResponse;
  1464 +@synthesize allowResumeForFileDownloads;
1445 @end 1465 @end
@@ -504,5 +504,29 @@ @@ -504,5 +504,29 @@
504 } 504 }
505 505
506 506
  507 +- (void)testPartialFetch
  508 +{
  509 + NSString *downloadPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"testfile.txt"];
  510 + NSString *tempPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"tempfile.txt"];
  511 + NSString *partialContent = @"This file should be exactly 163 bytes long when encoded as UTF8, Unix line breaks with no BOM.\n";
  512 + [partialContent writeToFile:tempPath atomically:NO encoding:NSASCIIStringEncoding error:nil];
  513 +
  514 + NSURL *url = [[[NSURL alloc] initWithString:@"http://allseeing-i.com/ASIHTTPRequest/Tests/test_partial_download.txt"] autorelease];
  515 + ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease];
  516 + [request setDownloadDestinationPath:downloadPath];
  517 + [request setTemporaryFileDownloadPath:tempPath];
  518 + [request setAllowResumeForFileDownloads:YES];
  519 + [request start];
  520 +
  521 + BOOL success = ([request contentLength] == 68);
  522 + GHAssertTrue(success,@"Failed to download a segment of the data");
  523 +
  524 + NSString *content = [NSString stringWithContentsOfFile:downloadPath];
  525 +
  526 + NSString *newPartialContent = [content substringFromIndex:95];
  527 + success = ([newPartialContent isEqualToString:@"This is the content we ought to be getting if we start from byte 95."]);
  528 + GHAssertTrue(success,@"Failed to append the correct data to the end of the file?");
  529 +
  530 +}
507 531
508 @end 532 @end
@@ -139,6 +139,13 @@ @@ -139,6 +139,13 @@
139 if ([[request requestMethod] isEqualToString:@"GET"]) { 139 if ([[request requestMethod] isEqualToString:@"GET"]) {
140 ASIHTTPRequest *HEADRequest = [[[ASIHTTPRequest alloc] initWithURL:[request url]] autorelease]; 140 ASIHTTPRequest *HEADRequest = [[[ASIHTTPRequest alloc] initWithURL:[request url]] autorelease];
141 [HEADRequest setMainRequest:request]; 141 [HEADRequest setMainRequest:request];
  142 +
  143 + //If we're downloading to a file, and we already have a partial download to start from
  144 + if ([request allowResumeForFileDownloads] && [request downloadDestinationPath] && [request temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[request temporaryFileDownloadPath]]) {
  145 + unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:[request temporaryFileDownloadPath] traverseLink:NO] fileSize];
  146 + [HEADRequest addRequestHeader:@"Range" value:[NSString stringWithFormat:@"bytes=%llu-",downloadedSoFar]];
  147 + }
  148 +
142 [self addHEADOperation:HEADRequest]; 149 [self addHEADOperation:HEADRequest];
143 150
144 //Tell the request not to reset the progress indicator when it gets a content-length, as we will get the length from the HEAD request 151 //Tell the request not to reset the progress indicator when it gets a content-length, as we will get the length from the HEAD request
@@ -25,6 +25,7 @@ @@ -25,6 +25,7 @@
25 - (void)testProgress; 25 - (void)testProgress;
26 - (void)testProgressWithAuthentication; 26 - (void)testProgressWithAuthentication;
27 - (void)testWithNoListener; 27 - (void)testWithNoListener;
  28 +- (void)testPartialResume;
28 29
29 - (void)setProgress:(float)newProgress; 30 - (void)setProgress:(float)newProgress;
30 31
@@ -303,5 +303,73 @@ static CFStringRef ASIHTTPRequestTestsRunMode = CFSTR("ASIHTTPRequestTestsRunMod @@ -303,5 +303,73 @@ static CFStringRef ASIHTTPRequestTestsRunMode = CFSTR("ASIHTTPRequestTestsRunMod
303 [networkQueue release]; 303 [networkQueue release];
304 } 304 }
305 305
  306 +- (void)testPartialResume
  307 +{
  308 + complete = NO;
  309 +
  310 + NSString *temporaryPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"MemexTrails_1.0b1.zip.download"];
  311 + if ([[NSFileManager defaultManager] fileExistsAtPath:temporaryPath]) {
  312 + [[NSFileManager defaultManager] removeItemAtPath:temporaryPath error:nil];
  313 + }
  314 +
  315 + NSString *downloadPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"MemexTrails_1.0b1.zip"];
  316 + if ([[NSFileManager defaultManager] fileExistsAtPath:downloadPath]) {
  317 + [[NSFileManager defaultManager] removeItemAtPath:downloadPath error:nil];
  318 + }
  319 +
  320 + NSURL *downloadURL = [NSURL URLWithString:@"http://trails-network.net/Downloads/MemexTrails_1.0b1.zip"];
  321 + networkQueue = [[ASINetworkQueue alloc] init];
  322 +
  323 + ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:downloadURL] autorelease];
  324 + [request setDownloadDestinationPath:downloadPath];
  325 + [request setTemporaryFileDownloadPath:temporaryPath];
  326 + [request setAllowResumeForFileDownloads:YES];
  327 + [networkQueue addOperation:request];
  328 + [networkQueue go];
  329 +
  330 + // Let the download run for 5 seconds, which hopefully won't be enough time to grab this file. If you have a super fast connection, this test may fail, serves you right for being so smug. :)
  331 + NSTimer *timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(stopQueue:) userInfo:nil repeats:NO];
  332 +
  333 + while (!complete) {
  334 + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
  335 + }
  336 +
  337 + // 5 seconds is up, let's tell the queue to stop
  338 + [networkQueue cancelAllOperations];
  339 +
  340 + [networkQueue release];
  341 + networkQueue = [[ASINetworkQueue alloc] init];
  342 +
  343 +
  344 + unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:temporaryPath traverseLink:NO] fileSize];
  345 + BOOL success = (downloadedSoFar > 0);
  346 + GHAssertTrue(success,@"Failed to download part of the file, so we can't proceed with this test");
  347 +
  348 + request = [[[ASIHTTPRequest alloc] initWithURL:downloadURL] autorelease];
  349 + [request setDownloadDestinationPath:downloadPath];
  350 + [request setTemporaryFileDownloadPath:temporaryPath];
  351 + [request setAllowResumeForFileDownloads:YES];
  352 +
  353 + [networkQueue addOperation:request];
  354 + [networkQueue go];
  355 +
  356 + [networkQueue waitUntilAllOperationsAreFinished];
  357 +
  358 + unsigned long long amountDownloaded = [[[NSFileManager defaultManager] fileAttributesAtPath:downloadPath traverseLink:NO] fileSize];
  359 + success = (amountDownloaded == 9145357);
  360 + GHAssertTrue(success,@"Failed to complete the download");
  361 +
  362 +
  363 + [networkQueue release];
  364 +
  365 + timeoutTimer = nil;
  366 +
  367 +}
  368 +
  369 +- (void)stopQueue:(id)sender
  370 +{
  371 + complete = YES;
  372 +}
  373 +
306 374
307 @end 375 @end