123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- #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
- // Log levels : off, error, warn, info, verbose
- // Other flags: trace
- static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | 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 !OS_OBJECT_USE_OBJC
- 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 !OS_OBJECT_USE_OBJC
- 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 !OS_OBJECT_USE_OBJC
- if (readQueue) dispatch_release(readQueue);
- #endif
-
- if (readBuffer)
- free(readBuffer);
- }
- @end
|