AFNetworking/AFNetworking/AFDownloadRequestOperation.m

290 lines
12 KiB
Objective-C

// AFDownloadRequestOperation.m
//
// Copyright (c) 2012 Peter Steinberger (http://petersteinberger.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import "AFDownloadRequestOperation.h"
#import "AFURLConnectionOperation.h"
#import <CommonCrypto/CommonDigest.h>
#include <fcntl.h>
#include <unistd.h>
@interface AFURLConnectionOperation (AFInternal)
@property (nonatomic, strong) NSURLRequest *request;
@property (readonly, nonatomic, assign) long long totalBytesRead;
@end
typedef void (^AFURLConnectionProgressiveOperationProgressBlock)(NSInteger bytes, long long totalBytes, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile);
@interface AFDownloadRequestOperation() {
NSError *_fileError;
}
@property (nonatomic, retain) NSString *tempPath;
@property (assign) long long totalContentLength;
@property (assign) long long offsetContentLength;
@property (nonatomic, copy) AFURLConnectionProgressiveOperationProgressBlock progressiveDownloadProgress;
@end
@implementation AFDownloadRequestOperation
@synthesize targetPath = _targetPath;
@synthesize tempPath = _tempPath;
@synthesize totalContentLength = _totalContentLength;
@synthesize offsetContentLength = _offsetContentLength;
@synthesize shouldResume = _shouldResume;
@synthesize deleteTempFileOnCancel = _deleteTempFileOnCancel;
@synthesize progressiveDownloadProgress = _progressiveDownloadProgress;
#pragma mark - Static
+ (AFDownloadRequestOperation *)downloadOperationWithRequest:(NSURLRequest *)urlRequest
targetPath:(NSString *)targetPath
shouldResume:(BOOL)shouldResume
success:(void (^)(NSURLRequest *request, NSString *filePath))success
failure:(void (^)(NSURLRequest *request, NSError *error))failure
{
AFDownloadRequestOperation *requestOperation = [[self alloc] initWithRequest:urlRequest targetPath:(NSString *)targetPath shouldResume:shouldResume];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
if (success) {
success(operation.request, ((AFDownloadRequestOperation *)operation).targetPath);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (failure) {
failure(operation.request, error);
}
}];
return requestOperation;
}
+ (NSString *)cacheFolder {
static NSString *cacheFolder;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *cacheDir = NSTemporaryDirectory();
cacheFolder = [[cacheDir stringByAppendingPathComponent:kAFNetworkingIncompleteDownloadFolderName] retain];
// ensure all cache directories are there (needed only once)
NSError *error = nil;
NSFileManager *fileMan = [[NSFileManager alloc] init];
if(![fileMan createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"Failed to create cache directory at %@", cacheFolder);
[fileMan release];
}
});
return cacheFolder;
}
// calculates the MD5 hash of a key
+ (NSString *)md5StringForString:(NSString *)string {
const char *str = [string UTF8String];
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, strlen(str), r);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
}
#pragma mark - Private
- (unsigned long long)fileSizeForPath:(NSString *)path {
signed long long fileSize = 0;
NSFileManager *fileManager = [[NSFileManager alloc] init]; // not thread safe
if ([fileManager fileExistsAtPath:path]) {
NSError *error = nil;
NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error];
if (!error && fileDict) {
fileSize = [fileDict fileSize];
}
}
[fileManager release];
return fileSize;
}
#pragma mark - NSObject
- (id)initWithRequest:(NSURLRequest *)urlRequest targetPath:(NSString *)targetPath shouldResume:(BOOL)shouldResume {
if ((self = [super initWithRequest:urlRequest])) {
NSParameterAssert(targetPath != nil && urlRequest != nil);
_shouldResume = shouldResume;
// we assume that at least the directory has to exist on the targetPath
BOOL isDirectory;
if(![[NSFileManager defaultManager] fileExistsAtPath:targetPath isDirectory:&isDirectory]) {
isDirectory = NO;
}
// if targetPath is a directory, use the file name we got from the urlRequest.
if (isDirectory) {
NSString *fileName = [urlRequest.URL lastPathComponent];
_targetPath = [[NSString pathWithComponents:[NSArray arrayWithObjects:targetPath, fileName, nil]] retain];
}else {
_targetPath = [targetPath retain];
}
// download is saved into a temporal file and remaned upon completion
NSString *tempPath = [self tempPath];
// do we need to resume the file?
BOOL isResuming = NO;
if (shouldResume) {
unsigned long long downloadedBytes = [self fileSizeForPath:tempPath];
if (downloadedBytes > 0) {
NSMutableURLRequest *mutableURLRequest = [urlRequest mutableCopy];
NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];
[mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
isResuming = YES;
}
}
// try to create/open a file at the target location
if (!isResuming) {
int fileDescriptor = open([tempPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666);
if (fileDescriptor > 0) {
close(fileDescriptor);
}
}
self.outputStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:isResuming];
// if the output stream can't be created, instantly destroy the object.
if (!self.outputStream) {
[self release];
return nil;
}
}
return self;
}
- (void)dealloc {
[_progressiveDownloadProgress release];
[_targetPath release];
[super dealloc];
}
#pragma mark - Public
- (BOOL)deleteTempFileWithError:(NSError **)error {
NSFileManager *fileManager = [[NSFileManager alloc] init];
BOOL success = YES;
@synchronized(self) {
NSString *tempPath = [self tempPath];
if ([fileManager fileExistsAtPath:tempPath]) {
success = [fileManager removeItemAtPath:[self tempPath] error:error];
}
}
[fileManager release];
return success;
}
- (NSString *)tempPath {
NSString *tempPath = nil;
if (self.targetPath) {
NSString *md5URLString = [[self class] md5StringForString:self.targetPath];
tempPath = [[[self class] cacheFolder] stringByAppendingPathComponent:md5URLString];
}
return tempPath;
}
- (void)setProgressiveDownloadProgressBlock:(void (^)(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile))block {
self.progressiveDownloadProgress = block;
}
#pragma mark - AFURLRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
self.completionBlock = ^ {
if([self isCancelled]) {
// should we clean up? most likely we don't.
if (self.isDeletingTempFileOnCancel) {
[self deleteTempFileWithError:&_fileError];
}
return;
}else {
// move file to final position and capture error
@synchronized(self) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager moveItemAtPath:[self tempPath] toPath:_targetPath error:&_fileError];
[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, _targetPath);
});
}
};
}
- (NSError *)error {
if (_fileError) {
return _fileError;
} else {
return [super error];
}
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[super connection:connection didReceiveResponse:response];
// check if we have the correct response
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
}
}
}
self.offsetContentLength = MAX(fileOffset, 0);
self.totalContentLength = totalContentLength;
[self.outputStream setProperty:[NSNumber numberWithLongLong:_offsetContentLength] forKey:NSStreamFileCurrentOffsetKey];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[super connection:connection didReceiveData:data];
if (self.progressiveDownloadProgress) {
self.progressiveDownloadProgress((long long)[data length], self.totalBytesRead, self.response.expectedContentLength,self.totalBytesRead + self.offsetContentLength, self.totalContentLength);
}
}
@end