Przeglądaj źródła

#27 Initial pass at HTTP range requests support

Pierre-Olivier Latour 11 lat temu
rodzic
commit
1e99e91407

+ 11 - 1
CGDWebServer/GCDWebServer.m

@@ -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];
       }

+ 1 - 0
CGDWebServer/GCDWebServerRequest.h

@@ -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

+ 32 - 2
CGDWebServer/GCDWebServerRequest.m

@@ -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;
 }

+ 4 - 0
CGDWebServer/GCDWebServerResponse.h

@@ -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

+ 57 - 4
CGDWebServer/GCDWebServerResponse.m

@@ -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 {