Skip to content

Commit

Permalink
WIP - implemented basic functionality of downloader - need to support…
Browse files Browse the repository at this point in the history
… resume and writing to disk.
  • Loading branch information
Josh Johnson committed Nov 4, 2012
1 parent 0d90fd1 commit 8b1f10f
Show file tree
Hide file tree
Showing 8 changed files with 5,285 additions and 33 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -15,4 +15,7 @@
xcuserdata
profile
*.moved-aside
DerivedData
DerivedData

# Custom
Sample/build
10 changes: 10 additions & 0 deletions AFAcceleratedDownloadRequestOperation.h
Expand Up @@ -23,4 +23,14 @@

@interface AFAcceleratedDownloadRequestOperation : AFHTTPRequestOperation

/** Defines the maximum size of single chunks for downloading a file */
@property (nonatomic, assign) NSUInteger maximumChunkSize;

/** Designated Initializer to create a download operation with resume support
* @param urlRequest request to the resource being downloaded
* @param shouldResume should the download request be resumed from a prior attempt or start over
* @return instance of class AFAcceleratedDownloadRequestOperation
*/
- (id)initWithRequest:(NSURLRequest *)urlRequest shouldResume:(BOOL)shouldResume;

@end
222 changes: 222 additions & 0 deletions AFAcceleratedDownloadRequestOperation.m
Expand Up @@ -19,12 +19,234 @@
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

#import <tgmath.h>
#import "AFAcceleratedDownloadRequestOperation.h"

#if !__has_feature(objc_arc)
#error "AFAcceleratedDownloadRequestOperation requires compiling with ARC."
#endif

NSString * const kAFInternalCachedURLFolderPrefix = @"af_";
NSString * const kAFInternalCachedFolderName = @"Incomplete";
NSString * const kAFInternalDownloadInformation = @"af_download.plist";
static const NSUInteger kAFInternalDefaultMaximumChunkSize = 4;

@interface AFAcceleratedDownloadRequestOperation ()

@property (nonatomic, strong) NSData *responseData;
@property (nonatomic, assign, getter = shouldResume) BOOL resume;
@property (nonatomic, strong) NSOperationQueue *innerQueue;

@property (nonatomic, strong) NSMutableArray *downloadedData;

+ (NSString *)downloadCacheFolder;
+ (NSURL *)downloadCacheURLForURL:(NSURL *)url;

- (NSOperation *)HEADOperationForURL:(NSURL *)url;
- (NSSet *)operationsForURL:(NSURL *)url contentSize:(NSUInteger)contentSize chunks:(NSUInteger)chunks;

@end

@implementation AFAcceleratedDownloadRequestOperation

#pragma mark - Class methods

+ (NSString *)downloadCacheFolder
{
static dispatch_once_t downloadFolder_onceToken;
static NSString *af_internal_acceleratedDownloadRequestCacheFolder = nil;

dispatch_once(&downloadFolder_onceToken, ^{
af_internal_acceleratedDownloadRequestCacheFolder = [NSTemporaryDirectory() stringByAppendingString:kAFInternalCachedFolderName];

NSError *folderError;
[[NSFileManager defaultManager] createDirectoryAtPath:af_internal_acceleratedDownloadRequestCacheFolder
withIntermediateDirectories:YES
attributes:nil
error:&folderError];

if (folderError) {
NSLog(@"Failed to create cache folder for download: %@", af_internal_acceleratedDownloadRequestCacheFolder);
}
});

return af_internal_acceleratedDownloadRequestCacheFolder;
}

+ (NSURL *)downloadCacheURLForURL:(NSURL *)url
{
NSURL *folderURL = [NSURL fileURLWithPath:[self downloadCacheFolder] isDirectory:YES];
NSString *folderName = [NSString stringWithFormat:@"%@%u", kAFInternalCachedURLFolderPrefix, url.absoluteString.hash];

NSURL *urlCacheFolderURL = [folderURL URLByAppendingPathComponent:folderName isDirectory:YES];

static dispatch_once_t urlFolder_onceToken;
dispatch_once(&urlFolder_onceToken, ^{
NSError *folderError;
[[NSFileManager defaultManager] createDirectoryAtURL:urlCacheFolderURL
withIntermediateDirectories:YES
attributes:nil
error:&folderError];
if (folderError) {
NSLog(@"Failed to create cache folder: %@ for URL: %@", [urlCacheFolderURL absoluteString], url);
}

});

return urlCacheFolderURL;
}

#pragma mark - Life cycle

