Moving resumeable streaming download functionality to AFHTTPRequestOperation

This commit is contained in:
Mattt Thompson 2012-03-25 12:23:40 -07:00
parent 7bd7ee609b
commit 331d7bcbf2
4 changed files with 134 additions and 126 deletions

View File

@ -44,6 +44,10 @@
*/
@property (readonly, nonatomic, retain) NSHTTPURLResponse *response;
/**
*/
@property (readonly, nonatomic, copy) NSString *responseFilePath;
///----------------------------------------------------------
/// @name Managing And Checking For Acceptable HTTP Responses
@ -91,6 +95,23 @@
*/
+ (BOOL)canProcessRequest:(NSURLRequest *)urlRequest;
///-------------------------------------------------
/// @name Configuring Resumeable Streaming Downloads
///-------------------------------------------------
/**
*/
- (void)setOutputStreamDownloadingToFile:(NSString *)path
shouldResume:(BOOL)shouldResume;
/**
Deletes the temporary file.
@return `YES` if the file is successfully removed or did not exist in the first place, otherwise `NO`.
*/
- (BOOL)deleteTemporaryFileWithError:(NSError **)error;
///-----------------------------------------------------------
/// @name Setting Completion Block Success / Failure Callbacks
///-----------------------------------------------------------

View File

@ -22,6 +22,8 @@
#import "AFHTTPRequestOperation.h"
NSString * const kAFNetworkingIncompleteDownloadDirectoryName = @"Incomplete";
static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
NSMutableString *string = [NSMutableString string];
@ -52,19 +54,55 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
return string;
}
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;
}
static NSString * AFIncompleteDownloadDirectory() {
static NSString *_af_incompleteDownloadDirectory = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *temporaryDirectory = NSTemporaryDirectory();
_af_incompleteDownloadDirectory = [[temporaryDirectory stringByAppendingPathComponent:kAFNetworkingIncompleteDownloadDirectoryName] retain];
NSError *error = nil;
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
if(![fileManager createDirectoryAtPath:_af_incompleteDownloadDirectory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(NSLocalizedString(@"Failed to create incomplete download directory at %@", nil), _af_incompleteDownloadDirectory);
}
});
return _af_incompleteDownloadDirectory;
}
#pragma mark -
@interface AFHTTPRequestOperation ()
@property (readwrite, nonatomic, retain) NSURLRequest *request;
@property (readwrite, nonatomic, retain) NSError *HTTPError;
@property (readwrite, nonatomic, copy) NSString *responseFilePath;
@end
@implementation AFHTTPRequestOperation
@synthesize acceptableStatusCodes = _acceptableStatusCodes;
@synthesize acceptableContentTypes = _acceptableContentTypes;
@synthesize HTTPError = _HTTPError;
@synthesize responseFilePath = _responseFilePath;
@synthesize successCallbackQueue = _successCallbackQueue;
@synthesize failureCallbackQueue = _failureCallbackQueue;
@dynamic request;
@dynamic response;
- (id)initWithRequest:(NSURLRequest *)request {
self = [super initWithRequest:request];
@ -95,10 +133,6 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
[super dealloc];
}
- (NSHTTPURLResponse *)response {
return (NSHTTPURLResponse *)[super response];
}
- (NSError *)error {
if (self.response && !self.HTTPError) {
if (![self hasAcceptableStatusCode]) {
@ -157,6 +191,38 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
}
}
- (void)setOutputStreamDownloadingToFile:(NSString *)path
shouldResume:(BOOL)shouldResume
{
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
isDirectory = NO;
}
if (isDirectory) {
self.responseFilePath = [NSString pathWithComponents:[NSArray arrayWithObjects:path, [[self.request URL] lastPathComponent], nil]];
} else {
self.responseFilePath = path;
}
NSString *temporaryFilePath = [AFIncompleteDownloadDirectory() stringByAppendingPathComponent:[[NSNumber numberWithInteger:[self.responseFilePath hash]] stringValue]];
if (shouldResume) {
unsigned long long downloadedBytes = AFFileSizeForPath(temporaryFilePath);
if (downloadedBytes > 0) {
NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease];
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", downloadedBytes] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
}
}
self.outputStream = [NSOutputStream outputStreamToFileAtPath:temporaryFilePath append:!![self.request valueForHTTPHeaderField:@"Range"]];
}
- (BOOL)deleteTemporaryFileWithError:(NSError **)error {
return NO;
}
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
@ -187,4 +253,45 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) {
return YES;
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
[super connection:connection didReceiveResponse:response];
if (![self.response isKindOfClass:[NSHTTPURLResponse class]]) {
return;
}
// check for valid response to resume the download if possible
long long totalContentLength = self.response.expectedContentLength;
long long fileOffset = 0;
if ([self.response statusCode] == 206) {
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]; // if this is *, it's converted to 0
}
}
}
unsigned long long offsetContentLength = MAX(fileOffset, 0);
[self.outputStream setProperty:[NSNumber numberWithLongLong:offsetContentLength] forKey:NSStreamFileCurrentOffsetKey];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[super connectionDidFinishLoading:connection];
if (self.responseFilePath) {
@synchronized(self) {
NSString *temporaryFilePath = [AFIncompleteDownloadDirectory() stringByAppendingPathComponent:[[NSNumber numberWithInteger:[self.responseFilePath hash]] stringValue]];
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
[fileManager moveItemAtPath:temporaryFilePath toPath:self.responseFilePath error:&_HTTPError];
}
}
}
@end

