2016-11-23 17:22:23 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2017-09-23 21:57:54 +00:00
|
|
|
import 'package:file/file.dart';
|
2017-01-28 16:33:22 +00:00
|
|
|
import 'package:mime/mime.dart';
|
2017-01-25 22:40:41 +00:00
|
|
|
|
2016-11-23 17:22:23 +00:00
|
|
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
|
|
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
|
|
|
|
|
|
|
String _pathify(String path) {
|
|
|
|
var p = path.replaceAll(_straySlashes, '');
|
|
|
|
|
|
|
|
Map<String, String> replace = {};
|
|
|
|
|
|
|
|
for (Match match in _param.allMatches(p)) {
|
|
|
|
if (match[3] != null) replace[match[0]] = ':${match[1]}';
|
|
|
|
}
|
|
|
|
|
|
|
|
replace.forEach((k, v) {
|
|
|
|
p = p.replaceAll(k, v);
|
|
|
|
});
|
|
|
|
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:19:34 +00:00
|
|
|
/// A static server plug-in.
|
2017-09-23 21:57:54 +00:00
|
|
|
class VirtualDirectory {
|
2016-11-23 20:14:05 +00:00
|
|
|
String _prefix;
|
2016-11-23 17:22:23 +00:00
|
|
|
Directory _source;
|
2017-02-27 00:19:34 +00:00
|
|
|
|
|
|
|
/// The directory to serve files from.
|
2016-11-23 17:22:23 +00:00
|
|
|
Directory get source => _source;
|
2017-02-27 00:19:34 +00:00
|
|
|
|
|
|
|
/// An optional callback to run before serving files.
|
2017-09-23 21:57:54 +00:00
|
|
|
final Function(File file, RequestContext req, ResponseContext res) callback;
|
|
|
|
|
|
|
|
final Angel app;
|
|
|
|
final FileSystem fileSystem;
|
2017-02-27 00:19:34 +00:00
|
|
|
|
|
|
|
/// Filenames to be resolved within directories as indices.
|
|
|
|
final Iterable<String> indexFileNames;
|
|
|
|
|
|
|
|
/// An optional public path to map requests to.
|
2016-11-23 17:22:23 +00:00
|
|
|
final String publicPath;
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
VirtualDirectory(this.app, this.fileSystem,
|
2016-11-23 17:22:23 +00:00
|
|
|
{Directory source,
|
|
|
|
this.indexFileNames: const ['index.html'],
|
2017-01-25 22:40:41 +00:00
|
|
|
this.publicPath: '/',
|
2017-09-23 21:57:54 +00:00
|
|
|
this.callback}) {
|
2016-11-23 20:14:05 +00:00
|
|
|
_prefix = publicPath.replaceAll(_straySlashes, '');
|
2016-11-23 17:22:23 +00:00
|
|
|
if (source != null) {
|
|
|
|
_source = source;
|
|
|
|
} else {
|
2017-09-23 21:57:54 +00:00
|
|
|
String dirPath = app.isProduction ? './build/web' : './web';
|
|
|
|
_source = fileSystem.directory(dirPath);
|
2017-06-16 02:05:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Responds to incoming HTTP requests.
|
|
|
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
|
|
|
if (req.method != 'GET') return new Future<bool>.value(true);
|
|
|
|
var path = req.path.replaceAll(_straySlashes, '');
|
2017-06-16 02:05:06 +00:00
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix))
|
|
|
|
return new Future<bool>.value(true);
|
2017-06-16 02:05:06 +00:00
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
return servePath(path, req, res);
|
2017-06-16 02:05:06 +00:00
|
|
|
}
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// A handler that serves the file at the given path, unless the user has requested that path.
|
|
|
|
RequestMiddleware pushState(String path) {
|
|
|
|
var vPath = path.replaceAll(_straySlashes, '');
|
|
|
|
if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
|
2017-06-16 02:05:06 +00:00
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
return (RequestContext req, ResponseContext res) {
|
|
|
|
var path = req.path.replaceAll(_straySlashes, '');
|
|
|
|
if (path == vPath) return new Future<bool>.value(true);
|
|
|
|
return servePath(vPath, req, res);
|
|
|
|
};
|
2017-02-22 23:43:27 +00:00
|
|
|
}
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Writes the file at the given virtual [path] to a response.
|
|
|
|
Future<bool> servePath(
|
|
|
|
String path, RequestContext req, ResponseContext res) async {
|
2017-02-22 23:43:27 +00:00
|
|
|
if (_prefix.isNotEmpty) {
|
2017-06-20 19:57:03 +00:00
|
|
|
// Only replace the *first* incidence
|
|
|
|
// Resolve: https://github.com/angel-dart/angel/issues/41
|
|
|
|
path = path.replaceFirst(new RegExp('^' + _pathify(_prefix)), '');
|
2017-02-22 23:43:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (path.isEmpty) path = '.';
|
2017-06-20 19:57:03 +00:00
|
|
|
path = path.replaceAll(_straySlashes, '');
|
2017-02-22 23:43:27 +00:00
|
|
|
|
|
|
|
var absolute = source.absolute.uri.resolve(path).toFilePath();
|
2017-09-23 21:57:54 +00:00
|
|
|
var stat = await fileSystem.stat(absolute);
|
2017-02-22 23:43:27 +00:00
|
|
|
return await serveStat(absolute, stat, req, res);
|
|
|
|
}
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Writes the file at the path given by the [stat] to a response.
|
2017-02-22 23:43:27 +00:00
|
|
|
Future<bool> serveStat(String absolute, FileStat stat, RequestContext req,
|
|
|
|
ResponseContext res) async {
|
2017-06-16 02:05:06 +00:00
|
|
|
if (stat.type == FileSystemEntityType.DIRECTORY)
|
2017-09-23 21:57:54 +00:00
|
|
|
return await serveDirectory(
|
|
|
|
fileSystem.directory(absolute), stat, req, res);
|
2017-02-22 23:43:27 +00:00
|
|
|
else if (stat.type == FileSystemEntityType.FILE)
|
2017-09-23 21:57:54 +00:00
|
|
|
return await serveFile(fileSystem.file(absolute), stat, req, res);
|
2017-02-22 23:43:27 +00:00
|
|
|
else if (stat.type == FileSystemEntityType.LINK) {
|
2017-09-23 21:57:54 +00:00
|
|
|
var link = fileSystem.link(absolute);
|
2017-02-22 23:43:27 +00:00
|
|
|
return await servePath(await link.resolveSymbolicLinks(), req, res);
|
|
|
|
} else
|
|
|
|
return true;
|
|
|
|
}
|
2016-11-23 20:14:05 +00:00
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Serves the index file of a [directory], if it exists.
|
2017-06-16 02:05:06 +00:00
|
|
|
Future<bool> serveDirectory(Directory directory, FileStat stat,
|
|
|
|
RequestContext req, ResponseContext res) async {
|
|
|
|
for (String indexFileName in indexFileNames) {
|
|
|
|
final index =
|
2017-09-23 21:57:54 +00:00
|
|
|
fileSystem.file(directory.absolute.uri.resolve(indexFileName));
|
2017-06-16 02:05:06 +00:00
|
|
|
if (await index.exists()) {
|
|
|
|
return await serveFile(index, stat, req, res);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
|
2017-09-23 21:57:54 +00:00
|
|
|
var value = req.headers.value('accept');
|
2017-06-16 02:05:06 +00:00
|
|
|
bool acceptable = value == null ||
|
2017-06-16 03:13:01 +00:00
|
|
|
value?.isNotEmpty != true ||
|
|
|
|
(mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) ||
|
2017-06-16 03:05:07 +00:00
|
|
|
value?.contains('*/*') == true;
|
2017-06-16 02:05:06 +00:00
|
|
|
if (!acceptable)
|
|
|
|
throw new AngelHttpException(
|
|
|
|
new UnsupportedError(
|
|
|
|
'Client requested $value, but server wanted to send $mimeType.'),
|
2017-09-23 21:57:54 +00:00
|
|
|
statusCode: 406,
|
2017-06-16 02:05:06 +00:00
|
|
|
message: '406 Not Acceptable');
|
|
|
|
}
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Writes the contents of a file to a response.
|
2017-06-16 02:05:06 +00:00
|
|
|
Future<bool> serveFile(
|
|
|
|
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
|
|
|
res.statusCode = 200;
|
|
|
|
|
|
|
|
if (callback != null) {
|
|
|
|
var r = callback(file, req, res);
|
|
|
|
r = r is Future ? await r : r;
|
|
|
|
if (r != null && r != true) return r;
|
|
|
|
}
|
|
|
|
|
2017-07-10 22:31:17 +00:00
|
|
|
var type = lookupMimeType(file.path) ?? 'application/octet-stream';
|
2017-06-16 02:05:06 +00:00
|
|
|
_ensureContentTypeAllowed(type, req);
|
2017-09-23 21:57:54 +00:00
|
|
|
res.headers['content-type'] = type;
|
2017-06-16 02:05:06 +00:00
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
await file.openRead().pipe(res);
|
2017-06-16 02:05:06 +00:00
|
|
|
return false;
|
|
|
|
}
|
2016-11-23 17:22:23 +00:00
|
|
|
}
|