├── .gitignore ├── AFDownloadRequestOperation.podspec ├── LICENCE ├── README.md ├── AFDownloadRequestOperation.h └── AFDownloadRequestOperation.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /AFDownloadRequestOperation.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AFDownloadRequestOperation' 3 | s.version = '2.0.1' 4 | s.summary = "A progressive download operation for AFNetworking." 5 | s.homepage = "https://github.com/steipete/AFDownloadRequestOperation" 6 | s.author = { 'Peter Steinberger' => 'steipete@gmail.com' } 7 | s.source = { :git => 'https://github.com/steipete/AFDownloadRequestOperation.git', :tag => s.version.to_s } 8 | s.ios.deployment_target = '6.0' 9 | s.osx.deployment_target = '10.8' 10 | s.requires_arc = true 11 | s.source_files = '*.{h,m}' 12 | s.license = 'MIT' 13 | s.dependency 'AFNetworking', '~> 2.0' 14 | end 15 | 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Peter Steinberger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AFDownloadRequestOperation 2 | ========================== 3 | 4 | * DEPRECATED AND NO LONGER MAINTAINED. USE `NSURLSession` instead. * 5 | 6 | A progressive download operation for AFNetworking. I wrote this to support large PDF downloads in [PSPDFKit, my commercial iOS PDF framework](http://pspdfkit.com), but it works for any file type. 7 | 8 | While AFNetworking already supports downloading files, this class has additional support to resume a partial download, uses a temporary directory and has a special block that helps with calculating the correct download progress. 9 | 10 | AFDownloadRequestOperation is smart with choosing the correct targetPath. If you set a folder, the file name of the downloaded URL will be used, else the file name that is already set. 11 | 12 | AFDownloadRequestOperation also relays any NSError that happened during a file operation to the faulure block. 13 | 14 | With partially resumed files, the progress delegate needs additional info. The server might only have a few totalByesExpected, but we wanna show the correct value that includes the previous progress. 15 | 16 | ``` objective-c 17 | [pdfRequest setProgressiveDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) { 18 | self.downloadProgress = totalBytesReadForFile/(float)totalBytesExpectedToReadForFile; 19 | }]; 20 | ``` 21 | 22 | The temporary folder will be automatically created on first access, but an be changed. It defaults to ```tmp/Incomplete/```. The temp directory will be cleaned by the system on a regular bases; so a resume will only succeed if there's not much time between. 23 | 24 | ### AFNetworking 25 | 26 | This is tested against iOS 6+, AFNetworking 2.0 and uses ARC. 27 | 28 | 29 | ### Creator 30 | 31 | [Peter Steinberger](http://github.com/steipete) 32 | [@steipete](https://twitter.com/steipete) 33 | 34 | ## License 35 | 36 | AFDownloadRequestOperation is available under the MIT license. See the LICENSE file for more info. 37 | -------------------------------------------------------------------------------- /AFDownloadRequestOperation.h: -------------------------------------------------------------------------------- 1 | // AFDownloadRequestOperation.h 2 | // 3 | // Copyright (c) 2012 Peter Steinberger (http://petersteinberger.com) 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | #import "AFHTTPRequestOperation.h" 24 | 25 | #define kAFNetworkingIncompleteDownloadFolderName @"Incomplete" 26 | 27 | /** 28 | `AFDownloadRequestOperation` is a subclass of `AFHTTPRequestOperation` for streamed file downloading. Supports Content-Range. (http://tools.ietf.org/html/rfc2616#section-14.16) 29 | */ 30 | @interface AFDownloadRequestOperation : AFHTTPRequestOperation 31 | 32 | /** 33 | A String value that defines the target path or directory. 34 | 35 | We try to be clever here and understand both a directory or a filename. 36 | The target directory should already be create, or the download fill fail. 37 | 38 | If the target is a directory, we use the last part of the URL as a default file name. 39 | targetPath is the responseObject if operation succeeds 40 | */ 41 | @property (strong) NSString *targetPath; 42 | 43 | /** 44 | A Boolean value that indicates if we should allow a downloaded file to overwrite 45 | a previously downloaded file of the same name. Default is `NO`. 46 | */ 47 | @property (assign) BOOL shouldOverwrite; 48 | 49 | /** 50 | A Boolean value that indicates if we should try to resume the download. Defaults is `YES`. 51 | 52 | Can only be set while creating the request. 53 | */ 54 | @property (assign, readonly) BOOL shouldResume; 55 | 56 | /** 57 | Deletes the temporary file if operations is cancelled. Defaults to `NO`. 58 | */ 59 | @property (assign, getter=isDeletingTempFileOnCancel) BOOL deleteTempFileOnCancel; 60 | 61 | /** 62 | Expected total length. This is different than expectedContentLength if the file is resumed. 63 | 64 | Note: this can also be zero if the file size is not sent (*) 65 | */ 66 | @property (assign, readonly) long long totalContentLength; 67 | 68 | /** 69 | Indicator for the file offset on partial downloads. This is greater than zero if the file download is resumed. 70 | */ 71 | @property (assign, readonly) long long offsetContentLength; 72 | 73 | /** 74 | The callback dispatch queue on progressive download. If `NULL` (default), the main queue is used. 75 | */ 76 | @property (nonatomic, assign) dispatch_queue_t progressiveDownloadCallbackQueue; 77 | 78 | ///---------------------------------- 79 | /// @name Creating Request Operations 80 | ///---------------------------------- 81 | 82 | /** 83 | Creates and returns an `AFDownloadRequestOperation` 84 | @param urlRequest The request object to be loaded asynchronously during execution of the operation 85 | @param targetPath The target path (with or without file name) 86 | @param shouldResume If YES, tries to resume a partial download if found. 87 | @return A new download request operation 88 | */ 89 | - (id)initWithRequest:(NSURLRequest *)urlRequest targetPath:(NSString *)targetPath shouldResume:(BOOL)shouldResume; 90 | 91 | /** 92 | Creates and returns an `AFDownloadRequestOperation` 93 | @param urlRequest The request object to be loaded asynchronously during execution of the operation 94 | @param fileIdentifier An unique file identifier to be used for the temporary filename, instead of calculating 95 | it using the targetPath. Which prevents problems of the iOS supplied random container paths. 96 | @param targetPath The target path (with or without file name) 97 | @param shouldResume If YES, tries to resume a partial download if found. 98 | @return A new download request operation 99 | */ 100 | - (id)initWithRequest:(NSURLRequest *)urlRequest fileIdentifier:(NSString *)fileIdentifier targetPath:(NSString *)targetPath shouldResume:(BOOL)shouldResume; 101 | 102 | /** 103 | Deletes the temporary file. 104 | 105 | Returns `NO` if an error happened, `YES` if the file is removed or did not exist in the first place. 106 | */ 107 | - (BOOL)deleteTempFileWithError:(NSError **)error; 108 | 109 | /** 110 | Returns the path used for the temporary file. Returns `nil` if the targetPath has not been set. 111 | */ 112 | - (NSString *)tempPath; 113 | 114 | /** 115 | Sets a callback to be called when an undetermined number of bytes have been downloaded from the server. This is a variant of setDownloadProgressBlock that adds support for progressive downloads and adds the 116 | 117 | @param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes five arguments: the number of bytes read since the last time the download progress block was called, the bytes expected to be read during the request, the bytes already read during this request, the total bytes read (including from previous partial downloads), and the total bytes expected to be read for the file. This block may be called multiple times. 118 | 119 | @see setDownloadProgressBlock 120 | */ 121 | - (void)setProgressiveDownloadProgressBlock:(void (^)(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile))block; 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /AFDownloadRequestOperation.m: -------------------------------------------------------------------------------- 1 | // AFDownloadRequestOperation.m 2 | // 3 | // Copyright (c) 2012 Peter Steinberger (http://petersteinberger.com) 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | #import "AFDownloadRequestOperation.h" 24 | #import "AFURLConnectionOperation.h" 25 | #import 26 | #include 27 | #include 28 | 29 | #if !__has_feature(objc_arc) 30 | #error "Compile this file with ARC" 31 | #endif 32 | 33 | @interface AFURLConnectionOperation (AFInternal) 34 | @property (nonatomic, strong) NSURLRequest *request; 35 | @property (readonly, nonatomic, assign) long long totalBytesRead; 36 | @end 37 | 38 | typedef void (^AFURLConnectionProgressiveOperationProgressBlock)(AFDownloadRequestOperation *operation, NSInteger bytes, long long totalBytes, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile); 39 | 40 | @interface AFDownloadRequestOperation() { 41 | NSError *_fileError; 42 | id _responseObject; 43 | } 44 | @property (nonatomic, strong) NSString *tempPath; 45 | @property (nonatomic, strong) NSString *fileIdentifier; 46 | @property (assign) long long totalContentLength; 47 | @property (nonatomic, assign) long long totalBytesReadPerDownload; 48 | @property (assign) long long offsetContentLength; 49 | @property (nonatomic, copy) AFURLConnectionProgressiveOperationProgressBlock progressiveDownloadProgress; 50 | @end 51 | 52 | @implementation AFDownloadRequestOperation 53 | 54 | #pragma mark - NSObject 55 | 56 | - (void)dealloc { 57 | if (_progressiveDownloadCallbackQueue) { 58 | #if !OS_OBJECT_USE_OBJC 59 | dispatch_release(_progressiveDownloadCallbackQueue); 60 | #endif 61 | _progressiveDownloadCallbackQueue = NULL; 62 | } 63 | } 64 | 65 | - (id)initWithRequest:(NSURLRequest *)urlRequest targetPath:(NSString *)targetPath shouldResume:(BOOL)shouldResume { 66 | return [self initWithRequest:urlRequest fileIdentifier:nil targetPath:targetPath shouldResume:shouldResume]; 67 | } 68 | 69 | - (id)initWithRequest:(NSURLRequest *)urlRequest fileIdentifier:(NSString *)fileIdentifier targetPath:(NSString *)targetPath shouldResume:(BOOL)shouldResume { if ((self = [super initWithRequest:urlRequest])) { 70 | NSParameterAssert(targetPath != nil && urlRequest != nil); 71 | _shouldResume = shouldResume; 72 | 73 | self.fileIdentifier = fileIdentifier; 74 | 75 | // Ee assume that at least the directory has to exist on the targetPath 76 | BOOL isDirectory; 77 | if(![[NSFileManager defaultManager] fileExistsAtPath:targetPath isDirectory:&isDirectory]) { 78 | isDirectory = NO; 79 | } 80 | // \If targetPath is a directory, use the file name we got from the urlRequest. 81 | if (isDirectory) { 82 | NSString *fileName = [urlRequest.URL lastPathComponent]; 83 | _targetPath = [NSString pathWithComponents:@[targetPath, fileName]]; 84 | }else { 85 | _targetPath = targetPath; 86 | } 87 | 88 | // Download is saved into a temorary file and renamed upon completion. 89 | NSString *tempPath = [self tempPath]; 90 | 91 | // Do we need to resume the file? 92 | BOOL isResuming = [self updateByteStartRangeForRequest]; 93 | 94 | // Try to create/open a file at the target location 95 | if (!isResuming) { 96 | int fileDescriptor = open([tempPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666); 97 | if (fileDescriptor > 0) close(fileDescriptor); 98 | } 99 | 100 | self.outputStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:isResuming]; 101 | // If the output stream can't be created, instantly destroy the object. 102 | if (!self.outputStream) return nil; 103 | 104 | // Give the object its default completionBlock. 105 | [self setCompletionBlockWithSuccess:nil failure:nil]; 106 | } 107 | return self; 108 | } 109 | 110 | // updates the current request to set the correct start-byte-range. 111 | - (BOOL)updateByteStartRangeForRequest { 112 | BOOL isResuming = NO; 113 | if (self.shouldResume) { 114 | unsigned long long downloadedBytes = [self fileSizeForPath:[self tempPath]]; 115 | if (downloadedBytes > 1) { 116 | 117 | // If the the current download-request's data has been fully downloaded, but other causes of the operation failed (such as the inability of the incomplete temporary file copied to the target location), next, retry this download-request, the starting-value (equal to the incomplete temporary file size) will lead to an HTTP 416 out of range error, unless we subtract one byte here. (We don't know the final size before sending the request) 118 | downloadedBytes--; 119 | 120 | NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy]; 121 | NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes]; 122 | [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"]; 123 | self.request = mutableURLRequest; 124 | isResuming = YES; 125 | } 126 | } 127 | return isResuming; 128 | } 129 | 130 | #pragma mark - Public 131 | 132 | - (BOOL)deleteTempFileWithError:(NSError *__autoreleasing*)error { 133 | NSFileManager *fileManager = [NSFileManager new]; 134 | BOOL success = YES; 135 | @synchronized(self) { 136 | NSString *tempPath = [self tempPath]; 137 | if ([fileManager fileExistsAtPath:tempPath]) { 138 | success = [fileManager removeItemAtPath:[self tempPath] error:error]; 139 | } 140 | } 141 | return success; 142 | } 143 | 144 | - (NSString *)tempPath { 145 | NSString *tempPath = nil; 146 | if (self.fileIdentifier) { 147 | tempPath = [[[self class] cacheFolder] stringByAppendingPathComponent:self.fileIdentifier]; 148 | } 149 | else if (self.targetPath) { 150 | NSString *md5URLString = [[self class] md5StringForString:self.targetPath]; 151 | tempPath = [[[self class] cacheFolder] stringByAppendingPathComponent:md5URLString]; 152 | } 153 | return tempPath; 154 | } 155 | 156 | 157 | - (void)setProgressiveDownloadProgressBlock:(void (^)(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile))block { 158 | self.progressiveDownloadProgress = block; 159 | } 160 | 161 | - (void)setProgressiveDownloadCallbackQueue:(dispatch_queue_t)progressiveDownloadCallbackQueue { 162 | if (progressiveDownloadCallbackQueue != _progressiveDownloadCallbackQueue) { 163 | if (_progressiveDownloadCallbackQueue) { 164 | #if !OS_OBJECT_USE_OBJC 165 | dispatch_release(_progressiveDownloadCallbackQueue); 166 | #endif 167 | _progressiveDownloadCallbackQueue = NULL; 168 | } 169 | 170 | if (progressiveDownloadCallbackQueue) { 171 | #if !OS_OBJECT_USE_OBJC 172 | dispatch_retain(progressiveDownloadCallbackQueue); 173 | #endif 174 | _progressiveDownloadCallbackQueue = progressiveDownloadCallbackQueue; 175 | } 176 | } 177 | } 178 | 179 | 180 | #pragma mark - Private 181 | 182 | - (unsigned long long)fileSizeForPath:(NSString *)path { 183 | signed long long fileSize = 0; 184 | NSFileManager *fileManager = [NSFileManager new]; // default is not thread safe 185 | if ([fileManager fileExistsAtPath:path]) { 186 | NSError *error = nil; 187 | NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error]; 188 | if (!error && fileDict) { 189 | fileSize = [fileDict fileSize]; 190 | } 191 | } 192 | return fileSize; 193 | } 194 | 195 | #pragma mark - AFHTTPRequestOperation 196 | 197 | + (NSIndexSet *)acceptableStatusCodes { 198 | NSMutableIndexSet *acceptableStatusCodes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; 199 | [acceptableStatusCodes addIndex:416]; 200 | 201 | return acceptableStatusCodes; 202 | } 203 | 204 | #pragma mark - AFURLRequestOperation 205 | 206 | - (void)pause { 207 | [super pause]; 208 | [self updateByteStartRangeForRequest]; 209 | } 210 | 211 | - (id)responseObject { 212 | @synchronized(self) { 213 | if (!_responseObject && [self isFinished] && !self.error) { 214 | NSError *localError = nil; 215 | if ([self isCancelled]) { 216 | // should we clean up? most likely we don't. 217 | if (self.isDeletingTempFileOnCancel) { 218 | [self deleteTempFileWithError:&localError]; 219 | if (localError) { 220 | _fileError = localError; 221 | } 222 | } 223 | 224 | // loss of network connections = error set, but not cancel 225 | }else if(!self.error) { 226 | // move file to final position and capture error 227 | NSFileManager *fileManager = [NSFileManager new]; 228 | if (self.shouldOverwrite) { 229 | [fileManager removeItemAtPath:_targetPath error:NULL]; // avoid "File exists" error 230 | } 231 | [fileManager moveItemAtPath:[self tempPath] toPath:_targetPath error:&localError]; 232 | if (localError) { 233 | _fileError = localError; 234 | } else { 235 | _responseObject = _targetPath; 236 | } 237 | } 238 | } 239 | } 240 | 241 | return _responseObject; 242 | } 243 | 244 | - (NSError *)error { 245 | return _fileError ?: [super error]; 246 | } 247 | 248 | #pragma mark - NSURLConnectionDelegate 249 | 250 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 251 | [super connection:connection didReceiveResponse:response]; 252 | 253 | // check if we have the correct response 254 | NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 255 | if (![httpResponse isKindOfClass:[NSHTTPURLResponse class]]) return; 256 | 257 | // check for valid response to resume the download if possible 258 | long long totalContentLength = self.response.expectedContentLength; 259 | long long fileOffset = 0; 260 | if(httpResponse.statusCode == 206) { 261 | NSString *contentRange = [httpResponse.allHeaderFields valueForKey:@"Content-Range"]; 262 | if ([contentRange hasPrefix:@"bytes"]) { 263 | NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]]; 264 | if ([bytes count] == 4) { 265 | fileOffset = [bytes[1] longLongValue]; 266 | totalContentLength = [bytes[3] longLongValue]; // if this is *, it's converted to 0 267 | } 268 | } 269 | }else if (httpResponse.statusCode != 200){ 270 | return; 271 | } 272 | 273 | self.totalBytesReadPerDownload = 0; 274 | self.offsetContentLength = MAX(fileOffset, 0); 275 | self.totalContentLength = totalContentLength; 276 | 277 | // Truncate cache file to offset provided by server. 278 | // Using self.outputStream setProperty:@(_offsetContentLength) forKey:NSStreamFileCurrentOffsetKey]; will not work (in contrary to the documentation) 279 | NSString *tempPath = [self tempPath]; 280 | if ([self fileSizeForPath:tempPath] != _offsetContentLength) { 281 | [self.outputStream close]; 282 | BOOL isResuming = _offsetContentLength > 0; 283 | if (isResuming) { 284 | NSFileHandle *file = [NSFileHandle fileHandleForWritingAtPath:tempPath]; 285 | [file truncateFileAtOffset:_offsetContentLength]; 286 | [file closeFile]; 287 | } 288 | self.outputStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:isResuming]; 289 | [self.outputStream open]; 290 | } 291 | } 292 | 293 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 294 | if (![self.responseSerializer validateResponse:self.response data:data ?: [NSData data] error:NULL]) 295 | return; // don't write to output stream if any error occurs 296 | 297 | [super connection:connection didReceiveData:data]; 298 | 299 | // track custom bytes read because totalBytesRead persists between pause/resume. 300 | self.totalBytesReadPerDownload += [data length]; 301 | 302 | if (self.progressiveDownloadProgress) { 303 | dispatch_async(self.progressiveDownloadCallbackQueue ?: dispatch_get_main_queue(), ^{ 304 | self.progressiveDownloadProgress(self,(NSInteger)[data length], self.totalBytesRead, self.response.expectedContentLength,self.totalBytesReadPerDownload + self.offsetContentLength, self.totalContentLength); 305 | }); 306 | } 307 | } 308 | 309 | #pragma mark - Static 310 | 311 | + (NSString *)cacheFolder { 312 | NSFileManager *filemgr = [NSFileManager new]; 313 | static NSString *cacheFolder; 314 | 315 | if (!cacheFolder) { 316 | NSString *cacheDir = NSTemporaryDirectory(); 317 | cacheFolder = [cacheDir stringByAppendingPathComponent:kAFNetworkingIncompleteDownloadFolderName]; 318 | } 319 | 320 | // ensure all cache directories are there 321 | NSError *error = nil; 322 | if(![filemgr createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error]) { 323 | NSLog(@"Failed to create cache directory at %@", cacheFolder); 324 | cacheFolder = nil; 325 | } 326 | return cacheFolder; 327 | } 328 | 329 | // calculates the MD5 hash of a key 330 | + (NSString *)md5StringForString:(NSString *)string { 331 | const char *str = [string UTF8String]; 332 | unsigned char r[CC_MD5_DIGEST_LENGTH]; 333 | CC_MD5(str, (uint32_t)strlen(str), r); 334 | return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", 335 | 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]]; 336 | } 337 | 338 | @end 339 | --------------------------------------------------------------------------------