Ben Copsey

Added the ability to resume large file downloads

... ... @@ -50,7 +50,7 @@
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
... ...
... ... @@ -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
... ...
... ... @@ -81,9 +81,11 @@ static NSError *ASIUnableToCreateRequestError;
[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 @@ static NSError *ASIUnableToCreateRequestError;
[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 @@ static NSError *ASIUnableToCreateRequestError;
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 @@ static NSError *ASIUnableToCreateRequestError;
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 @@ static NSError *ASIUnableToCreateRequestError;
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 @@ static NSError *ASIUnableToCreateRequestError;
// 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 @@ static NSError *ASIUnableToCreateRequestError;
}
[progressLock lock];
complete = YES;
[self updateProgressIndicators];
[self updateProgressIndicators];
if (readStream) {
CFReadStreamClose(readStream);
... ... @@ -1442,4 +1461,5 @@ static NSError *ASIUnableToCreateRequestError;
@synthesize defaultResponseEncoding;
@synthesize responseEncoding;
@synthesize allowCompressedResponse;
@synthesize allowResumeForFileDownloads;
@end
... ...
... ... @@ -504,5 +504,29 @@
}
- (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
... ...
... ... @@ -139,6 +139,13 @@
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
... ...
... ... @@ -25,6 +25,7 @@
- (void)testProgress;
- (void)testProgressWithAuthentication;
- (void)testWithNoListener;
- (void)testPartialResume;
- (void)setProgress:(float)newProgress;
... ...
... ... @@ -303,5 +303,73 @@ static CFStringRef ASIHTTPRequestTestsRunMode = CFSTR("ASIHTTPRequestTestsRunMod
[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
... ...