View File

@ -142,11 +142,6 @@ extern NSString * const kAFNetworkingIncompleteDownloadDirectoryName;
*/
@property (readonly, nonatomic, copy) NSString *responseString;
/**
*/
@property (readonly, nonatomic, copy) NSString *responseFilePath;
///------------------------
/// @name Accessing Streams
///------------------------
@ -178,23 +173,6 @@ extern NSString * const kAFNetworkingIncompleteDownloadDirectoryName;
*/
- (id)initWithRequest:(NSURLRequest *)urlRequest;
///-------------------------------------------------
/// @name Configuring Resumeable Streaming Downloads
///-------------------------------------------------
/**
*/
- (void)setOutputStreamDownloadingToFile:(NSString *)path
shouldResume:(BOOL)shouldResume;
/**
Deletes the temporary file.
@return `YES` if the file is successfully removed or did not exist in the first place, otherwise `NO`.
*/
- (BOOL)deleteTemporaryFileWithError:(NSError **)error;
///---------------------------------
/// @name Setting Progress Callbacks
///---------------------------------

View File

@ -37,8 +37,6 @@ NSString * const AFNetworkingErrorDomain = @"com.alamofire.networking.error";
NSString * const AFNetworkingOperationDidStartNotification = @"com.alamofire.networking.operation.start";
NSString * const AFNetworkingOperationDidFinishNotification = @"com.alamofire.networking.operation.finish";
NSString * const kAFNetworkingIncompleteDownloadDirectoryName = @"Incomplete";
typedef void (^AFURLConnectionOperationProgressBlock)(NSInteger bytes, long long totalBytes, long long totalBytesExpected);
typedef BOOL (^AFURLConnectionOperationAuthenticationAgainstProtectionSpaceBlock)(NSURLConnection *connection, NSURLProtectionSpace *protectionSpace);
typedef void (^AFURLConnectionOperationAuthenticationChallengeBlock)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge);
@ -82,38 +80,6 @@ static inline BOOL AFStateTransitionIsValid(AFOperationState fromState, AFOperat
}
}
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;
}
static NSString * AFIncompleteDownloadDirectory() {
static NSString *_af_incompleteDownloadDirectory = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *temporaryDirectory = NSTemporaryDirectory();
_af_incompleteDownloadDirectory = [[temporaryDirectory stringByAppendingPathComponent:kAFNetworkingIncompleteDownloadDirectoryName] retain];
NSError *error = nil;
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
if(![fileManager createDirectoryAtPath:_af_incompleteDownloadDirectory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(NSLocalizedString(@"Failed to create incomplete download directory at %@", nil), _af_incompleteDownloadDirectory);
}
});
return _af_incompleteDownloadDirectory;
}
@interface AFURLConnectionOperation ()
@property (readwrite, nonatomic, assign) AFOperationState state;
@property (readwrite, nonatomic, retain) NSRecursiveLock *lock;
@ -123,7 +89,6 @@ static NSString * AFIncompleteDownloadDirectory() {
@property (readwrite, nonatomic, retain) NSError *error;
@property (readwrite, nonatomic, retain) NSData *responseData;
@property (readwrite, nonatomic, copy) NSString *responseString;
@property (readwrite, nonatomic, copy) NSString *responseFilePath;
@property (readwrite, nonatomic, assign) long long totalBytesRead;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationProgressBlock uploadProgress;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationProgressBlock downloadProgress;
@ -144,7 +109,6 @@ static NSString * AFIncompleteDownloadDirectory() {
@synthesize error = _error;
@synthesize responseData = _responseData;
@synthesize responseString = _responseString;
@synthesize responseFilePath = _responseFilePath;
@synthesize totalBytesRead = _totalBytesRead;
@dynamic inputStream;
@synthesize outputStream = _outputStream;
@ -262,38 +226,6 @@ static NSString * AFIncompleteDownloadDirectory() {
self.request = mutableRequest;
}
- (void)setOutputStreamDownloadingToFile:(NSString *)path
shouldResume:(BOOL)shouldResume
{
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
isDirectory = NO;
}
if (isDirectory) {
self.responseFilePath = [NSString pathWithComponents:[NSArray arrayWithObjects:path, [[self.request URL] lastPathComponent], nil]];
} else {
self.responseFilePath = path;
}
NSString *temporaryFilePath = [AFIncompleteDownloadDirectory() stringByAppendingPathComponent:[[NSNumber numberWithInteger:[self.responseFilePath hash]] stringValue]];
if (shouldResume) {
unsigned long long downloadedBytes = AFFileSizeForPath(temporaryFilePath);
if (downloadedBytes > 0) {
NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease];
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", downloadedBytes] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
}
}
self.outputStream = [NSOutputStream outputStreamToFileAtPath:temporaryFilePath append:!![self.request valueForHTTPHeaderField:@"Range"]];
}
- (BOOL)deleteTemporaryFileWithError:(NSError **)error {
return NO;
}
- (void)setUploadProgressBlock:(void (^)(NSInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite))block {
self.uploadProgress = block;
}
@ -512,28 +444,6 @@ didReceiveResponse:(NSURLResponse *)response
if (self.outputStream) {
[self.outputStream open];
}
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (![HTTPResponse isKindOfClass:[NSHTTPURLResponse class]]) {
return;
}
// check for valid response to resume the download if possible
long long totalContentLength = self.response.expectedContentLength;
long long fileOffset = 0;
if([HTTPResponse statusCode] == 206) {
NSString *contentRange = [[HTTPResponse 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]; // if this is *, it's converted to 0
}
}
}
unsigned long long offsetContentLength = MAX(fileOffset, 0);
[self.outputStream setProperty:[NSNumber numberWithLongLong:offsetContentLength] forKey:NSStreamFileCurrentOffsetKey];
}
- (void)connection:(NSURLConnection *)__unused connection
@ -551,15 +461,7 @@ didReceiveResponse:(NSURLResponse *)response
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)__unused connection {
if (self.responseFilePath) {
@synchronized(self) {
NSString *temporaryFilePath = [AFIncompleteDownloadDirectory() stringByAppendingPathComponent:[[NSNumber numberWithInteger:[self.responseFilePath hash]] stringValue]];
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
[fileManager moveItemAtPath:temporaryFilePath toPath:self.responseFilePath error:&_error];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)__unused connection {
self.responseData = [self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
[self.outputStream close];