Merge branch 'experimental-pause-resume' of https://github.com/steipete/AFNetworking into steipete-pause-resume

This commit is contained in:
Mattt Thompson 2012-04-09 21:40:49 -07:00
commit ddfb00cc44
4 changed files with 203 additions and 32 deletions

View file

@ -25,31 +25,29 @@
/** /**
*/ */
@interface AFDownloadRequestOperation : AFHTTPRequestOperation { @interface AFDownloadRequestOperation : AFHTTPRequestOperation;
@private
NSString *_responsePath;
NSError *_downloadError;
NSString *_destination;
BOOL _allowOverwrite;
BOOL _deletesFileUponFailure;
}
@property (readonly, nonatomic, copy) NSString *responsePath; /**
A Boolean value that indicates if we should try to resume the download. Defaults is `YES`.
Can only be set while creating the request.
Note: This allows long-lasting resumes between app-starts. Use this for content that doesn't change.
If the file changed in the meantime, you'll end up with a broken file.
*/
@property (assign, readonly) BOOL shouldResume;
/** /**
Set a destination. If you don't manually set one, this defaults to the documents directory.
Note: This can point to a path or a file. If this is a path, response.suggestedFilename will be used for the filename.
*/ */
- (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite; - (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite;
/**
*/
- (BOOL)deletesFileUponFailure;
/** /**
Deletes the temporary file if operation fails/is cancelled. Defaults to `NO`.
*/ */
- (void)setDeletesFileUponFailure:(BOOL)deletesFileUponFailure; @property (assign) BOOL deletesFileUponFailure;
///** ///**

View file

@ -24,22 +24,42 @@
#import "AFURLConnectionOperation.h" #import "AFURLConnectionOperation.h"
@interface AFDownloadRequestOperation() @interface AFDownloadRequestOperation()
@property (readwrite, nonatomic, copy) NSString *responsePath; @property (readwrite, nonatomic, retain) NSURLRequest *request;
@property (readwrite, nonatomic, retain) NSError *downloadError; @property (readwrite, nonatomic, retain) NSError *downloadError;
@property (readwrite, nonatomic, copy) NSString *destination; @property (readwrite, nonatomic, copy) NSString *destination;
@property (readwrite, nonatomic, assign) BOOL allowOverwrite; @property (readwrite, nonatomic, assign) BOOL allowOverwrite;
@property (readwrite, nonatomic, assign) BOOL deletesFileUponFailure;
@end @end
static unsigned long long AFFileSizeForPath(NSString *path) {
unsigned long long fileSize = 0;
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
if ([fileManager fileExistsAtPath:path]) {
NSError *error = nil;
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&error];
if (!error && attributes) {
fileSize = [attributes fileSize];
}
}
return fileSize;
}
@implementation AFDownloadRequestOperation @implementation AFDownloadRequestOperation
@synthesize responsePath = _responsePath; @synthesize shouldResume = _shouldResume;
@synthesize downloadError = _downloadError; @synthesize downloadError = _downloadError;
@synthesize destination = _destination; @synthesize destination = _destination;
@synthesize allowOverwrite = _allowOverwrite; @synthesize allowOverwrite = _allowOverwrite;
@synthesize deletesFileUponFailure = _deletesFileUponFailure; @synthesize deletesFileUponFailure = _deletesFileUponFailure;
@dynamic request;
- (id)initWithRequest:(NSURLRequest *)urlRequest {
if ((self = [super initWithRequest:urlRequest])) {
_shouldResume = YES;
_destination = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] copy];
}
return self;
}
- (void)dealloc { - (void)dealloc {
[_responsePath release];
[_downloadError release]; [_downloadError release];
[_destination release]; [_destination release];
[super dealloc]; [super dealloc];
@ -53,13 +73,51 @@
} }
} }
// the temporary path depends on the request URL.
- (NSString *)temporaryPath {
NSString *temporaryPath = nil;
if (self.destination) {
NSString *hashString = [NSString stringWithFormat:@"%u", [self.request.URL hash]];
temporaryPath = [AFCreateIncompleteDownloadDirectoryPath() stringByAppendingPathComponent:hashString];
}
return temporaryPath;
}
// build the final destination with _destination and response.suggestedFilename.
- (NSString *)destinationPath {
NSString *destinationPath = _destination;
// we assume that at least the directory has to exist on the targetPath
BOOL isDirectory;
if(![[NSFileManager defaultManager] fileExistsAtPath:_destination isDirectory:&isDirectory]) {
isDirectory = NO;
}
// if targetPath is a directory, use the file name we got from the urlRequest.
if (isDirectory) {
destinationPath = [NSString pathWithComponents:[NSArray arrayWithObjects:_destination, self.response.suggestedFilename, nil]];
}
return destinationPath;
}
- (BOOL)deleteTempFileWithError:(NSError **)error {
NSFileManager *fileManager = [[NSFileManager alloc] init];
BOOL success = YES;
@synchronized(self) {
NSString *tempPath = [self temporaryPath];
if ([fileManager fileExistsAtPath:tempPath]) {
success = [fileManager removeItemAtPath:[self temporaryPath] error:error];
}
}
[fileManager release];
return success;
}
#pragma mark - #pragma mark -
- (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite { - (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite {
[self willChangeValueForKey:@"isReady"];
self.destination = path;
self.allowOverwrite = allowOverwrite; self.allowOverwrite = allowOverwrite;
[self didChangeValueForKey:@"isReady"]; self.destination = path;
} }
#pragma mark - NSOperation #pragma mark - NSOperation
@ -70,13 +128,59 @@
- (void)start { - (void)start {
if ([self isReady]) { if ([self isReady]) {
// TODO Create temporary path self.responseFilePath = [self temporaryPath];
self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.destination append:NO];
if (_shouldResume) {
unsigned long long tempFileSize = AFFileSizeForPath([self temporaryPath]);
if (tempFileSize > 0) {
NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease];
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", tempFileSize] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
[self.outputStream setProperty:[NSNumber numberWithUnsignedLongLong:tempFileSize] forKey:NSStreamFileCurrentOffsetKey];
}
}
[super start]; [super start];
} }
} }
#pragma mark - AFURLRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
self.completionBlock = ^ {
if([self isCancelled]) {
if (self.deletesFileUponFailure) {
[self deleteTempFileWithError:&_downloadError];
}
return;
}else {
@synchronized(self) {
NSString *destinationPath = [self destinationPath];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if (_allowOverwrite && [fileManager fileExistsAtPath:destinationPath]) {
[fileManager removeItemAtPath:destinationPath error:&_downloadError];
}
if (!_downloadError) {
[fileManager moveItemAtPath:[self temporaryPath] toPath:destinationPath error:&_downloadError];
}
[fileManager release];
}
}
if (self.error) {
dispatch_async(self.failureCallbackQueue ? self.failureCallbackQueue : dispatch_get_main_queue(), ^{
failure(self, self.error);
});
} else {
dispatch_async(self.successCallbackQueue ? self.successCallbackQueue : dispatch_get_main_queue(), ^{
success(self, _destination);
});
}
};
}
#pragma mark - #pragma mark -
//- (void)setDecideDestinationWithSuggestedFilenameBlock:(void (^)(NSString *filename))block; //- (void)setDecideDestinationWithSuggestedFilenameBlock:(void (^)(NSString *filename))block;

View file

@ -28,6 +28,8 @@
*/ */
extern NSSet * AFContentTypesFromHTTPHeader(NSString *string); extern NSSet * AFContentTypesFromHTTPHeader(NSString *string);
extern NSString * AFCreateIncompleteDownloadDirectoryPath(void);
/** /**
`AFHTTPRequestOperation` is a subclass of `AFURLConnectionOperation` for requests using the HTTP or HTTPS protocols. It encapsulates the concept of acceptable status codes and content types, which determine the success or failure of a request. `AFHTTPRequestOperation` is a subclass of `AFURLConnectionOperation` for requests using the HTTP or HTTPS protocols. It encapsulates the concept of acceptable status codes and content types, which determine the success or failure of a request.
*/ */
@ -43,9 +45,28 @@ extern NSSet * AFContentTypesFromHTTPHeader(NSString *string);
@property (readonly, nonatomic, retain) NSHTTPURLResponse *response; @property (readonly, nonatomic, retain) NSHTTPURLResponse *response;
/** /**
Set a target file for the response, will stream directly into this destination.
Defaults to nil, which will use a memory stream. Will create a new outputStream on change.
Note: Changing this while the request is not in ready state will be ignored.
*/ */
@property (readonly, nonatomic, copy) NSString *responseFilePath; @property (nonatomic, copy) NSString *responseFilePath;
/**
Expected total length. This is different than expectedContentLength if the file is resumed.
On regular requests, this is equal to self.response.expectedContentLength unless we resume a request.
Note: this can also be -1 if the file size is not sent (*)
*/
@property (assign, readonly) long long totalContentLength;
/**
Indicator for the file offset on partial/resumed downloads.
This is greater than zero if the file download is resumed.
*/
@property (assign, readonly) long long offsetContentLength;
///---------------------------------------------------------- ///----------------------------------------------------------
/// @name Managing And Checking For Acceptable HTTP Responses /// @name Managing And Checking For Acceptable HTTP Responses

View file

