reimplemented multipart form data to use a body streaming object that aggregates file streams and in-memory data objects to compose the request body just in time. also trimmed the multipart form body public API and added CoreMobileServices framework to enable lookup of MIME type by file extension.

This commit is contained in:
Max Lansing 2012-06-30 16:32:11 -07:00
parent 5e90a17c76
commit ced167f867
3 changed files with 811 additions and 571 deletions

View file

@ -24,7 +24,7 @@
@class AFHTTPRequestOperation; @class AFHTTPRequestOperation;
@protocol AFHTTPClientOperation; @protocol AFHTTPClientOperation;
@protocol AFMultipartFormData; @protocol AFStreamingMultipartFormData;
/** /**
Posted when network reachability changes. Posted when network reachability changes.
@ -304,7 +304,7 @@ extern NSString * AFQueryStringFromParametersWithEncoding(NSDictionary *paramete
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method - (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
path:(NSString *)path path:(NSString *)path
parameters:(NSDictionary *)parameters parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block; constructingBodyWithBlock:(void (^)(id <AFStreamingMultipartFormData> formData))block;
///------------------------------- ///-------------------------------
/// @name Creating HTTP Operations /// @name Creating HTTP Operations
@ -457,15 +457,10 @@ extern NSString * AFQueryStringFromParametersWithEncoding(NSDictionary *paramete
@see `AFHTTPClient -multipartFormRequestWithMethod:path:parameters:constructingBodyWithBlock:` @see `AFHTTPClient -multipartFormRequestWithMethod:path:parameters:constructingBodyWithBlock:`
*/ */
@protocol AFMultipartFormData
/**
Appends HTTP headers, followed by the encoded data and the multipart form boundary.
@param headers The HTTP headers to be appended to the form data. @protocol AFStreamingMultipartFormData
@param body The data to be encoded and appended to the form data.
*/
- (void)appendPartWithHeaders:(NSDictionary *)headers body:(NSData *)body;
/** /**
Appends the HTTP headers `Content-Disposition: form-data; name=#{name}"`, followed by the encoded data and the multipart form boundary. Appends the HTTP headers `Content-Disposition: form-data; name=#{name}"`, followed by the encoded data and the multipart form boundary.
@ -473,18 +468,9 @@ extern NSString * AFQueryStringFromParametersWithEncoding(NSDictionary *paramete
@param data The data to be encoded and appended to the form data. @param data The data to be encoded and appended to the form data.
@param name The name to be associated with the specified data. This parameter must not be `nil`. @param name The name to be associated with the specified data. This parameter must not be `nil`.
*/ */
- (void)appendPartWithFormData:(NSData *)data name:(NSString *)name; - (void)appendPartWithFormData:(NSData *)data name:(NSString *)name;
/**
Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary.
@param data The data to be encoded and appended to the form data.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
@param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
@param filename The filename to be associated with the specified data. This parameter must not be `nil`.
*/
- (void)appendPartWithFileData:(NSData *)data name:(NSString *)name fileName:(NSString *)fileName mimeType:(NSString *)mimeType;
/** /**
Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary. Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary.
@ -496,34 +482,8 @@ extern NSString * AFQueryStringFromParametersWithEncoding(NSDictionary *paramete
@discussion The filename and MIME type for this data in the form will be automatically generated, using `NSURLResponse` `-suggestedFilename` and `-MIMEType`, respectively. @discussion The filename and MIME type for this data in the form will be automatically generated, using `NSURLResponse` `-suggestedFilename` and `-MIMEType`, respectively.
*/ */
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL name:(NSString *)name error:(NSError **)error; - (BOOL)appendPartWithFileURL:(NSURL *)fileURL name:(NSString *)name error:(NSError **)error;
/**
Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary, using NSInputStream to read input.
@param fileURL The URL corresponding to the file whose content will be appended to the form.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
@param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
@discussion The filename is generated from the last component of the streamingURL parameter. The size of the buffer used to copy from streamingURL to the output stream is defined by the constant kAFStreamToStreamBufferSize.
*/
- (void)appendPartWithStreamingURL:(NSURL *)streamingURL
name:(NSString *)name
mimeType:(NSString *)mimeType;
/**
Appends encoded data to the form data.
@param data The data to be encoded and appended to the form data.
*/
- (void)appendData:(NSData *)data;
/**
Appends a string to the form data.
@param string The string to be encoded and appended to the form data.
*/
- (void)appendString:(NSString *)string;
@end @end

