#27 Initial pass at HTTP range requests support

This commit is contained in:
Pierre-Olivier Latour
2014-03-19 20:57:35 -07:00
parent 096b07a201
commit 1e99e91407
5 changed files with 105 additions and 7 deletions

View File

@@ -365,6 +365,10 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
return [GCDWebServerFileResponse responseWithFile:path];
}
- (GCDWebServerResponse*)_responseWithPartialContentsOfFile:(NSString*)path byteRange:(NSRange)range {
return [GCDWebServerFileResponse responseWithFile:path byteRange:range];
}
- (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path {
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:path];
if (enumerator == nil) {
@@ -423,11 +427,17 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
}
response = [server _responseWithContentsOfDirectory:filePath];
} else {
response = [server _responseWithContentsOfFile:filePath];
NSRange range = request.byteRange;
if ((range.location != NSNotFound) || (range.length > 0)) {
response = [server _responseWithPartialContentsOfFile:filePath byteRange:range];
} else {
response = [server _responseWithContentsOfFile:filePath];
}
}
}
if (response) {
response.cacheControlMaxAge = age;
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
} else {
response = [GCDWebServerResponse responseWithStatusCode:404];
}

View File

@@ -35,6 +35,7 @@
@property(nonatomic, readonly) NSDictionary* query; // May be nil
@property(nonatomic, readonly) NSString* contentType; // Automatically parsed from headers (nil if request has no body)
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers
@property(nonatomic, readonly) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -bytes] from end)
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
- (BOOL)hasBody; // Convenience method
@end

View File

@@ -46,6 +46,7 @@ enum {
NSDictionary* _query;
NSString* _type;
NSUInteger _length;
NSRange _range;
}
@end
@@ -133,7 +134,7 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
@implementation GCDWebServerRequest : NSObject
@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length;
@synthesize method=_method, URL=_url, headers=_headers, path=_path, query=_query, contentType=_type, contentLength=_length, byteRange=_range;
- (id)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
if ((self = [super init])) {
@@ -151,10 +152,39 @@ static NSStringEncoding _StringEncodingFromCharset(NSString* charset) {
return nil;
}
_length = length;
if ((_length > 0) && (_type == nil)) {
_type = [kGCDWebServerDefaultMimeType copy];
}
_range = NSMakeRange(NSNotFound, 0);
NSString* rangeHeader = [[_headers objectForKey:@"Range"] lowercaseString];
if (rangeHeader) {
if ([rangeHeader hasPrefix:@"bytes="]) {
NSArray* components = [[rangeHeader substringFromIndex:6] componentsSeparatedByString:@","];
if (components.count == 1) {
components = [[components firstObject] componentsSeparatedByString:@"-"];
if (components.count == 2) {
NSString* startString = [components objectAtIndex:0];
NSInteger startValue = [startString integerValue];
NSString* endString = [components objectAtIndex:1];
NSInteger endValue = [endString integerValue];
if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999"
_range.location = startValue;
_range.length = endValue - startValue + 1;
} else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-"
_range.location = startValue;
_range.length = NSUIntegerMax;
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
_range.location = NSNotFound;
_range.length = endValue;
}
}
}
}
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
}
}
}
return self;
}

View File

@@ -70,6 +70,10 @@
@interface GCDWebServerFileResponse : GCDWebServerResponse
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range;
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
- (id)initWithFile:(NSString*)path;
- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range; // Pass [NSNotFound, 0] to disable byte range entirely, [offset, length] to enable byte range from beginning of file or [NSNotFound, -bytes] from end of file
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
@end

View File

@@ -49,6 +49,8 @@
@interface GCDWebServerFileResponse () {
@private
NSString* _path;
NSUInteger _offset;
NSUInteger _size;
int _file;
}
@end
@@ -251,24 +253,63 @@
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path isAttachment:attachment]);
}
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range]);
}
+ (GCDWebServerFileResponse*)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
return ARC_AUTORELEASE([[[self class] alloc] initWithFile:path byteRange:range isAttachment:attachment]);
}
- (id)initWithFile:(NSString*)path {
return [self initWithFile:path isAttachment:NO];
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
}
- (id)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
}
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range {
return [self initWithFile:path byteRange:range isAttachment:NO];
}
- (id)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment {
struct stat info;
if (lstat([path fileSystemRepresentation], &info) || !(info.st_mode & S_IFREG)) {
DNOT_REACHED();
ARC_RELEASE(self);
return nil;
}
if ((range.location != NSNotFound) || (range.length > 0)) {
if (range.location != NSNotFound) {
range.location = MIN(range.location, info.st_size);
range.length = MIN(range.length, info.st_size - range.location);
} else {
range.length = MIN(range.length, info.st_size);
range.location = info.st_size - range.length;
}
if (range.length == 0) {
ARC_RELEASE(self);
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
}
}
NSString* type = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
if (type == nil) {
type = kGCDWebServerDefaultMimeType;
}
if ((self = [super initWithContentType:type contentLength:(NSUInteger)info.st_size])) {
if ((self = [super initWithContentType:type contentLength:(range.location != NSNotFound ? range.length : info.st_size)])) {
_path = [path copy];
if (range.location != NSNotFound) {
_offset = range.location;
_size = range.length;
[self setStatusCode:206];
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
} else {
_offset = 0;
_size = info.st_size;
}
if (attachment) { // TODO: Use http://tools.ietf.org/html/rfc5987 to encode file names with special characters instead of using lossy conversion to ISO 8859-1
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
@@ -293,12 +334,24 @@
- (BOOL)open {
DCHECK(_file <= 0);
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
return (_file > 0 ? YES : NO);
if (_file <= 0) {
return NO;
}
if (lseek(_file, _offset, SEEK_SET) != _offset) {
close(_file);
_file = 0;
return NO;
}
return YES;
}
- (NSInteger)read:(void*)buffer maxLength:(NSUInteger)length {
DCHECK(_file > 0);
return read(_file, buffer, length);
ssize_t result = read(_file, buffer, MIN(length, _size));
if (result > 0) {
_size -= result;
}
return result;
}
- (BOOL)close {