From 331d7bcbf2e19d2c275b5b1c8b0dd51a64e66ea4 Mon Sep 17 00:00:00 2001 From: Mattt Thompson Date: Sun, 25 Mar 2012 12:23:40 -0700 Subject: [PATCH] Moving resumeable streaming download functionality to AFHTTPRequestOperation --- AFNetworking/AFHTTPRequestOperation.h | 21 +++++ AFNetworking/AFHTTPRequestOperation.m | 117 +++++++++++++++++++++++- AFNetworking/AFURLConnectionOperation.h | 22 ----- AFNetworking/AFURLConnectionOperation.m | 100 +------------------- 4 files changed, 134 insertions(+), 126 deletions(-) diff --git a/AFNetworking/AFHTTPRequestOperation.h b/AFNetworking/AFHTTPRequestOperation.h index 122099e..d46d1ba 100644 --- a/AFNetworking/AFHTTPRequestOperation.h +++ b/AFNetworking/AFHTTPRequestOperation.h @@ -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 ///----------------------------------------------------------- diff --git a/AFNetworking/AFHTTPRequestOperation.m b/AFNetworking/AFHTTPRequestOperation.m index 871cb18..84e4549 100644 --- a/AFNetworking/AFHTTPRequestOperation.m +++ b/AFNetworking/AFHTTPRequestOperation.m @@ -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 diff --git a/AFNetworking/AFURLConnectionOperation.h b/AFNetworking/AFURLConnectionOperation.h index 9a7f34e..872476d 100644 --- a/AFNetworking/AFURLConnectionOperation.h +++ b/AFNetworking/AFURLConnectionOperation.h @@ -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 ///--------------------------------- diff --git a/AFNetworking/AFURLConnectionOperation.m b/AFNetworking/AFURLConnectionOperation.m index b196ae6..56a9854 100644 --- a/AFNetworking/AFURLConnectionOperation.m +++ b/AFNetworking/AFURLConnectionOperation.m @@ -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];