@ -90,13 +90,32 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
return string; return string;
} }
NSString * AFCreateIncompleteDownloadDirectoryPath(void) {
static NSString *incompleteDownloadPath;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *tempDirectory = NSTemporaryDirectory();
incompleteDownloadPath = [[tempDirectory stringByAppendingPathComponent:kAFNetworkingIncompleteDownloadDirectoryName] retain];
NSError *error = nil;
NSFileManager *fileMan = [[NSFileManager alloc] init];
if(![fileMan createDirectoryAtPath:incompleteDownloadPath withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"Failed to create incomplete downloads directory at %@", incompleteDownloadPath);
}
[fileMan release];
});
return incompleteDownloadPath;
}
#pragma mark - #pragma mark -
@interface AFHTTPRequestOperation () @interface AFHTTPRequestOperation ()
@property (readwrite, nonatomic, retain) NSURLRequest *request; @property (readwrite, nonatomic, retain) NSURLRequest *request;
@property (readwrite, nonatomic, retain) NSHTTPURLResponse *response; @property (readwrite, nonatomic, retain) NSHTTPURLResponse *response;
@property (readwrite, nonatomic, retain) NSError *HTTPError; @property (readwrite, nonatomic, retain) NSError *HTTPError;
@property (readwrite, nonatomic, copy) NSString *responseFilePath; @property (assign) long long totalContentLength;
@property (assign) long long offsetContentLength;
@end @end
@implementation AFHTTPRequestOperation @implementation AFHTTPRequestOperation
@ -104,6 +123,8 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
@synthesize responseFilePath = _responseFilePath; @synthesize responseFilePath = _responseFilePath;
@synthesize successCallbackQueue = _successCallbackQueue; @synthesize successCallbackQueue = _successCallbackQueue;
@synthesize failureCallbackQueue = _failureCallbackQueue; @synthesize failureCallbackQueue = _failureCallbackQueue;
@synthesize totalContentLength = _totalContentLength;
@synthesize offsetContentLength = _offsetContentLength;
@dynamic request; @dynamic request;
@dynamic response; @dynamic response;
@ -154,7 +175,7 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
} else { } else {
offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length]; offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
} }
NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease]; NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease];
if ([[self.response allHeaderFields] valueForKey:@"ETag"]) { if ([[self.response allHeaderFields] valueForKey:@"ETag"]) {
[mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"]; [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
@ -178,7 +199,7 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
if (_successCallbackQueue) { if (_successCallbackQueue) {
dispatch_release(_successCallbackQueue); dispatch_release(_successCallbackQueue);
} }
if (successCallbackQueue) { if (successCallbackQueue) {
dispatch_retain(successCallbackQueue); dispatch_retain(successCallbackQueue);
_successCallbackQueue = successCallbackQueue; _successCallbackQueue = successCallbackQueue;
@ -223,6 +244,19 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
}; };
} }
- (void)setResponseFilePath:(NSString *)responseFilePath {
if ([self isReady] && responseFilePath != _responseFilePath) {
[_responseFilePath release];
_responseFilePath = [responseFilePath retain];
if (responseFilePath) {
self.outputStream = [NSOutputStream outputStreamToFileAtPath:responseFilePath append:NO];
}else {
self.outputStream = [NSOutputStream outputStreamToMemory];
}
}
}
#pragma mark - AFHTTPClientOperation #pragma mark - AFHTTPClientOperation
+ (NSIndexSet *)acceptableStatusCodes { + (NSIndexSet *)acceptableStatusCodes {
@ -264,6 +298,9 @@ didReceiveResponse:(NSURLResponse *)response
{ {
self.response = (NSHTTPURLResponse *)response; self.response = (NSHTTPURLResponse *)response;
// 206 = Partial Content.
long long totalContentLength = self.response.expectedContentLength;
long long fileOffset = 0;
if ([self.response statusCode] != 206) { if ([self.response statusCode] != 206) {
if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) { if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
[self.outputStream setProperty:[NSNumber numberWithInteger:0] forKey:NSStreamFileCurrentOffsetKey]; [self.outputStream setProperty:[NSNumber numberWithInteger:0] forKey:NSStreamFileCurrentOffsetKey];
@ -272,8 +309,19 @@ didReceiveResponse:(NSURLResponse *)response
self.outputStream = [NSOutputStream outputStreamToMemory]; self.outputStream = [NSOutputStream outputStreamToMemory];
} }
} }
}else {
NSString *contentRange = [self.response.allHeaderFields valueForKey:@"Content-Range"];
if ([contentRange hasPrefix:@"bytes"]) {
NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]];
if ([bytes count] == 4) {
fileOffset = [[bytes objectAtIndex:1] longLongValue];
totalContentLength = [[bytes objectAtIndex:2] longLongValue] ?: -1; // if this is *, it's converted to 0, but -1 is default.
}
}
} }
self.offsetContentLength = MAX(fileOffset, 0);
self.totalContentLength = totalContentLength;
[self.outputStream open]; [self.outputStream open];
} }