mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-02-11 00:00:07 +08:00
#27 Initial pass at HTTP range requests support
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user