From 47f6793c3f30f65950432ce390ce11c4abbb7cc7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 9 Apr 2012 17:47:02 -0700 Subject: [PATCH] Restore download resuming. The default destination will be the documents folder, unless set otherwise. We also look into the response metadata for the actual filename (unless set otherwise) --- AFNetworking/AFDownloadRequestOperation.h | 23 +++-- AFNetworking/AFDownloadRequestOperation.m | 114 ++++++++++++++++++++-- 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/AFNetworking/AFDownloadRequestOperation.h b/AFNetworking/AFDownloadRequestOperation.h index da0a3cb..651c56d 100644 --- a/AFNetworking/AFDownloadRequestOperation.h +++ b/AFNetworking/AFDownloadRequestOperation.h @@ -27,22 +27,27 @@ */ @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 cba1714..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,21 +73,51 @@ } } - +// the temporary path depends on the request URL. - (NSString *)temporaryPath { NSString *temporaryPath = nil; if (self.destination) { - NSString *hashString = [NSString stringWithFormat:@"%d", [self.destination hash]]; + 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.destination = path; self.allowOverwrite = allowOverwrite; + self.destination = path; } #pragma mark - NSOperation @@ -78,13 +128,59 @@ - (void)start { if ([self isReady]) { - NSString *temporaryPath = [self temporaryPath]; - self.outputStream = [NSOutputStream outputStreamToFileAtPath:temporaryPath 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;