- (id)initWithRequest:(NSURLRequest *)urlRequest shouldResume:(BOOL)shouldResume
{
if (self = [super initWithRequest:urlRequest]) {
_innerQueue = [[NSOperationQueue alloc] init];
[_innerQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
_resume = shouldResume;
_maximumChunkSize = kAFInternalDefaultMaximumChunkSize;
_downloadedData = [NSMutableArray array];
}
return self;
}

- (id)initWithRequest:(NSURLRequest *)urlRequest
{
return [self initWithRequest:urlRequest shouldResume:YES];
}

#pragma mark - AFHTTPRequestOperation overrides

- (void)start
{
// Build Info Dictionary
// TODO: Setup for resuming

// Start HEAD request to begin downloading process
NSOperation *headOperation = [self HEADOperationForURL:[self.request URL]];
[self.innerQueue addOperation:headOperation];
}

#pragma mark - Properties

- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *, id))success failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure
{
__block typeof(self) weak_self = self;
[self setCompletionBlock:^{
if (success) {
success(weak_self, weak_self.responseData);
}
}];
}

#pragma mark - Helpers

- (NSSet *)operationsForURL:(NSURL *)url contentSize:(NSUInteger)contentSize chunks:(NSUInteger)chunks
{
NSMutableSet *operationSet = [NSMutableSet set];

NSInteger chunkSize = contentSize / chunks;
NSInteger chunkRemainder = fmod(contentSize, chunks);
NSInteger chunkPosition = 0;
NSUInteger downloadNumber = 0;

[self.downloadedData removeAllObjects];
self.downloadedData = [NSMutableArray array];

__weak typeof(self) weak_self = self;

for (NSInteger i = 0; i < chunks; i++) {
NSInteger divisonBuffer = 0;
if (i == (chunks - 1)) {
divisonBuffer = -chunkRemainder;
}

NSOutputStream *stream = [NSOutputStream outputStreamToMemory];

NSNumber *requestSizeEnd = @(chunkPosition + chunkSize + divisonBuffer);
NSString *rangeString = [NSString stringWithFormat:@"bytes=%i-%@/%i", chunkPosition, requestSizeEnd, contentSize];
NSLog(@"range string: %@", rangeString);

NSMutableURLRequest *downloadRequest = [NSMutableURLRequest requestWithURL:url];
[downloadRequest setValue:@"bytes" forHTTPHeaderField:@"If-Ranges"];
[downloadRequest setValue:rangeString forHTTPHeaderField:@"Range"];

AFHTTPRequestOperation *downloadOperation = [[AFHTTPRequestOperation alloc] initWithRequest:downloadRequest];
// [downloadOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
// float percentDone = ((float)((int)totalBytesRead) / (float)((int)totalBytesExpectedToRead)) * 100;
// NSLog(@"download number %i %f%%: %u %llu %llu %@", downloadNumber, percentDone, bytesRead, totalBytesRead, totalBytesExpectedToRead, requestSize);
// }];

[downloadOperation setOutputStream:stream];
[weak_self.downloadedData addObject:stream];

[downloadOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSOutputStream *stream = [weak_self.downloadedData objectAtIndex:downloadNumber];
[stream close];
// NSLog(@"download number %i finished", downloadNumber);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"download error: %@", [error localizedDescription]);
}];

[operationSet addObject:downloadOperation];
chunkPosition += chunkSize + 1;

downloadNumber++;
}

return operationSet;
}

- (NSOperation *)HEADOperationForURL:(NSURL *)url
{
NSParameterAssert(url);

NSMutableURLRequest *headRequest = [NSMutableURLRequest requestWithURL:self.request.URL];
[headRequest setHTTPMethod:@"HEAD"];
[headRequest setValue:@"bytes" forHTTPHeaderField:@"If-Ranges"];
[headRequest setValue:@"bytes=0-1/1" forHTTPHeaderField:@"Range"];

__weak typeof(self) weak_self = self;

AFHTTPRequestOperation *headOperation = [[AFHTTPRequestOperation alloc] initWithRequest:headRequest];
[headOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// NSInteger statusCode = operation.response.statusCode;
// NSLog(@"%i", statusCode);

NSString *contentLengthString = [operation.response.allHeaderFields objectForKey:@"Content-Range"];
contentLengthString = [contentLengthString stringByReplacingOccurrencesOfString:@"bytes 0-1/" withString:@""];
NSInteger contentLength = [contentLengthString integerValue];

// TODO: If 200, only create one request since it should be 206 when supported

NSSet *operations = [weak_self operationsForURL:operation.request.URL contentSize:contentLength chunks:weak_self.maximumChunkSize];
NSOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{

NSMutableData *compiledData = [NSMutableData data];
for (id partialData in weak_self.downloadedData) {
[compiledData appendData:[partialData propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
}

weak_self.responseData = compiledData;

if (weak_self.completionBlock) {
weak_self.completionBlock();
}
}];

for (NSOperation *operation in operations) {
[finishOperation addDependency:operation];
[weak_self.innerQueue addOperation:operation];
}

[weak_self.innerQueue addOperation:finishOperation];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

// handle HEAD error

}];

return headOperation;
}

