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')),