| #import "HTTPAsyncFileResponse.h" |
| #import "HTTPConnection.h" |
| #import "HTTPLogging.h" |
| |
| #import <unistd.h> |
| #import <fcntl.h> |
| |
| #if ! __has_feature(objc_arc) |
| #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). |
| #endif |
| |
| /** |
| * Does ARC support support GCD objects? |
| * It does if the minimum deployment target is iOS 6+ or Mac OS X 8+ |
| **/ |
| #if TARGET_OS_IPHONE |
| |
| // Compiling for iOS |
| |
| #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 0 |
| #else // iOS 5.X or earlier |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 1 |
| #endif |
| |
| #else |
| |
| // Compiling for Mac OS X |
| |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 0 |
| #else |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier |
| #endif |
| |
| #endif |
| |
| // Log levels : off, error, warn, info, verbose |
| // Other flags: trace |
| static const DDLogLevel httpLogLevel = DDLogLevelWarning; // | HTTP_LOG_FLAG_TRACE; |
| |
| #define NULL_FD -1 |
| |
| /** |
| * Architecure overview: |
| * |
| * HTTPConnection will invoke our readDataOfLength: method to fetch data. |
| * We will return nil, and then proceed to read the data via our readSource on our readQueue. |
| * Once the requested amount of data has been read, we then pause our readSource, |
| * and inform the connection of the available data. |
| * |
| * While our read is in progress, we don't have to worry about the connection calling any other methods, |
| * except the connectionDidClose method, which would be invoked if the remote end closed the socket connection. |
| * To safely handle this, we do a synchronous dispatch on the readQueue, |
| * and nilify the connection as well as cancel our readSource. |
| * |
| * In order to minimize resource consumption during a HEAD request, |
| * we don't open the file until we have to (until the connection starts requesting data). |
| **/ |
| |
| @implementation HTTPAsyncFileResponse |
| |
| - (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent |
| { |
| if ((self = [super init])) |
| { |
| HTTPLogTrace(); |
| |
| connection = parent; // Parents retain children, children do NOT retain parents |
| |
| fileFD = NULL_FD; |
| filePath = [fpath copy]; |
| if (filePath == nil) |
| { |
| HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE); |
| |
| return nil; |
| } |
| |
| NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL]; |
| if (fileAttributes == nil) |
| { |
| HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath); |
| |
| return nil; |
| } |
| |
| fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue]; |
| fileOffset = 0; |
| |
| aborted = NO; |
| |
| // We don't bother opening the file here. |
| // If this is a HEAD request we only need to know the fileLength. |
| } |
| return self; |
| } |
| |
| - (void)abort |
| { |
| HTTPLogTrace(); |
| |
| [connection responseDidAbort:self]; |
| aborted = YES; |
| } |
| |
| - (void)processReadBuffer |
| { |
| // This method is here to allow superclasses to perform post-processing of the data. |
| // For an example, see the HTTPDynamicFileResponse class. |
| // |
| // At this point, the readBuffer has readBufferOffset bytes available. |
| // This method is in charge of updating the readBufferOffset. |
| // Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...) |
| |
| // Copy the data out of the temporary readBuffer. |
| data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset]; |
| |
| // Reset the read buffer. |
| readBufferOffset = 0; |
| |
| // Notify the connection that we have data available for it. |
| [connection responseHasAvailableData:self]; |
| } |
| |
| - (void)pauseReadSource |
| { |
| if (!readSourceSuspended) |
| { |
| HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self); |
| |
| readSourceSuspended = YES; |
| dispatch_suspend(readSource); |
| } |
| } |
| |
| - (void)resumeReadSource |
| { |
| if (readSourceSuspended) |
| { |
| HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self); |
| |
| readSourceSuspended = NO; |
| dispatch_resume(readSource); |
| } |
| } |
| |
| - (void)cancelReadSource |
| { |
| HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self); |
| |
| dispatch_source_cancel(readSource); |
| |
| // Cancelling a dispatch source doesn't |
| // invoke the cancel handler if the dispatch source is paused. |
| |
| if (readSourceSuspended) |
| { |
| readSourceSuspended = NO; |
| dispatch_resume(readSource); |
| } |
| } |
| |
| - (BOOL)openFileAndSetupReadSource |
| { |
| HTTPLogTrace(); |
| |
| fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK)); |
| if (fileFD == NULL_FD) |
| { |
| HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath); |
| |
| return NO; |
| } |
| |
| HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath); |
| |
| readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL); |
| readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue); |
| |
| |
| dispatch_source_set_event_handler(readSource, ^{ |
| |
| HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD); |
| |
| // Determine how much data we should read. |
| // |
| // It is OK if we ask to read more bytes than exist in the file. |
| // It is NOT OK to over-allocate the buffer. |
| |
| unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource); |
| |
| UInt64 _bytesLeftInFile = fileLength - readOffset; |
| |
| NSUInteger bytesAvailableOnFD; |
| NSUInteger bytesLeftInFile; |
| |
| bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD; |
| bytesLeftInFile = (_bytesLeftInFile > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile; |
| |
| NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset; |
| |
| NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile); |
| |
| NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft); |
| |
| // Make sure buffer is big enough for read request. |
| // Do not over-allocate. |
| |
| if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset)) |
| { |
| readBufferSize = bytesToRead; |
| readBuffer = reallocf(readBuffer, (size_t)bytesToRead); |
| |
| if (readBuffer == NULL) |
| { |
| HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self); |
| |
| [self pauseReadSource]; |
| [self abort]; |
| |
| return; |
| } |
| } |
| |
| // Perform the read |
| |
| HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead); |
| |
| ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead); |
| |
| // Check the results |
| if (result < 0) |
| { |
| HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath); |
| |
| [self pauseReadSource]; |
| [self abort]; |
| } |
| else if (result == 0) |
| { |
| HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath); |
| |
| [self pauseReadSource]; |
| [self abort]; |
| } |
| else // (result > 0) |
| { |
| HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result); |
| |
| readOffset += result; |
| readBufferOffset += result; |
| |
| [self pauseReadSource]; |
| [self processReadBuffer]; |
| } |
| |
| }); |
| |
| int theFileFD = fileFD; |
| #if NEEDS_DISPATCH_RETAIN_RELEASE |
| dispatch_source_t theReadSource = readSource; |
| #endif |
| |
| dispatch_source_set_cancel_handler(readSource, ^{ |
| |
| // Do not access self from within this block in any way, shape or form. |
| // |
| // Note: You access self if you reference an iVar. |
| |
| HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD); |
| |
| #if NEEDS_DISPATCH_RETAIN_RELEASE |
| dispatch_release(theReadSource); |
| #endif |
| close(theFileFD); |
| }); |
| |
| readSourceSuspended = YES; |
| |
| return YES; |
| } |
| |
| - (BOOL)openFileIfNeeded |
| { |
| if (aborted) |
| { |
| // The file operation has been aborted. |
| // This could be because we failed to open the file, |
| // or the reading process failed. |
| return NO; |
| } |
| |
| if (fileFD != NULL_FD) |
| { |
| // File has already been opened. |
| return YES; |
| } |
| |
| return [self openFileAndSetupReadSource]; |
| } |
| |
| - (UInt64)contentLength |
| { |
| HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength); |
| |
| return fileLength; |
| } |
| |
| - (UInt64)offset |
| { |
| HTTPLogTrace(); |
| |
| return fileOffset; |
| } |
| |
| - (void)setOffset:(UInt64)offset |
| { |
| HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset); |
| |
| if (![self openFileIfNeeded]) |
| { |
| // File opening failed, |
| // or response has been aborted due to another error. |
| return; |
| } |
| |
| fileOffset = offset; |
| readOffset = offset; |
| |
| off_t result = lseek(fileFD, (off_t)offset, SEEK_SET); |
| if (result == -1) |
| { |
| HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath); |
| |
| [self abort]; |
| } |
| } |
| |
| - (NSData *)readDataOfLength:(NSUInteger)length |
| { |
| HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length); |
| |
| if (data) |
| { |
| NSUInteger dataLength = [data length]; |
| |
| HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength); |
| |
| fileOffset += dataLength; |
| |
| NSData *result = data; |
| data = nil; |
| |
| return result; |
| } |
| else |
| { |
| if (![self openFileIfNeeded]) |
| { |
| // File opening failed, |
| // or response has been aborted due to another error. |
| return nil; |
| } |
| |
| dispatch_sync(readQueue, ^{ |
| |
| NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed."); |
| |
| readRequestLength = length; |
| [self resumeReadSource]; |
| }); |
| |
| return nil; |
| } |
| } |
| |
| - (BOOL)isDone |
| { |
| BOOL result = (fileOffset == fileLength); |
| |
| HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); |
| |
| return result; |
| } |
| |
| - (NSString *)filePath |
| { |
| return filePath; |
| } |
| |
| - (BOOL)isAsynchronous |
| { |
| HTTPLogTrace(); |
| |
| return YES; |
| } |
| |
| - (void)connectionDidClose |
| { |
| HTTPLogTrace(); |
| |
| if (fileFD != NULL_FD) |
| { |
| dispatch_sync(readQueue, ^{ |
| |
| // Prevent any further calls to the connection |
| connection = nil; |
| |
| // Cancel the readSource. |
| // We do this here because the readSource's eventBlock has retained self. |
| // In other words, if we don't cancel the readSource, we will never get deallocated. |
| |
| [self cancelReadSource]; |
| }); |
| } |
| } |
| |
| - (void)dealloc |
| { |
| HTTPLogTrace(); |
| |
| #if NEEDS_DISPATCH_RETAIN_RELEASE |
| if (readQueue) dispatch_release(readQueue); |
| #endif |
| |
| if (readBuffer) |
| free(readBuffer); |
| } |
| |
| @end |