View file

@ -30,6 +30,9 @@
#if __IPHONE_OS_VERSION_MIN_REQUIRED #if __IPHONE_OS_VERSION_MIN_REQUIRED
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#else
#import <CoreServices/CoreServices.h>
#endif #endif
#ifdef _SYSTEMCONFIGURATION_H #ifdef _SYSTEMCONFIGURATION_H
@ -41,10 +44,10 @@
#import <netdb.h> #import <netdb.h>
#endif #endif
NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change"; NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change";
@interface AFMultipartFormData : NSObject <AFMultipartFormData> @interface AFStreamingMultipartFormData : NSObject <AFStreamingMultipartFormData>
- (id)initWithURLRequest:(NSMutableURLRequest *)request - (id)initWithURLRequest:(NSMutableURLRequest *)request
stringEncoding:(NSStringEncoding)encoding; stringEncoding:(NSStringEncoding)encoding;
@ -52,6 +55,17 @@ NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire
@end @end
@interface AFMultipartBodyStream : NSInputStream <NSStreamDelegate>
-(id)initWithStringEncoding:(NSStringEncoding)encoding;
-(BOOL)addFileFromURL:(NSURL *)fileURL name:(NSString *)name error:(NSError **)error;
-(void)addFormData:(NSData *)data name:(NSString *)name;
-(BOOL)empty;
-(NSUInteger)contentLength;
@end
#pragma mark - #pragma mark -
#ifdef _SYSTEMCONFIGURATION_H #ifdef _SYSTEMCONFIGURATION_H
@ -454,10 +468,6 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
[request setHTTPMethod:method]; [request setHTTPMethod:method];
[request setAllHTTPHeaderFields:self.defaultHeaders]; [request setAllHTTPHeaderFields:self.defaultHeaders];
if ([method isEqualToString:@"GET"] || [method isEqualToString:@"HEAD"]) {
[request setHTTPShouldUsePipelining:YES];
}
if (parameters) { if (parameters) {
if ([method isEqualToString:@"GET"] || [method isEqualToString:@"HEAD"] || [method isEqualToString:@"DELETE"]) { if ([method isEqualToString:@"GET"] || [method isEqualToString:@"HEAD"] || [method isEqualToString:@"DELETE"]) {
url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:[path rangeOfString:@"?"].location == NSNotFound ? @"?%@" : @"&%@", AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding)]]; url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:[path rangeOfString:@"?"].location == NSNotFound ? @"?%@" : @"&%@", AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding)]];
@ -487,12 +497,14 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) {
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method - (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
path:(NSString *)path path:(NSString *)path
parameters:(NSDictionary *)parameters parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData>formData))block constructingBodyWithBlock:(void (^)(id <AFStreamingMultipartFormData>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] initWithURLRequest:request stringEncoding:self.stringEncoding] autorelease];
if (parameters) { __block AFStreamingMultipartFormData * formData = [[[AFStreamingMultipartFormData alloc] initWithURLRequest:request stringEncoding:self.stringEncoding] autorelease];
// __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;
if ([component.value isKindOfClass:[NSData class]]) { if ([component.value isKindOfClass:[NSData class]]) {
@ -505,7 +517,6 @@ 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);
@ -700,18 +711,23 @@ static inline NSString * AFMultipartFormFinalBoundary() {
return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, kAFMultipartFormBoundary, kAFMultipartFormCRLF]; return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, kAFMultipartFormBoundary, kAFMultipartFormCRLF];
} }
@interface AFMultipartFormData () #pragma mark --
#pragma mark AFStreamingMultipartFormData
@interface AFStreamingMultipartFormData () {
AFMultipartBodyStream * bodyStream;
}
@property (readwrite, nonatomic, retain) NSMutableURLRequest *request; @property (readwrite, nonatomic, retain) NSMutableURLRequest *request;
@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding; @property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding;
@property (readwrite, nonatomic, retain) NSOutputStream *outputStream;
@property (readwrite, nonatomic, copy) NSString *temporaryFilePath;
@end @end
@implementation AFMultipartFormData @implementation AFStreamingMultipartFormData
@synthesize request = _request; @synthesize request = _request;
@synthesize stringEncoding = _stringEncoding; @synthesize stringEncoding = _stringEncoding;
@synthesize outputStream = _outputStream;
@synthesize temporaryFilePath = _temporaryFilePath;
- (id)initWithURLRequest:(NSMutableURLRequest *)request - (id)initWithURLRequest:(NSMutableURLRequest *)request
stringEncoding:(NSStringEncoding)encoding stringEncoding:(NSStringEncoding)encoding
@ -720,172 +736,432 @@ static inline NSString * AFMultipartFormFinalBoundary() {
if (!self) { if (!self) {
return nil; return nil;
} }
self.request = request; self.request = request;
self.stringEncoding = encoding; self.stringEncoding = encoding;
bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
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 {
[_request release]; [bodyStream release];
if (_outputStream) {
[_outputStream close];
[_outputStream release];
_outputStream = nil;
}
[_temporaryFilePath release];
[super dealloc]; [super dealloc];
} }
-(void)appendPartWithFormData:(NSData *)data name:(NSString *)name {
[bodyStream addFormData:data name:name];
}
-(BOOL)appendPartWithFileURL:(NSURL *)fileURL name:(NSString *)name error:(NSError **)error {
return [bodyStream addFileFromURL:fileURL name:name error:error];
}
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData { - (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
// Close the stream and return the original request if no data has been written //return the original request if no data has been added
if ([[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] integerValue] == 0) { if ([bodyStream empty]) {
[self.outputStream close];
return self.request; return self.request;
} }
[self appendData:[AFMultipartFormFinalBoundary() dataUsingEncoding:self.stringEncoding]];
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kAFMultipartFormBoundary] forHTTPHeaderField:@"Content-Type"]; [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 setValue:[NSString stringWithFormat:@"%d",[bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
[self.request setHTTPBodyStream:bodyStream];
[self.outputStream close];
[self.request setHTTPBodyStream:[NSInputStream inputStreamWithFileAtPath:self.temporaryFilePath]];
return self.request; return self.request;
} }
#pragma mark - AFMultipartFormData -(void)appendData:(NSData *)data {
- (void)appendBoundary {
if ([[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] integerValue] == 0) {
[self appendString:AFMultipartFormInitialBoundary()];
} else {
[self appendString:AFMultipartFormEncapsulationBoundary()];
}
} }
- (void)appendPartWithHeaders:(NSDictionary *)headers @end
body:(NSData *)body
{
[self appendBoundary];
for (NSString *field in [headers allKeys]) { #pragma mark --
[self appendString:[NSString stringWithFormat:@"%@: %@%@", field, [headers valueForKey:field], kAFMultipartFormCRLF]]; #pragma mark AFMultipartBodyStream
@interface AFMultipartBodyStream () {
//all undocumented nsstream/nsinputstream/CFReadStream bull shit
CFReadStreamClientCallBack copiedCallback;
CFStreamClientContext copiedContext;
CFOptionFlags requestedEvents;
NSStreamStatus streamStatus;
id <NSStreamDelegate> delegate;
NSMutableArray * fileNames;
NSMutableDictionary * fileURLs;
NSMutableDictionary * fileHeaders;
NSMutableArray * formNames;
NSMutableDictionary * formDatas;
NSMutableDictionary * formHeaders;
NSUInteger readElementCursor;
NSUInteger readOffsetCursor;
NSUInteger readHeaderOffsetCursor;
NSInputStream * currentFileStream;
NSStringEncoding stringEncoding;
} }
[self appendString:kAFMultipartFormCRLF]; @end
[self appendData:body];
@implementation AFMultipartBodyStream
-(id)init {
self = [super init];
fileNames = [[NSMutableArray alloc] init];
fileURLs = [[NSMutableDictionary alloc] init];
fileHeaders = [[NSMutableDictionary alloc] init];
formNames = [[NSMutableArray alloc] init];
formDatas = [[NSMutableDictionary alloc] init];
formHeaders = [[NSMutableDictionary alloc] init];
currentFileStream = NULL;
stringEncoding = NSUTF8StringEncoding;
streamStatus = NSStreamStatusNotOpen;
[self resetCursors];
[self setDelegate:self];
return self;
} }
- (void)appendPartWithFormData:(NSData *)data -(id)initWithStringEncoding:(NSStringEncoding)encoding {
name:(NSString *)name self = [self init];
{ stringEncoding = encoding;
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; return self;
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];
[self appendPartWithHeaders:mutableHeaders body:data];
} }
- (NSMutableDictionary *)fileHeadersWithName:(NSString *)name -(void)resetCursors {
fileName:(NSString *)fileName readElementCursor = 0;
mimeType:(NSString *)mimeType { readOffsetCursor = 0;
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; readHeaderOffsetCursor = 0;
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
return mutableHeaders;
} }
- (void)appendPartWithFileData:(NSData *)data -(void)dealloc {
name:(NSString *)name [fileNames release];
fileName:(NSString *)fileName [fileURLs release];
mimeType:(NSString *)mimeType [fileHeaders release];
{ [formNames release];
[formDatas release];
[self appendPartWithHeaders:[self fileHeadersWithName:name fileName:fileName mimeType:mimeType] body:data]; [formHeaders release];
if (currentFileStream) {
[currentFileStream close];
[currentFileStream release];
currentFileStream = NULL;
}
[super dealloc];
} }
- (void)appendPartWithStreamingURL:(NSURL *)streamingURL -(BOOL)empty {
name:(NSString *)name if (([fileURLs count] + [formDatas count]) == 0)
mimeType:(NSString *)mimeType return YES;
{ else
return NO;
NSString * fileName = [[streamingURL pathComponents] objectAtIndex:([[streamingURL pathComponents] count] - 1)];
[self appendPartWithHeaders:[self fileHeadersWithName:name fileName:fileName mimeType:mimeType] body:nil];
NSInputStream * inputStream = [NSInputStream inputStreamWithURL:streamingURL];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
[inputStream open];
void * dataBuffer = malloc(kAFStreamToStreamBufferSize);
NSInteger bytesRead = [inputStream read:dataBuffer maxLength:kAFStreamToStreamBufferSize];
while (bytesRead > 0) {
NSData * tempData = [NSData dataWithBytesNoCopy:dataBuffer length:bytesRead freeWhenDone:NO];
[self appendData:tempData];
bytesRead = [inputStream read:dataBuffer maxLength:kAFStreamToStreamBufferSize];
} }
[inputStream close];
free(dataBuffer);
}
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL -(BOOL)addFileFromURL:(NSURL *)fileURL name:(NSString *)name error:(NSError **)error {
name:(NSString *)name assert([self streamStatus] == NSStreamStatusNotOpen);
error:(NSError **)error
{
if (![fileURL isFileURL]) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setValue:fileURL forKey:NSURLErrorFailingURLErrorKey]; [userInfo setValue:fileURL forKey:NSURLErrorFailingURLErrorKey];
if (![fileURL isFileURL]) {
[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;
}
if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
[userInfo setValue:NSLocalizedString(@"File URL not reachable.", nil) forKey:NSLocalizedFailureReasonErrorKey];
if (error != NULL) {
*error = [[[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadURL userInfo:userInfo] autorelease];
}
return NO; return NO;
} }
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL]; [fileNames addObject:name];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; [fileURLs setObject:fileURL forKey:name];
[self generateHeaders];
NSURLResponse *response = nil; return YES;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error]; }
if (data && response) { -(void)addFormData:(NSData *)data name:(NSString *)name {
[self appendPartWithFileData:data name:name fileName:[response suggestedFilename] mimeType:[response MIMEType]]; assert([self streamStatus] == NSStreamStatusNotOpen);
[formNames addObject:name];
[formDatas setObject:data forKey:name];
[self generateHeaders];
}
-(void)generateHeaders {
[formHeaders removeAllObjects];
for (NSString * formName in formNames) {
[formHeaders setObject:[self headersDataForForm:formName] forKey:formName];
}
[fileHeaders removeAllObjects];
for (NSString * fileName in fileNames) {
[fileHeaders setObject:[self headersDataForFile:fileName] forKey:fileName];
}
}
-(NSString *)stringForHeaders:(NSDictionary *)headers {
NSMutableString * headerString = [NSMutableString string];
for (NSString *field in [headers allKeys]) {
[headerString appendString:[NSString stringWithFormat:@"%@: %@%@", field, [headers valueForKey:field], kAFMultipartFormCRLF]];
}
return [NSString stringWithString:headerString];
}
-(NSData *)headersDataForDict:(NSDictionary *)headersDict {
NSMutableString * headerString = [NSMutableString string];
if ([formHeaders count] == 0 && [fileHeaders count] == 0) {
[headerString appendString:AFMultipartFormInitialBoundary()];
} else {
[headerString appendString:AFMultipartFormEncapsulationBoundary()];
}
[headerString appendString:[self stringForHeaders:headersDict]];
[headerString appendString:kAFMultipartFormCRLF];
return [headerString dataUsingEncoding:stringEncoding];
}
-(NSData *)headersDataForFile:(NSString *)name {
NSURL * fileURL = [fileURLs objectForKey:name];
NSString * fileName = [[fileURL pathComponents] objectAtIndex:([[fileURL pathComponents] count] - 1)];
NSMutableDictionary * mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)[fileURL pathExtension], NULL);
NSString * MIMEType = [(NSString*) UTTypeCopyPreferredTagWithClass (UTI, kUTTagClassMIMEType) autorelease];
CFRelease(UTI);
[mutableHeaders setValue:MIMEType forKey:@"Content-Type"];
return [self headersDataForDict:mutableHeaders];
}
-(NSData *)headersDataForForm:(NSString *)name {
return [self headersDataForDict:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"]];
}
-(NSInteger)readData:(NSData *)data intoBuffer:(uint8_t *)buffer maxLength:(NSUInteger)len offsetCursor:(NSUInteger*)offsetCursorPtr {
NSUInteger bytesAvailable = [data length] - *offsetCursorPtr;
if (len > bytesAvailable) {
[data getBytes:buffer range:NSMakeRange(*offsetCursorPtr, bytesAvailable)];
*offsetCursorPtr += bytesAvailable;
return bytesAvailable;
} else {
[data getBytes:buffer range:NSMakeRange(*offsetCursorPtr, len)];
*offsetCursorPtr += len;
return len;
}
}
-(void)nextElement {
readOffsetCursor = 0;
readHeaderOffsetCursor = 0;
readElementCursor += 1;
if (currentFileStream) {
[currentFileStream close];
[currentFileStream release];
currentFileStream = NULL;
}
}
-(NSUInteger)contentLength {
NSUInteger total = 0;
for (NSString * formName in formNames) {
total += [[formHeaders objectForKey:formName] length];
total += [[formDatas objectForKey:formName] length];
}
for (NSString * fileName in fileNames) {
NSError * error;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[[fileURLs objectForKey:fileName] path] error:&error];
total += [[fileHeaders objectForKey:fileName] length];
total += [[fileAttributes objectForKey:NSFileSize] longValue];
}
total += [[self finalBoundaryData] length];
return total;
}
-(NSUInteger)totalElements {
return [formDatas count] + [fileURLs count];
}
-(NSData *)finalBoundaryData {
return [AFMultipartFormFinalBoundary() dataUsingEncoding:stringEncoding];
}
#pragma mark - NSStream subclass overrides
-(void)open {
streamStatus = NSStreamStatusOpen;
}
-(void)close {
streamStatus = NSStreamStatusClosed;
[self resetCursors];
}
- (id <NSStreamDelegate> )delegate {
return delegate;
}
- (void)setDelegate:(id<NSStreamDelegate>)aDelegate {
if (aDelegate == nil) {
delegate = self;
}
else {
delegate = aDelegate;
}
}
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
}
- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
}
- (id)propertyForKey:(NSString *)key {
return nil;
}
- (BOOL)setProperty:(id)property forKey:(NSString *)key {
return NO;
}
- (NSStreamStatus)streamStatus {
return streamStatus;
}
- (NSError *)streamError {
return nil;
}
#pragma mark - NSInputStream subclass overrides
-(NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
assert ([self streamStatus] == NSStreamStatusOpen);
NSUInteger bytesRead = 0;
NSUInteger readFileCursor = (readElementCursor - [formNames count]);
if (readElementCursor < [formNames count]) {
//reading from formDatas
NSString * currentFormName = [formNames objectAtIndex:readElementCursor];
NSData * currentData = [formDatas objectForKey:currentFormName];
NSData * headersData = [formHeaders objectForKey:currentFormName];
if (readHeaderOffsetCursor < [headersData length]) {
bytesRead = [self readData:headersData intoBuffer:buffer maxLength:len offsetCursor:&readHeaderOffsetCursor];
} else {
bytesRead = [self readData:currentData intoBuffer:buffer maxLength:len offsetCursor:&readOffsetCursor];
}
if (readOffsetCursor == [currentData length]) {
[self nextElement];
}
}
else if (readElementCursor >= [formNames count] && readFileCursor < [fileNames count]) {
//reading from files
NSString * currentFileName = [fileNames objectAtIndex:readFileCursor];
NSURL * currentFileURL = [fileURLs objectForKey:currentFileName];
NSData * headersData = [fileHeaders objectForKey:currentFileName];
assert(headersData != NULL);
if (readHeaderOffsetCursor < [headersData length]) {
bytesRead = [self readData:headersData intoBuffer:buffer maxLength:len offsetCursor:&readHeaderOffsetCursor];
} else {
if (!currentFileStream) {
currentFileStream = [[NSInputStream inputStreamWithURL:currentFileURL] retain];
currentFileStream.delegate = self;
[currentFileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[currentFileStream open];
}
bytesRead = [currentFileStream read:buffer maxLength:len];
if (![currentFileStream hasBytesAvailable] || bytesRead == 0) {
[self nextElement];
}
}
}
else {
//add final boundary
bytesRead = [self readData:[self finalBoundaryData] intoBuffer:buffer maxLength:len offsetCursor:&readOffsetCursor];
if (readOffsetCursor == [[self finalBoundaryData] length]) {
[self nextElement];
}
}
if (bytesRead < len && readElementCursor <= [self totalElements]) {
//recurse to fill out the buffer, this is critical if the above read operations produce a zero length buffer for some reason (empty form data, end of file) because returning zero from this method will result in the stream being closed.
bytesRead += [self read:buffer+bytesRead maxLength:len-bytesRead];
}
//doesn't seem to make a diff if we do the callbacks or not for the HTTP request use anyway,
//and it sometimes crashes if the callback reciever or context have been released.
// if (CFReadStreamGetStatus((CFReadStreamRef)self) == kCFStreamStatusOpen) {
// double delayInSeconds = 0;
// dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
// dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// if (copiedCallback && (requestedEvents & kCFStreamEventHasBytesAvailable)) {
// NSLog(@"callback kCFStreamEventHasBytesAvailable... bytes read: %d", bytesRead);
// copiedCallback((CFReadStreamRef)self, kCFStreamEventHasBytesAvailable, &copiedContext);
// }
// });
// }
assert(bytesRead > 0); //really should never return zero from this call
return bytesRead;
}
-(BOOL)hasBytesAvailable {
if ([self totalElements] == 0) {
return NO;
} else if (readElementCursor < ([self totalElements] + 1)) {
return YES; return YES;
} else { } else {
return NO; return NO;
} }
} }
- (void)appendString:(NSString *)string { -(BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
[self appendData:[string dataUsingEncoding:self.stringEncoding]]; return NO;
} }
- (void)appendData:(NSData *)data { #pragma mark - Undocumented CFReadStream bridged methods
if ([data length] == 0) {
return; - (void)_scheduleInCFRunLoop:(CFRunLoopRef)aRunLoop forMode:(CFStringRef)aMode {
} }
if ([self.outputStream hasSpaceAvailable]) { - (BOOL)_setCFClientFlags:(CFOptionFlags)inFlags
const uint8_t *dataBuffer = (uint8_t *) [data bytes]; callback:(CFReadStreamClientCallBack)inCallback
[self.outputStream write:&dataBuffer[0] maxLength:[data length]]; context:(CFStreamClientContext *)inContext {
} else {
NSLog(@"Failed to append to outputStream!"); if (inCallback != NULL) {
requestedEvents = inFlags;
copiedCallback = inCallback;
memcpy(&copiedContext, inContext, sizeof(CFStreamClientContext));
if (copiedContext.info && copiedContext.retain) {
copiedContext.retain(copiedContext.info);
} }
copiedCallback((CFReadStreamRef)self, kCFStreamEventHasBytesAvailable, &copiedContext);
} }
else {
requestedEvents = kCFStreamEventNone;
copiedCallback = NULL;
if (copiedContext.info && copiedContext.release) {
copiedContext.release(copiedContext.info);
}
memset(&copiedContext, 0, sizeof(CFStreamClientContext));
}
return YES;
}
- (void)_unscheduleFromCFRunLoop:(CFRunLoopRef)aRunLoop forMode:(CFStringRef)aMode {
}
@end @end

