support content-range header (#59)
Fixes https://github.com/dart-lang/shelf_static/issues/3
diff --git a/lib/src/static_handler.dart b/lib/src/static_handler.dart
index 11220aa..533fe93 100644
--- a/lib/src/static_handler.dart
+++ b/lib/src/static_handler.dart
@@ -190,12 +190,64 @@
final headers = {
HttpHeaders.contentLengthHeader: stat.size.toString(),
- HttpHeaders.lastModifiedHeader: formatHttpDate(stat.modified)
+ HttpHeaders.lastModifiedHeader: formatHttpDate(stat.modified),
+ HttpHeaders.acceptRangesHeader: 'bytes',
};
final contentType = await getContentType();
+ final length = await file.length();
+ final range = request.headers[HttpHeaders.rangeHeader];
if (contentType != null) headers[HttpHeaders.contentTypeHeader] = contentType;
+ if (range != null) {
+ // We only support one range, where the standard support several.
+ final matches = RegExp(r'^bytes=(\d*)\-(\d*)$').firstMatch(range);
+ // If the range header have the right format, handle it.
+ if (matches != null && (matches[1]!.isNotEmpty || matches[2]!.isNotEmpty)) {
+ // Serve sub-range.
+ int start; // First byte position - inclusive.
+ int end; // Last byte position - inclusive.
+ if (matches[1]!.isEmpty) {
+ start = length - int.parse(matches[2]!);
+ if (start < 0) start = 0;
+ end = length - 1;
+ } else {
+ start = int.parse(matches[1]!);
+ end = matches[2]!.isEmpty ? length - 1 : int.parse(matches[2]!);
+ }
+ // If the range is syntactically invalid the Range header
+ // MUST be ignored (RFC 2616 section 14.35.1).
+ if (start <= end) {
+ if (end >= length) {
+ end = length - 1;
+ }
+ if (start >= length) {
+ return Response(
+ HttpStatus.requestedRangeNotSatisfiable,
+ headers: headers,
+ );
+ }
+ // Override Content-Length with the actual bytes sent.
+ headers[HttpHeaders.contentLengthHeader] = (end - start + 1).toString();
+
+ // Set 'Partial Content' status code.
+ headers[HttpHeaders.contentRangeHeader] = 'bytes $start-$end/$length';
+ // Pipe the 'range' of the file.
+ if (request.method == 'HEAD') {
+ return Response(
+ HttpStatus.partialContent,
+ headers: headers,
+ );
+ } else {
+ return Response(
+ HttpStatus.partialContent,
+ body: file.openRead(start, end + 1),
+ headers: headers,
+ );
+ }
+ }
+ }
+ }
return Response.ok(
request.method == 'HEAD' ? null : file.openRead(),
headers: headers,
diff --git a/test/create_file_handler_test.dart b/test/create_file_handler_test.dart
index 500edbf..8544bb8 100644
--- a/test/create_file_handler_test.dart
+++ b/test/create_file_handler_test.dart
@@ -71,6 +71,62 @@
});
});
+ group('the content range header', () {
+ test('is bytes from 0 to 4', () async {
+ final handler = createFileHandler(p.join(d.sandbox, 'file.txt'));
+ final response = await makeRequest(
+ handler,
+ '/file.txt',
+ headers: {'range': 'bytes=0-4'},
+ );
+ expect(response.statusCode, equals(HttpStatus.partialContent));
+ expect(
+ response.headers[HttpHeaders.acceptRangesHeader],
+ 'bytes',
+ );
+ expect(
+ response.headers[HttpHeaders.contentRangeHeader],
+ 'bytes 0-4/8',
+ );
+ });
+ test('at the end of has overflow from 0 to 9', () async {
+ final handler = createFileHandler(p.join(d.sandbox, 'file.txt'));
+ final response = await makeRequest(
+ handler,
+ '/file.txt',
+ headers: {'range': 'bytes=0-9'},
+ );
+ expect(
+ response.statusCode,
+ equals(HttpStatus.partialContent),
+ );
+ expect(
+ response.headers[HttpHeaders.acceptRangesHeader],
+ 'bytes',
+ );
+ expect(
+ response.headers[HttpHeaders.contentRangeHeader],
+ 'bytes 0-7/8',
+ );
+ });
+ test('at the start of has overflow from 8 to 9', () async {
+ final handler = createFileHandler(p.join(d.sandbox, 'file.txt'));
+ final response = await makeRequest(
+ handler,
+ '/file.txt',
+ headers: {'range': 'bytes=8-9'},
+ );
+ expect(
+ response.headers[HttpHeaders.acceptRangesHeader],
+ 'bytes',
+ );
+ expect(
+ response.statusCode,
+ equals(HttpStatus.requestedRangeNotSatisfiable),
+ );
+ });
+ });
+
group('throws an ArgumentError for', () {
test("a file that doesn't exist", () {
expect(() => createFileHandler(p.join(d.sandbox, 'nothing.txt')),