Merge branch 'experimental-pause-resume' of https://github.com/steipete/AFNetworking into steipete-pause-resume
This commit is contained in:
commit
ddfb00cc44
4 changed files with 203 additions and 32 deletions
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
///**
|
///**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue