From 2413d95c3ffe806d424890c185739d6218a2fb38 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 6 Apr 2012 03:00:04 -0700 Subject: [PATCH 1/4] we don't need to declare ivars anymore. --- AFNetworking/AFDownloadRequestOperation.h | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/AFNetworking/AFDownloadRequestOperation.h b/AFNetworking/AFDownloadRequestOperation.h index a059518..da0a3cb 100644 --- a/AFNetworking/AFDownloadRequestOperation.h +++ b/AFNetworking/AFDownloadRequestOperation.h @@ -25,14 +25,7 @@ /** */ -@interface AFDownloadRequestOperation : AFHTTPRequestOperation { -@private - NSString *_responsePath; - NSError *_downloadError; - NSString *_destination; - BOOL _allowOverwrite; - BOOL _deletesFileUponFailure; -} +@interface AFDownloadRequestOperation : AFHTTPRequestOperation; @property (readonly, nonatomic, copy) NSString *responsePath; From 24564772df1881f44f910f56768a87c1652c3870 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 6 Apr 2012 03:00:33 -0700 Subject: [PATCH 2/4] create and set temporary path --- AFNetworking/AFDownloadRequestOperation.m | 16 ++++++++++++---- AFNetworking/AFHTTPRequestOperation.h | 2 ++ AFNetworking/AFHTTPRequestOperation.m | 23 +++++++++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/AFNetworking/AFDownloadRequestOperation.m b/AFNetworking/AFDownloadRequestOperation.m index 9e3888c..cba1714 100644 --- a/AFNetworking/AFDownloadRequestOperation.m +++ b/AFNetworking/AFDownloadRequestOperation.m @@ -53,13 +53,21 @@ } } + +- (NSString *)temporaryPath { + NSString *temporaryPath = nil; + if (self.destination) { + NSString *hashString = [NSString stringWithFormat:@"%d", [self.destination hash]]; + temporaryPath = [AFCreateIncompleteDownloadDirectoryPath() stringByAppendingPathComponent:hashString]; + } + return temporaryPath; +} + #pragma mark - - (void)setDestination:(NSString *)path allowOverwrite:(BOOL)allowOverwrite { - [self willChangeValueForKey:@"isReady"]; self.destination = path; self.allowOverwrite = allowOverwrite; - [self didChangeValueForKey:@"isReady"]; } #pragma mark - NSOperation @@ -70,8 +78,8 @@ - (void)start { if ([self isReady]) { - // TODO Create temporary path - self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.destination append:NO]; + NSString *temporaryPath = [self temporaryPath]; + self.outputStream = [NSOutputStream outputStreamToFileAtPath:temporaryPath append:NO]; [super start]; } diff --git a/AFNetworking/AFHTTPRequestOperation.h b/AFNetworking/AFHTTPRequestOperation.h index 4b6a63e..28c8639 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. */ diff --git a/AFNetworking/AFHTTPRequestOperation.m b/AFNetworking/AFHTTPRequestOperation.m index be53c61..49247a8 100644 --- a/AFNetworking/AFHTTPRequestOperation.m +++ b/AFNetworking/AFHTTPRequestOperation.m @@ -90,6 +90,24 @@ 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 () @@ -154,7 +172,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 +196,7 @@ static NSString * AFStringFromIndexSet(NSIndexSet *indexSet) { if (_successCallbackQueue) { dispatch_release(_successCallbackQueue); } - + if (successCallbackQueue) { dispatch_retain(successCallbackQueue); _successCallbackQueue = successCallbackQueue; @@ -264,6 +282,7 @@ didReceiveResponse:(NSURLResponse *)response { self.response = (NSHTTPURLResponse *)response; + // 206 = Partial Content. if ([self.response statusCode] != 206) { if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) { [self.outputStream setProperty:[NSNumber numberWithInteger:0] forKey:NSStreamFileCurrentOffsetKey]; From 61eda7c4e02b4cd7b8a68bc35af07a9c196921a5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 9 Apr 2012 17:45:41 -0700 Subject: [PATCH 3/4] adds function to responseFilePath (streaming into a file, if set) adds total/offsetContentLength. We really need those in case we pause/resume. --- AFNetworking/AFHTTPRequestOperation.h | 23 +++++++++++++++++-- AFNetworking/AFHTTPRequestOperation.m | 33 +++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/AFNetworking/AFHTTPRequestOperation.h b/AFNetworking/AFHTTPRequestOperation.h index 28c8639..c4e0bd5 100644 --- a/AFNetworking/AFHTTPRequestOperation.h +++ b/AFNetworking/AFHTTPRequestOperation.h @@ -50,9 +50,28 @@ extern NSString * AFCreateIncompleteDownloadDirectoryPath(void); @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 49247a8..8d389a0 100644 --- a/AFNetworking/AFHTTPRequestOperation.m +++ b/AFNetworking/AFHTTPRequestOperation.m @@ -114,7 +114,8 @@ NSString * AFCreateIncompleteDownloadDirectoryPath(void) { @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 @@ -122,6 +123,8 @@ NSString * AFCreateIncompleteDownloadDirectoryPath(void) { @synthesize responseFilePath = _responseFilePath; @synthesize successCallbackQueue = _successCallbackQueue; @synthesize failureCallbackQueue = _failureCallbackQueue; +@synthesize totalContentLength = _totalContentLength; +@synthesize offsetContentLength = _offsetContentLength; @dynamic request; @dynamic response; @@ -241,6 +244,19 @@ NSString * AFCreateIncompleteDownloadDirectoryPath(void) { }; } +- (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 { @@ -283,6 +299,8 @@ 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]; @@ -291,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]; } From 47f6793c3f30f65950432ce390ce11c4abbb7cc7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 9 Apr 2012 17:47:02 -0700 Subject: [PATCH 4/4] 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;