diff --git a/AFNetworking/AFDownloadRequestOperation.h b/AFNetworking/AFDownloadRequestOperation.h index a059518..651c56d 100644 --- a/AFNetworking/AFDownloadRequestOperation.h +++ b/AFNetworking/AFDownloadRequestOperation.h @@ -25,31 +25,29 @@ /** */ -@interface AFDownloadRequestOperation : AFHTTPRequestOperation { -@private - NSString *_responsePath; - NSError *_downloadError; - NSString *_destination; - BOOL _allowOverwrite; - BOOL _deletesFileUponFailure; -} +@interface AFDownloadRequestOperation : AFHTTPRequestOperation; -@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; -/** - - */ -- (BOOL)deletesFileUponFailure; -/** - +/** + Deletes the temporary file if operation fails/is cancelled. Defaults to `NO`. */ -- (void)setDeletesFileUponFailure:(BOOL)deletesFileUponFailure; +@property (assign) BOOL deletesFileUponFailure; ///** diff --git a/AFNetworking/AFDownloadRequestOperation.m b/AFNetworking/AFDownloadRequestOperation.m index 9e3888c..e41ff63 100644 --- a/AFNetworking/AFDownloadRequestOperation.m +++ b/AFNetworking/AFDownloadRequestOperation.m @@ -24,22 +24,42 @@ #import "AFURLConnectionOperation.h" @interface AFDownloadRequestOperation() -@property (readwrite, nonatomic, copy) NSString *responsePath; +@property (readwrite, nonatomic, retain) NSURLRequest *request; @property (readwrite, nonatomic, retain) NSError *downloadError; @property (readwrite, nonatomic, copy) NSString *destination; @property (readwrite, nonatomic, assign) BOOL allowOverwrite; -@property (readwrite, nonatomic, assign) BOOL deletesFileUponFailure; @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 -@synthesize responsePath = _responsePath; +@synthesize shouldResume = _shouldResume; @synthesize downloadError = _downloadError; @synthesize destination = _destination; @synthesize allowOverwrite = _allowOverwrite; @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 { - [_responsePath release]; [_downloadError release]; [_destination release]; [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 - - (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite { - [self willChangeValueForKey:@"isReady"]; - self.destination = path; self.allowOverwrite = allowOverwrite; - [self didChangeValueForKey:@"isReady"]; + self.destination = path; } #pragma mark - NSOperation @@ -70,13 +128,59 @@ - (void)start { if ([self isReady]) { - // TODO Create temporary path - self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.destination append:NO]; + self.responseFilePath = [self temporaryPath]; + + 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]; } } +#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 - //- (void)setDecideDestinationWithSuggestedFilenameBlock:(void (^)(NSString *filename))block; diff --git a/AFNetworking/AFHTTPRequestOperation.h b/AFNetworking/AFHTTPRequestOperation.h index 3c7ed31..a477b9b 100644 --- a/AFNetworking/AFHTTPRequestOperation.h +++ b/AFNetworking/AFHTTPRequestOperation.h @@ -28,6 +28,8 @@ */ 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. */ @@ -43,9 +45,28 @@ extern NSSet * AFContentTypesFromHTTPHeader(NSString *string); @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 diff --git a/AFNetworking/AFHTTPRequestOperation.m b/AFNetworking/AFHTTPRequestOperation.m index be53c61..8d389a0 100644 --- a/AFNetworking/AFHTTPRequestOperation.m +++ b/AFNetworking/AFHTTPRequestOperation.m @@ -90,13 +90,32 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) { 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 - @interface AFHTTPRequestOperation () @property (readwrite, nonatomic, retain) NSURLRequest *request; @property (readwrite, nonatomic, retain) NSHTTPURLResponse *response; @property (readwrite, nonatomic, retain) NSError *HTTPError; -@property (readwrite, nonatomic, copy) NSString *responseFilePath; +@property (assign) long long totalContentLength; +@property (assign) long long offsetContentLength; @end @implementation AFHTTPRequestOperation @@ -104,6 +123,8 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) { @synthesize responseFilePath = _responseFilePath; @synthesize successCallbackQueue = _successCallbackQueue; @synthesize failureCallbackQueue = _failureCallbackQueue; +@synthesize totalContentLength = _totalContentLength; +@synthesize offsetContentLength = _offsetContentLength; @dynamic request; @dynamic response; @@ -154,7 +175,7 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) { } else { offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length]; } - + NSMutableURLRequest *mutableURLRequest = [[self.request mutableCopy] autorelease]; if ([[self.response allHeaderFields] valueForKey:@"ETag"]) { [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"]; @@ -178,7 +199,7 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) { if (_successCallbackQueue) { dispatch_release(_successCallbackQueue); } - + if (successCallbackQueue) { dispatch_retain(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 + (NSIndexSet *)acceptableStatusCodes { @@ -264,6 +298,9 @@ didReceiveResponse:(NSURLResponse *)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.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) { [self.outputStream setProperty:[NSNumber numberWithInteger:0] forKey:NSStreamFileCurrentOffsetKey]; @@ -272,8 +309,19 @@ didReceiveResponse:(NSURLResponse *)response 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]; }