Permalink
Browse files

Added the ability to resume large file downloads

  • Loading branch information...
1 parent 49f9d13 commit 4f5ae4694625def1cfa68183fa1fc7eb9070967b @pokeb committed Mar 16, 2009
Showing with 133 additions and 10 deletions.
  1. +1 −1 ASIFormDataRequestTests.m
  2. +4 −1 ASIHTTPRequest.h
  3. +28 −8 ASIHTTPRequest.m
  4. +24 −0 ASIHTTPRequestTests.m
  5. +7 −0 ASINetworkQueue.m
  6. +1 −0 ASINetworkQueueTests.h
  7. +68 −0 ASINetworkQueueTests.m
View
2 ASIFormDataRequestTests.m
@@ -50,7 +50,7 @@ - (void)testPostWithFileUpload
success = ([[request responseString] isEqualToString:[NSString stringWithFormat:@"post_var: %@\r\npost_var2: %@\r\npost_var3: %@\r\nfile_name: %@\r\nfile_size: %hu",@"foo",d,v,@"file",size]]);
GHAssertTrue(success,@"Failed to upload the correct data (using NSData)");
}
-
+
@end
View
5 ASIHTTPRequest.h
@@ -179,6 +179,8 @@ typedef enum _ASINetworkErrorType {
NSStringEncoding defaultResponseEncoding;
NSStringEncoding responseEncoding;
+
+ BOOL allowResumeForFileDownloads;
}
#pragma mark init / dealloc
@@ -311,7 +313,7 @@ typedef enum _ASINetworkErrorType {
@property (assign) BOOL useKeychainPersistance;
@property (assign) BOOL useSessionPersistance;
@property (retain) NSString *downloadDestinationPath;
-@property (retain,readonly) NSString *temporaryFileDownloadPath;
+@property (retain) NSString *temporaryFileDownloadPath;
@property (assign) SEL didFinishSelector;
@property (assign) SEL didFailSelector;
@property (retain,readonly) NSString *authenticationRealm;
@@ -339,4 +341,5 @@ typedef enum _ASINetworkErrorType {
@property (assign) NSStringEncoding defaultResponseEncoding;
@property (assign) NSStringEncoding responseEncoding;
@property (assign) BOOL allowCompressedResponse;
+@property (assign) BOOL allowResumeForFileDownloads;
@end
View
36 ASIHTTPRequest.m
@@ -81,9 +81,11 @@ - (id)initWithURL:(NSURL *)newURL
[self setUploadBufferSize:0];
[self setResponseHeaders:nil];
[self setTimeOutSeconds:10];
+ [self setAllowResumeForFileDownloads:NO];
[self setUseKeychainPersistance:NO];
[self setUseSessionPersistance:YES];
[self setUseCookiePersistance:YES];
+ [self setRawResponseData:nil];
[self setRequestCookies:[[[NSMutableArray alloc] init] autorelease]];
[self setDidFinishSelector:@selector(requestFinished:)];
[self setDidFailSelector:@selector(requestFailed:)];
@@ -272,6 +274,12 @@ - (void)main
[self addRequestHeader:@"Accept-Encoding" value:@"gzip"];
}
+ // Should this request resume an existing download?
+ if ([self allowResumeForFileDownloads] && [self downloadDestinationPath] && [self temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[self temporaryFileDownloadPath]]) {
+ unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:[self temporaryFileDownloadPath] traverseLink:NO] fileSize];
+ [self addRequestHeader:@"Range" value:[NSString stringWithFormat:@"bytes=%llu-",downloadedSoFar]];
+ }
+
// Add custom headers
NSDictionary *headers;
@@ -322,8 +330,9 @@ - (void)startRequest
contentLength = 0;
}
[self setResponseHeaders:nil];
- [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]];
-
+ if (![self downloadDestinationPath]) {
+ [self setRawResponseData:[[[NSMutableData alloc] init] autorelease]];
+ }
// Create the stream for the request.
readStream = CFReadStreamCreateForStreamedHTTPRequest(kCFAllocatorDefault, request,readStream);
if (!readStream) {
@@ -431,10 +440,14 @@ - (void)cancelLoad
if (rawResponseData) {
[self setRawResponseData:nil];
- // If we were downloading to a file, let's remove it
+ // If we were downloading to a file
} else if (temporaryFileDownloadPath) {
[outputStream close];
- [[NSFileManager defaultManager] removeItemAtPath:temporaryFileDownloadPath error:NULL];
+
+ // If we haven't said we might want to resume, let's remove the temporary file too
+ if (![self allowResumeForFileDownloads]) {
+ [[NSFileManager defaultManager] removeItemAtPath:temporaryFileDownloadPath error:NULL];
+ }
}
[self setResponseHeaders:nil];
@@ -755,6 +768,7 @@ - (BOOL)readResponseHeadersReturningAuthenticationFailure
BOOL isAuthenticationChallenge = NO;
CFHTTPMessageRef headers = (CFHTTPMessageRef)CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (CFHTTPMessageIsHeaderComplete(headers)) {
+
CFDictionaryRef headerFields = CFHTTPMessageCopyAllHeaderFields(headers);
[self setResponseHeaders:(NSDictionary *)headerFields];
CFRelease(headerFields);
@@ -1083,9 +1097,14 @@ - (void)handleBytesAvailable
// Are we downloading to a file?
if (downloadDestinationPath) {
if (!outputStream) {
- [temporaryFileDownloadPath release];
- temporaryFileDownloadPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]] retain];
- outputStream = [[NSOutputStream alloc] initToFileAtPath:temporaryFileDownloadPath append:NO];
+ BOOL append = NO;
+ if (![self temporaryFileDownloadPath]) {
+ [self setTemporaryFileDownloadPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
+ } else if ([self allowResumeForFileDownloads]) {
+ append = YES;
+ }
+
+ outputStream = [[NSOutputStream alloc] initToFileAtPath:temporaryFileDownloadPath append:append];
[outputStream open];
}
[outputStream write:buffer maxLength:bytesRead];
@@ -1108,7 +1127,7 @@ - (void)handleStreamComplete
}
[progressLock lock];
complete = YES;
- [self updateProgressIndicators];
+ [self updateProgressIndicators];
if (readStream) {
CFReadStreamClose(readStream);
@@ -1442,4 +1461,5 @@ + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest
@synthesize defaultResponseEncoding;
@synthesize responseEncoding;
@synthesize allowCompressedResponse;
+@synthesize allowResumeForFileDownloads;
@end
View
24 ASIHTTPRequestTests.m
@@ -504,5 +504,29 @@ - (void)testCompressedResponse
}
+- (void)testPartialFetch
+{
+ NSString *downloadPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"testfile.txt"];
+ NSString *tempPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"tempfile.txt"];
+ NSString *partialContent = @"This file should be exactly 163 bytes long when encoded as UTF8, Unix line breaks with no BOM.\n";
+ [partialContent writeToFile:tempPath atomically:NO encoding:NSASCIIStringEncoding error:nil];
+
+ NSURL *url = [[[NSURL alloc] initWithString:@"http://allseeing-i.com/ASIHTTPRequest/Tests/test_partial_download.txt"] autorelease];
+ ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:url] autorelease];
+ [request setDownloadDestinationPath:downloadPath];
+ [request setTemporaryFileDownloadPath:tempPath];
+ [request setAllowResumeForFileDownloads:YES];
+ [request start];
+
+ BOOL success = ([request contentLength] == 68);
+ GHAssertTrue(success,@"Failed to download a segment of the data");
+
+ NSString *content = [NSString stringWithContentsOfFile:downloadPath];
+
+ NSString *newPartialContent = [content substringFromIndex:95];
+ success = ([newPartialContent isEqualToString:@"This is the content we ought to be getting if we start from byte 95."]);
+ GHAssertTrue(success,@"Failed to append the correct data to the end of the file?");
+
+}
@end
View
7 ASINetworkQueue.m
@@ -139,6 +139,13 @@ - (void)addOperation:(NSOperation *)operation
if ([[request requestMethod] isEqualToString:@"GET"]) {
ASIHTTPRequest *HEADRequest = [[[ASIHTTPRequest alloc] initWithURL:[request url]] autorelease];
[HEADRequest setMainRequest:request];
+
+ //If we're downloading to a file, and we already have a partial download to start from
+ if ([request allowResumeForFileDownloads] && [request downloadDestinationPath] && [request temporaryFileDownloadPath] && [[NSFileManager defaultManager] fileExistsAtPath:[request temporaryFileDownloadPath]]) {
+ unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:[request temporaryFileDownloadPath] traverseLink:NO] fileSize];
+ [HEADRequest addRequestHeader:@"Range" value:[NSString stringWithFormat:@"bytes=%llu-",downloadedSoFar]];
+ }
+
[self addHEADOperation:HEADRequest];
//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
View
1 ASINetworkQueueTests.h
@@ -25,6 +25,7 @@
- (void)testProgress;
- (void)testProgressWithAuthentication;
- (void)testWithNoListener;
+- (void)testPartialResume;
- (void)setProgress:(float)newProgress;
View
68 ASINetworkQueueTests.m
@@ -303,5 +303,73 @@ - (void)testWithNoListener
[networkQueue release];
}
+- (void)testPartialResume
+{
+ complete = NO;
+
+ NSString *temporaryPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"MemexTrails_1.0b1.zip.download"];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:temporaryPath]) {
+ [[NSFileManager defaultManager] removeItemAtPath:temporaryPath error:nil];
+ }
+
+ NSString *downloadPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"MemexTrails_1.0b1.zip"];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:downloadPath]) {
+ [[NSFileManager defaultManager] removeItemAtPath:downloadPath error:nil];
+ }
+
+ NSURL *downloadURL = [NSURL URLWithString:@"http://trails-network.net/Downloads/MemexTrails_1.0b1.zip"];
+ networkQueue = [[ASINetworkQueue alloc] init];
+
+ ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:downloadURL] autorelease];
+ [request setDownloadDestinationPath:downloadPath];
+ [request setTemporaryFileDownloadPath:temporaryPath];
+ [request setAllowResumeForFileDownloads:YES];
+ [networkQueue addOperation:request];
+ [networkQueue go];
+
+ // 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. :)
+ NSTimer *timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(stopQueue:) userInfo:nil repeats:NO];
+
+ while (!complete) {
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
+ }
+
+ // 5 seconds is up, let's tell the queue to stop
+ [networkQueue cancelAllOperations];
+
+ [networkQueue release];
+ networkQueue = [[ASINetworkQueue alloc] init];
+
+
+ unsigned long long downloadedSoFar = [[[NSFileManager defaultManager] fileAttributesAtPath:temporaryPath traverseLink:NO] fileSize];
+ BOOL success = (downloadedSoFar > 0);
+ GHAssertTrue(success,@"Failed to download part of the file, so we can't proceed with this test");
+
+ request = [[[ASIHTTPRequest alloc] initWithURL:downloadURL] autorelease];
+ [request setDownloadDestinationPath:downloadPath];
+ [request setTemporaryFileDownloadPath:temporaryPath];
+ [request setAllowResumeForFileDownloads:YES];
+
+ [networkQueue addOperation:request];
+ [networkQueue go];
+
+ [networkQueue waitUntilAllOperationsAreFinished];
+
+ unsigned long long amountDownloaded = [[[NSFileManager defaultManager] fileAttributesAtPath:downloadPath traverseLink:NO] fileSize];
+ success = (amountDownloaded == 9145357);
+ GHAssertTrue(success,@"Failed to complete the download");
+
+
+ [networkQueue release];
+
+ timeoutTimer = nil;
+
+}
+
+- (void)stopQueue:(id)sender
+{
+ complete = YES;
+}
+
@end

0 comments on commit 4f5ae46

Please sign in to comment.