blob: 4fb0803ff450da222adc3096d9255ce0c6522c9c [file] [log] [blame]
library shelf_static;
import 'dart:io';
import 'package:http_parser/http_parser.dart';
import 'package:mime/mime.dart' as mime;
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
// directory listing
// hidden files
/// Creates a Shelf [Handler] that serves files from the provided
/// [fileSystemPath].
///
/// 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, String defaultDocument}) {
var rootDir = new Directory(fileSystemPath);
if (!rootDir.existsSync()) {
throw new ArgumentError('A directory corresponding to fileSystemPath '
'"$fileSystemPath" could not be found');
}
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
if (request.requestedUri.path.contains(' ')) {
return new Response.forbidden('The requested path is invalid.');
}
var segs = [fileSystemPath]..addAll(request.url.pathSegments);
var requestedPath = p.joinAll(segs);
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 (!serveFilesOutsidePath) {
var resolvedPath = file.resolveSymbolicLinksSync();
// Do not serve a file outside of the original fileSystemPath
if (!p.isWithin(fileSystemPath, resolvedPath)) {
file = null;
}
}
if (file == null) {
return new Response.notFound('Not Found');
}
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;
if (ifModifiedSince != null && !fileStat.changed.isAfter(ifModifiedSince)) {
return new Response.notModified();
}
var headers = <String, String>{
HttpHeaders.CONTENT_LENGTH: fileStat.size.toString(),
HttpHeaders.LAST_MODIFIED: formatHttpDate(fileStat.changed)
};
var contentType = mime.lookupMimeType(requestedPath);
if (contentType != null) {
headers[HttpHeaders.CONTENT_TYPE] = contentType;
}
return new Response.ok(file.openRead(), headers: headers);
};
}
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) =>
createStaticHandler(fileSystemPath);