Merge pull request #368 from AFNetworking/experimental-multipart-streaming

Multipart Upload Streaming
This commit is contained in:
Mattt Thompson 2012-06-11 07:19:00 -07:00
commit 2071196450
2 changed files with 87 additions and 38 deletions

View file

@ -43,17 +43,18 @@
NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change"; NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change";
static NSString * const kAFMultipartFormBoundary = @"Boundary+0xAbCdEfGbOuNdArY";
@interface AFMultipartFormData : NSObject <AFMultipartFormData> { @interface AFMultipartFormData : NSObject <AFMultipartFormData> {
@private @private
NSMutableURLRequest *_request;
NSStringEncoding _stringEncoding; NSStringEncoding _stringEncoding;
NSMutableData *_mutableData; NSOutputStream *_outputStream;
NSString *_temporaryFilePath;
} }
@property (readonly) NSData *data; - (id)initWithURLRequest:(NSMutableURLRequest *)request
stringEncoding:(NSStringEncoding)encoding;
- (id)initWithStringEncoding:(NSStringEncoding)encoding; - (NSMutableURLRequest *)requestByFinalizingMultipartFormData;
@end @end
@ -88,7 +89,7 @@ static NSString * AFBase64EncodedStringFromString(NSString *string) {
} }
static uint8_t const kAFBase64EncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static uint8_t const kAFBase64EncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
NSUInteger idx = (i / 3) * 4; NSUInteger idx = (i / 3) * 4;
output[idx + 0] = kAFBase64EncodingTable[(value >> 18) & 0x3F]; output[idx + 0] = kAFBase64EncodingTable[(value >> 18) & 0x3F];
output[idx + 1] = kAFBase64EncodingTable[(value >> 12) & 0x3F]; output[idx + 1] = kAFBase64EncodingTable[(value >> 12) & 0x3F];
@ -276,7 +277,7 @@ static NSString * AFPropertyListStringFromParameters(NSDictionary *parameters) {
// Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 // Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
NSString *preferredLanguageCodes = [[NSLocale preferredLanguages] componentsJoinedByString:@", "]; NSString *preferredLanguageCodes = [[NSLocale preferredLanguages] componentsJoinedByString:@", "];
[self setDefaultHeader:@"Accept-Language" value:[NSString stringWithFormat:@"%@, en-us;q=0.8", preferredLanguageCodes]]; [self setDefaultHeader:@"Accept-Language" value:[NSString stringWithFormat:@"%@, en-us;q=0.8", preferredLanguageCodes]];
#if __IPHONE_OS_VERSION_MIN_REQUIRED #if __IPHONE_OS_VERSION_MIN_REQUIRED
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
[self setDefaultHeader:@"User-Agent" value:[NSString stringWithFormat:@"%@/%@ (%@, %@ %@, %@, Scale/%f)", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey], [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey], @"unknown", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion], [[UIDevice currentDevice] model], ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [[UIScreen mainScreen] scale] : 1.0)]]; [self setDefaultHeader:@"User-Agent" value:[NSString stringWithFormat:@"%@/%@ (%@, %@ %@, %@, Scale/%f)", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey], [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey], @"unknown", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion], [[UIDevice currentDevice] model], ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [[UIScreen mainScreen] scale] : 1.0)]];
@ -350,7 +351,7 @@ static void AFNetworkReachabilityCallback(SCNetworkReachabilityRef __unused targ
if (block) { if (block) {
block(status); block(status);
} }
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingReachabilityDidChangeNotification object:[NSNumber numberWithInt:status]]; [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingReachabilityDidChangeNotification object:[NSNumber numberWithInt:status]];
} }
@ -368,9 +369,9 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
if (!self.baseURL) { if (!self.baseURL) {
return; return;
} }
self.networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [[self.baseURL host] UTF8String]); self.networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [[self.baseURL host] UTF8String]);
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status){ AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status){
self.networkReachabilityStatus = status; self.networkReachabilityStatus = status;
if (self.networkReachabilityStatusBlock) { if (self.networkReachabilityStatusBlock) {
@ -491,7 +492,7 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData>formData))block constructingBodyWithBlock:(void (^)(id <AFMultipartFormData>formData))block
{ {
NSMutableURLRequest *request = [self requestWithMethod:method path:path parameters:nil]; NSMutableURLRequest *request = [self requestWithMethod:method path:path parameters:nil];
__block AFMultipartFormData *formData = [[AFMultipartFormData alloc] initWithStringEncoding:self.stringEncoding]; __block AFMultipartFormData *formData = [[[AFMultipartFormData alloc] initWithURLRequest:request stringEncoding:self.stringEncoding] autorelease];
for (AFQueryStringComponent *component in AFQueryStringComponentsFromKeyAndValue(nil, parameters)) { for (AFQueryStringComponent *component in AFQueryStringComponentsFromKeyAndValue(nil, parameters)) {
NSData *data = nil; NSData *data = nil;
@ -505,17 +506,12 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
[formData appendPartWithFormData:data name:[component.key description]]; [formData appendPartWithFormData:data name:[component.key description]];
} }
} }
if (block) { if (block) {
block(formData); block(formData);
} }
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kAFMultipartFormBoundary] forHTTPHeaderField:@"Content-Type"]; return [formData requestByFinalizingMultipartFormData];
[request setHTTPBody:[formData data]];
[formData autorelease];
return request;
} }
- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)urlRequest - (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)urlRequest
@ -535,9 +531,9 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
if (!operation) { if (!operation) {
operation = [[[AFHTTPRequestOperation alloc] initWithRequest:urlRequest] autorelease]; operation = [[[AFHTTPRequestOperation alloc] initWithRequest:urlRequest] autorelease];
} }
[operation setCompletionBlockWithSuccess:success failure:failure]; [operation setCompletionBlockWithSuccess:success failure:failure];
return operation; return operation;
} }
@ -586,7 +582,7 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
}); });
dispatch_release(dispatchGroup); dispatch_release(dispatchGroup);
}]; }];
NSPredicate *finishedOperationPredicate = [NSPredicate predicateWithFormat:@"isFinished == YES"]; NSPredicate *finishedOperationPredicate = [NSPredicate predicateWithFormat:@"isFinished == YES"];
for (AFHTTPRequestOperation *operation in operations) { for (AFHTTPRequestOperation *operation in operations) {
@ -670,6 +666,25 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
#pragma mark - #pragma mark -
static NSString * const kAFMultipartTemporaryFileDirectoryName = @"com.alamofire.uploads";
static NSString * AFMultipartTemporaryFileDirectoryPath() {
static NSString *multipartTemporaryFilePath = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
multipartTemporaryFilePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:kAFMultipartTemporaryFileDirectoryName] copy];
NSError *error = nil;
if(![[NSFileManager defaultManager] createDirectoryAtPath:multipartTemporaryFilePath withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"Failed to create multipary temporary file directory at %@", multipartTemporaryFilePath);
}
});
return multipartTemporaryFilePath;
}
static NSString * const kAFMultipartFormBoundary = @"Boundary+0xAbCdEfGbOuNdArY";
static NSString * const kAFMultipartFormCRLF = @"\r\n"; static NSString * const kAFMultipartFormCRLF = @"\r\n";
static inline NSString * AFMultipartFormInitialBoundary() { static inline NSString * AFMultipartFormInitialBoundary() {
@ -685,45 +700,76 @@ static inline NSString * AFMultipartFormFinalBoundary() {
} }
@interface AFMultipartFormData () @interface AFMultipartFormData ()
@property (readwrite, nonatomic, retain) NSMutableURLRequest *request;
@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding; @property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding;
@property (readwrite, nonatomic, retain) NSMutableData *mutableData; @property (readwrite, nonatomic, retain) NSOutputStream *outputStream;
@property (readwrite, nonatomic, copy) NSString *temporaryFilePath;
@end @end
@implementation AFMultipartFormData @implementation AFMultipartFormData
@synthesize request = _request;
@synthesize stringEncoding = _stringEncoding; @synthesize stringEncoding = _stringEncoding;
@synthesize mutableData = _mutableData; @synthesize outputStream = _outputStream;
@synthesize temporaryFilePath = _temporaryFilePath;
- (id)initWithStringEncoding:(NSStringEncoding)encoding { - (id)initWithURLRequest:(NSMutableURLRequest *)request
stringEncoding:(NSStringEncoding)encoding
{
self = [super init]; self = [super init];
if (!self) { if (!self) {
return nil; return nil;
} }
self.request = request;
self.stringEncoding = encoding; self.stringEncoding = encoding;
self.mutableData = [NSMutableData dataWithLength:0];
self.temporaryFilePath = [AFMultipartTemporaryFileDirectoryPath() stringByAppendingPathComponent:[NSString stringWithFormat:@"%u", [[self.request URL] hash]]];
self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.temporaryFilePath append:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[self.outputStream scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[self.outputStream open];
return self; return self;
} }
- (void)dealloc { - (void)dealloc {
[_mutableData release]; [_request release];
if (_outputStream) {
[_outputStream close];
[_outputStream release];
_outputStream = nil;
}
[_temporaryFilePath release];
[super dealloc]; [super dealloc];
} }
- (NSData *)data { - (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
NSMutableData *finalizedData = [NSMutableData dataWithData:self.mutableData]; [self appendData:[AFMultipartFormFinalBoundary() dataUsingEncoding:self.stringEncoding]];
[finalizedData appendData:[AFMultipartFormFinalBoundary() dataUsingEncoding:self.stringEncoding]];
return finalizedData; [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kAFMultipartFormBoundary] forHTTPHeaderField:@"Content-Type"];
[self.request setValue:[[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] stringValue] forHTTPHeaderField:@"Content-Length"];
[self.request setHTTPBodyStream:[NSInputStream inputStreamWithFileAtPath:self.temporaryFilePath]];
[self.outputStream close];
return self.request;
} }
#pragma mark - AFMultipartFormData #pragma mark - AFMultipartFormData
- (void)appendPartWithHeaders:(NSDictionary *)headers body:(NSData *)body { - (void)appendBoundary {
if ([self.mutableData length] == 0) { if ([[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] integerValue] == 0) {
[self appendString:AFMultipartFormInitialBoundary()]; [self appendString:AFMultipartFormInitialBoundary()];
} else { } else {
[self appendString:AFMultipartFormEncapsulationBoundary()]; [self appendString:AFMultipartFormEncapsulationBoundary()];
} }
}
- (void)appendPartWithHeaders:(NSDictionary *)headers body:(NSData *)body {
[self appendBoundary];
for (NSString *field in [headers allKeys]) { for (NSString *field in [headers allKeys]) {
[self appendString:[NSString stringWithFormat:@"%@: %@%@", field, [headers valueForKey:field], kAFMultipartFormCRLF]]; [self appendString:[NSString stringWithFormat:@"%@: %@%@", field, [headers valueForKey:field], kAFMultipartFormCRLF]];
@ -754,7 +800,7 @@ static inline NSString * AFMultipartFormFinalBoundary() {
[userInfo setValue:fileURL forKey:NSURLErrorFailingURLErrorKey]; [userInfo setValue:fileURL forKey:NSURLErrorFailingURLErrorKey];
[userInfo setValue:NSLocalizedString(@"Expected URL to be a file URL", nil) forKey:NSLocalizedFailureReasonErrorKey]; [userInfo setValue:NSLocalizedString(@"Expected URL to be a file URL", nil) forKey:NSLocalizedFailureReasonErrorKey];
if (error != NULL) { if (error != NULL) {
*error = [[[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadURL userInfo:userInfo] autorelease]; *error = [[[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadURL userInfo:userInfo] autorelease];
} }
return NO; return NO;
@ -775,12 +821,15 @@ static inline NSString * AFMultipartFormFinalBoundary() {
} }
} }
- (void)appendData:(NSData *)data {
[self.mutableData appendData:data];
}
- (void)appendString:(NSString *)string { - (void)appendString:(NSString *)string {
[self appendData:[string dataUsingEncoding:self.stringEncoding]]; [self appendData:[string dataUsingEncoding:self.stringEncoding]];
} }
- (void)appendData:(NSData *)data {
if ([self.outputStream hasSpaceAvailable]) {
const uint8_t *dataBuffer = (uint8_t *) [data bytes];
[self.outputStream write:&dataBuffer[0] maxLength:[data length]];
}
}
@end @end

View file

@ -282,7 +282,7 @@
F8E469571395739C00DB05C8 /* Project object */ = { F8E469571395739C00DB05C8 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 0420; LastUpgradeCheck = 0430;
ORGANIZATIONNAME = Gowalla; ORGANIZATIONNAME = Gowalla;
}; };
buildConfigurationList = F8E4695A1395739C00DB05C8 /* Build configuration list for PBXProject "AFNetworking iOS Example" */; buildConfigurationList = F8E4695A1395739C00DB05C8 /* Build configuration list for PBXProject "AFNetworking iOS Example" */;