View file

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
50ABD6ED159FC2CE001BE42C /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */; };
F8129C7415910C37009BFE23 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F8129C7215910C37009BFE23 /* AppDelegate.m */; }; F8129C7415910C37009BFE23 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F8129C7215910C37009BFE23 /* AppDelegate.m */; };
F8D0701B14310F4A00653FD3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8E469E213957DF700DB05C8 /* SystemConfiguration.framework */; }; F8D0701B14310F4A00653FD3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8E469E213957DF700DB05C8 /* SystemConfiguration.framework */; };
F8D0701C14310F4F00653FD3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8E469E013957DF100DB05C8 /* Security.framework */; }; F8D0701C14310F4F00653FD3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8E469E013957DF100DB05C8 /* Security.framework */; };
@ -39,6 +40,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
F8129C3815910830009BFE23 /* Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = SOURCE_ROOT; }; F8129C3815910830009BFE23 /* Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = SOURCE_ROOT; };
F8129C7215910C37009BFE23 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; F8129C7215910C37009BFE23 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
F8129C7315910C37009BFE23 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; F8129C7315910C37009BFE23 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; };
@ -95,6 +97,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50ABD6ED159FC2CE001BE42C /* MobileCoreServices.framework in Frameworks */,
F8E469651395739D00DB05C8 /* UIKit.framework in Frameworks */, F8E469651395739D00DB05C8 /* UIKit.framework in Frameworks */,
F8E469671395739D00DB05C8 /* Foundation.framework in Frameworks */, F8E469671395739D00DB05C8 /* Foundation.framework in Frameworks */,
F8E469691395739D00DB05C8 /* CoreGraphics.framework in Frameworks */, F8E469691395739D00DB05C8 /* CoreGraphics.framework in Frameworks */,
@ -142,6 +145,7 @@
F8E469551395739C00DB05C8 = { F8E469551395739C00DB05C8 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */,
F8E469B71395759C00DB05C8 /* Networking Extensions */, F8E469B71395759C00DB05C8 /* Networking Extensions */,
F8E4696A1395739D00DB05C8 /* Classes */, F8E4696A1395739D00DB05C8 /* Classes */,
F8E469ED1395812A00DB05C8 /* Images */, F8E469ED1395812A00DB05C8 /* Images */,