v0.1.4 - createStaticHandler learned defaultDocument
Resolves kevmoo/shelf_static.dart#1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e16584..6165e47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.4
+
+* Added named (optional) `defaultDocument` argument to `createStaticHandler`.
+
## 0.1.3
* `createStaticHandler` added `serveFilesOutsidePath` optional parameter.
diff --git a/example/example_server.dart b/example/example_server.dart
index 3331b64..3c050eb 100644
--- a/example/example_server.dart
+++ b/example/example_server.dart
@@ -11,7 +11,8 @@
'root of the project.');
}
var handler = const shelf.Pipeline().addMiddleware(shelf.logRequests())
- .addHandler(createStaticHandler('example/files'));
+ .addHandler(createStaticHandler('example/files',
+ defaultDocument: 'index.html'));
io.serve(handler, 'localhost', 8080).then((server) {
print('Serving at http://${server.address.host}:${server.port}');
diff --git a/lib/shelf_static.dart b/lib/shelf_static.dart
index bf42a41..ebdea75 100644
--- a/lib/shelf_static.dart
+++ b/lib/shelf_static.dart
@@ -8,7 +8,6 @@
import 'package:shelf/shelf.dart';
// directory listing
-// default document
// hidden files
/// Creates a Shelf [Handler] that serves files from the provided
@@ -17,8 +16,12 @@
/// Accessing a path containing symbolic links will succeed only if the resolved
/// path is within [fileSystemPath]. To allow access to paths outside of
/// [fileSystemPath], set [serveFilesOutsidePath] to `true`.
+///
+/// When a existing directory is requested and a [defaultDocument] is specified
+/// the directory is checked for a file with that name. If it exists, it is
+/// served.
Handler createStaticHandler(String fileSystemPath,
- {bool serveFilesOutsidePath: false}) {
+ {bool serveFilesOutsidePath: false, String defaultDocument}) {
var rootDir = new Directory(fileSystemPath);
if (!rootDir.existsSync()) {
throw new ArgumentError('A directory corresponding to fileSystemPath '
@@ -27,6 +30,12 @@
fileSystemPath = rootDir.resolveSymbolicLinksSync();
+ if (defaultDocument != null) {
+ if (defaultDocument != p.basename(defaultDocument)) {
+ throw new ArgumentError('defaultDocument must be a file name.');
+ }
+ }
+
return (Request request) {
// TODO: expand these checks and/or follow updates to Uri class to be more
// strict. https://code.google.com/p/dart/issues/detail?id=16081
@@ -37,9 +46,18 @@
var segs = [fileSystemPath]..addAll(request.url.pathSegments);
var requestedPath = p.joinAll(segs);
- var file = new File(requestedPath);
- if (!file.existsSync()) {
+ var fileType = FileSystemEntity.typeSync(requestedPath, followLinks: true);
+
+ File file = null;
+
+ if (fileType == FileSystemEntityType.FILE) {
+ file = new File(requestedPath);
+ } else if (fileType == FileSystemEntityType.DIRECTORY) {
+ file = _tryDefaultFile(requestedPath, defaultDocument);
+ }
+
+ if (file == null) {
return new Response.notFound('Not Found');
}
@@ -52,6 +70,19 @@
}
}
+ if (fileType == FileSystemEntityType.DIRECTORY &&
+ !request.url.path.endsWith('/')) {
+ // when serving the default document for a directory, if the requested
+ // path doesn't end with '/', redirect to the path with a trailing '/'
+ var uri = request.requestedUri;
+ assert(!uri.path.endsWith('/'));
+ var location = new Uri(scheme: uri.scheme, userInfo: uri.userInfo,
+ host: uri.host, port: uri.port, path: uri.path + '/',
+ query: uri.query);
+
+ return new Response.movedPermanently(location.toString());
+ }
+
var fileStat = file.statSync();
var ifModifiedSince = request.ifModifiedSince;
@@ -75,6 +106,20 @@
};
}
+File _tryDefaultFile(String dirPath, String defaultFile) {
+ if (defaultFile == null) return null;
+
+ var filePath = p.join(dirPath, defaultFile);
+
+ var file = new File(filePath);
+
+ if (file.existsSync()) {
+ return file;
+ }
+
+ return null;
+}
+
/// Use [createStaticHandler] instead.
@deprecated
Handler getHandler(String fileSystemPath) =>
diff --git a/pubspec.yaml b/pubspec.yaml
index ef404f8..ddb208e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: shelf_static
-version: 0.1.3
+version: 0.1.4
author: Kevin Moore <github@j832.com>
description: Static file server support for Shelf
homepage: https://github.com/kevmoo/shelf_static.dart
diff --git a/test/default_document_test.dart b/test/default_document_test.dart
new file mode 100644
index 0000000..ada30c5
--- /dev/null
+++ b/test/default_document_test.dart
@@ -0,0 +1,145 @@
+library shelf_static.default_document_test;
+
+import 'dart:io';
+//import 'package:http_parser/http_parser.dart';
+//import 'package:path/path.dart' as p;
+import 'package:scheduled_test/descriptor.dart' as d;
+import 'package:scheduled_test/scheduled_test.dart';
+
+import 'package:shelf_static/shelf_static.dart';
+import 'test_util.dart';
+
+void main() {
+ setUp(() {
+ var tempDir;
+ schedule(() {
+ return Directory.systemTemp.createTemp('shelf_static-test-').then((dir) {
+ tempDir = dir;
+ d.defaultRoot = tempDir.path;
+ });
+ });
+
+ d.file('index.html', '<html></html>').create();
+ d.file('root.txt', 'root txt').create();
+ d.dir('files', [
+ d.file('index.html', '<html><body>files</body></html>'),
+ d.file('with space.txt', 'with space content')
+ ]).create();
+
+ currentSchedule.onComplete.schedule(() {
+ d.defaultRoot = null;
+ return tempDir.delete(recursive: true);
+ });
+ });
+
+ group('default document value', () {
+ test('cannot contain slashes', () {
+ var invalidValues = ['file/foo.txt', '/bar.txt', '//bar.txt',
+ '//news/bar.txt', 'foo/../bar.txt'];
+
+ for(var val in invalidValues) {
+ expect(() => createStaticHandler(d.defaultRoot, defaultDocument: val),
+ throwsArgumentError);
+ }
+ });
+ });
+
+ group('no default document specified', () {
+ test('access "/index.html"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot);
+
+ return makeRequest(handler, '/index.html').then((response) {
+ expect(response.statusCode, HttpStatus.OK);
+ expect(response.contentLength, 13);
+ expect(response.readAsString(), completion('<html></html>'));
+ });
+ });
+ });
+
+ test('access "/"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot);
+
+ return makeRequest(handler, '/').then((response) {
+ expect(response.statusCode, HttpStatus.NOT_FOUND);
+ });
+ });
+ });
+
+ test('access "/files"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot);
+
+ return makeRequest(handler, '/files').then((response) {
+ expect(response.statusCode, HttpStatus.NOT_FOUND);
+ });
+ });
+ });
+
+ test('access "/files/" dir', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot);
+
+ return makeRequest(handler, '/files/').then((response) {
+ expect(response.statusCode, HttpStatus.NOT_FOUND);
+ });
+ });
+ });
+ });
+
+ group('default document specified', () {
+ test('access "/index.html"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot,
+ defaultDocument: 'index.html');
+
+ return makeRequest(handler, '/index.html').then((response) {
+ expect(response.statusCode, HttpStatus.OK);
+ expect(response.contentLength, 13);
+ expect(response.readAsString(), completion('<html></html>'));
+ });
+ });
+ });
+
+ test('access "/"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot,
+ defaultDocument: 'index.html');
+
+ return makeRequest(handler, '/').then((response) {
+ expect(response.statusCode, HttpStatus.OK);
+ expect(response.contentLength, 13);
+ expect(response.readAsString(), completion('<html></html>'));
+ });
+ });
+ });
+
+ test('access "/files"', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot,
+ defaultDocument: 'index.html');
+
+ return makeRequest(handler, '/files').then((response) {
+ expect(response.statusCode, HttpStatus.MOVED_PERMANENTLY);
+ expect(response.headers,
+ containsPair(HttpHeaders.LOCATION, 'http://localhost/files/'));
+ });
+ });
+ });
+
+ test('access "/files/" dir', () {
+ schedule(() {
+ var handler = createStaticHandler(d.defaultRoot,
+ defaultDocument: 'index.html');
+
+ return makeRequest(handler, '/files/').then((response) {
+ expect(response.statusCode, HttpStatus.OK);
+ expect(response.contentLength, 31);
+ expect(response.readAsString(),
+ completion('<html><body>files</body></html>'));
+ });
+ });
+ });
+ });
+}
diff --git a/test/harness_console.dart b/test/harness_console.dart
index 1a2d9e8..742ff7a 100644
--- a/test/harness_console.dart
+++ b/test/harness_console.dart
@@ -4,6 +4,7 @@
import 'alternative_root_test.dart' as alternative_root;
import 'basic_file_test.dart' as basic_file;
+import 'default_document_test.dart' as default_document;
import 'get_handler_test.dart' as get_handler;
import 'sample_test.dart' as sample;
import 'symbolic_link_test.dart' as symbolic_link;
@@ -12,6 +13,7 @@
groupSep = ' - ';
group('alternative_root', alternative_root.main);
group('basic_file', basic_file.main);
+ group('default_document', default_document.main);
group('get_handler', get_handler.main);
group('sample', sample.main);
group('symbolic_link', symbolic_link.main);