@end
Expand Up @@ -26,6 +26,8 @@
A45E0B3A1616BA9E00E23C6D /* AFURLConnectionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = A45E0B2F1616BA9E00E23C6D /* AFURLConnectionOperation.m */; };
A45E0B3B1616BA9E00E23C6D /* AFXMLRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = A45E0B311616BA9E00E23C6D /* AFXMLRequestOperation.m */; };
A45E0B3C1616BA9E00E23C6D /* UIImageView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = A45E0B331616BA9E00E23C6D /* UIImageView+AFNetworking.m */; };
A45E0B3F1617EEDE00E23C6D /* JJViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A45E0B3E1617EEDE00E23C6D /* JJViewController.m */; };
A477FE88164635A200407DFE /* JJViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = A477FE87164635A200407DFE /* JJViewController.xib */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -63,6 +65,9 @@
A45E0B311616BA9E00E23C6D /* AFXMLRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AFXMLRequestOperation.m; path = Libraries/AFNetworking/AFNetworking/AFXMLRequestOperation.m; sourceTree = "<group>"; };
A45E0B321616BA9E00E23C6D /* UIImageView+AFNetworking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImageView+AFNetworking.h"; path = "Libraries/AFNetworking/AFNetworking/UIImageView+AFNetworking.h"; sourceTree = "<group>"; };
A45E0B331616BA9E00E23C6D /* UIImageView+AFNetworking.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImageView+AFNetworking.m"; path = "Libraries/AFNetworking/AFNetworking/UIImageView+AFNetworking.m"; sourceTree = "<group>"; };
A45E0B3D1617EEDE00E23C6D /* JJViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JJViewController.h; sourceTree = "<group>"; };
A45E0B3E1617EEDE00E23C6D /* JJViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JJViewController.m; sourceTree = "<group>"; };
A477FE87164635A200407DFE /* JJViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JJViewController.xib; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -113,6 +118,9 @@
children = (
A45E0AE71616B6F200E23C6D /* JJAppDelegate.h */,
A45E0AE81616B6F200E23C6D /* JJAppDelegate.m */,
A45E0B3D1617EEDE00E23C6D /* JJViewController.h */,
A45E0B3E1617EEDE00E23C6D /* JJViewController.m */,
A477FE87164635A200407DFE /* JJViewController.xib */,
A45E0ADF1616B6F200E23C6D /* Supporting Files */,
);
path = "AFAcceleratedDownloadRequestOperation-Sample";
Expand Down Expand Up @@ -231,6 +239,7 @@
A45E0AEB1616B6F200E23C6D /* Default.png in Resources */,
A45E0AED1616B6F200E23C6D /* Default@2x.png in Resources */,
A45E0AEF1616B6F200E23C6D /* Default-568h@2x.png in Resources */,
A477FE88164635A200407DFE /* JJViewController.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -253,6 +262,7 @@
A45E0B3A1616BA9E00E23C6D /* AFURLConnectionOperation.m in Sources */,
A45E0B3B1616BA9E00E23C6D /* AFXMLRequestOperation.m in Sources */,
A45E0B3C1616BA9E00E23C6D /* UIImageView+AFNetworking.m in Sources */,
A45E0B3F1617EEDE00E23C6D /* JJViewController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Expand Up @@ -7,45 +7,16 @@
//

#import "JJAppDelegate.h"

#import "AFNetworking.h"
#import "JJViewController.h"

@implementation JJAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self.window setRootViewController:[[JJViewController alloc] initWithNibName:nil bundle:nil]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application
{
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end
@@ -0,0 +1,13 @@
//
// JJViewController.h
// AFAcceleratedDownloadRequestOperation-Sample
//
// Created by Josh Johnson on 9/29/12.
// Copyright (c) 2012 jnjosh.com. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface JJViewController : UIViewController

@end

0 comments on commit 8b1f10f

Please sign in to comment.