This commit is contained in:
Tobe O 2019-05-02 19:29:09 -04:00
parent 42779a85e4
commit fd53da891f
12 changed files with 85 additions and 73 deletions

View file

@ -1,3 +1,8 @@
# 2.1.3
* Apply lints.
* Pin to Dart `>=2.0.0 <3.0.0`.
* Use at least version `2.0.0-rc.0` of `angel_framework`.
# 2.1.2+1 # 2.1.2+1
* Fix a typo that prevented `Range` requests from working. * Fix a typo that prevented `Range` requests from working.

View file

@ -25,20 +25,20 @@ import 'package:angel_static/angel_static.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
main() async { main() async {
var app = new Angel(); var app = Angel();
var fs = const LocalFileSystem(); var fs = const LocalFileSystem();
// Normal static server // Normal static server
var vDir = new VirtualDirectory(app, fs, source: new Directory('./public')); var vDir = VirtualDirectory(app, fs, source: Directory('./public'));
// Send Cache-Control, ETag, etc. as well // Send Cache-Control, ETag, etc. as well
var vDir = new CachingVirtualDirectory(app, fs, source: new Directory('./public')); var vDir = CachingVirtualDirectory(app, fs, source: Directory('./public'));
// Mount the VirtualDirectory's request handler // Mount the VirtualDirectory's request handler
app.fallback(vDir.handleRequest); app.fallback(vDir.handleRequest);
// Start your server!!! // Start your server!!!
await new AngelHttp(app).startServer(); await AngelHttp(app).startServer();
} }
``` ```
@ -49,7 +49,7 @@ the user is requesting that file. This can be very useful for SPA's.
```dart ```dart
// Create VirtualDirectory as well // Create VirtualDirectory as well
var vDir = new CachingVirtualDirectory(...); var vDir = CachingVirtualDirectory(...);
// Mount it // Mount it
app.fallback(vDir.handleRequest); app.fallback(vDir.handleRequest);

View file

@ -1,3 +1,8 @@
include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
strong-mode: strong-mode:
implicit-casts: false implicit-casts: false
linter:
rules:
- unnecessary_const
- unnecessary_new

View file

@ -5,10 +5,10 @@ import 'package:file/local.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
main(List<String> args) async { main(List<String> args) async {
var app = new Angel(); var app = Angel();
var http = new AngelHttp(app); var http = AngelHttp(app);
var fs = const LocalFileSystem(); var fs = const LocalFileSystem();
var vDir = new CachingVirtualDirectory( var vDir = CachingVirtualDirectory(
app, app,
fs, fs,
allowDirectoryListing: true, allowDirectoryListing: true,
@ -24,7 +24,7 @@ main(List<String> args) async {
..addExtension('md', 'text/plain') ..addExtension('md', 'text/plain')
..addExtension('yaml', 'text/plain'); ..addExtension('yaml', 'text/plain');
app.logger = new Logger('example') app.logger = Logger('example')
..onRecord.listen((rec) { ..onRecord.listen((rec) {
print(rec); print(rec);
if (rec.error != null) print(rec.error); if (rec.error != null) print(rec.error);
@ -32,6 +32,7 @@ main(List<String> args) async {
}); });
app.fallback(vDir.handleRequest); app.fallback(vDir.handleRequest);
app.fallback((req, res) => throw AngelHttpException.notFound());
var server = await http.startServer('127.0.0.1', 3000); var server = await http.startServer('127.0.0.1', 3000);
print('Serving from ${vDir.source.path}'); print('Serving from ${vDir.source.path}');

View file

@ -12,7 +12,7 @@ String accessLevelToString(CacheAccessLevel accessLevel) {
case CacheAccessLevel.PUBLIC: case CacheAccessLevel.PUBLIC:
return 'public'; return 'public';
default: default:
throw new ArgumentError('Unrecognized cache access level: $accessLevel'); throw ArgumentError('Unrecognized cache access level: $accessLevel');
} }
} }
@ -38,16 +38,16 @@ class CachingVirtualDirectory extends VirtualDirectory {
final int maxAge; final int maxAge;
CachingVirtualDirectory(Angel app, FileSystem fileSystem, CachingVirtualDirectory(Angel app, FileSystem fileSystem,
{this.accessLevel: CacheAccessLevel.PUBLIC, {this.accessLevel = CacheAccessLevel.PUBLIC,
Directory source, Directory source,
bool debug, bool debug,
Iterable<String> indexFileNames, Iterable<String> indexFileNames,
this.maxAge: 0, this.maxAge = 0,
this.noCache: false, this.noCache = false,
this.onlyInProduction: false, this.onlyInProduction = false,
this.useEtags: true, this.useEtags = true,
bool allowDirectoryListing, bool allowDirectoryListing,
bool useBuffer: false, bool useBuffer = false,
String publicPath, String publicPath,
callback(File file, RequestContext req, ResponseContext res)}) callback(File file, RequestContext req, ResponseContext res)})
: super(app, fileSystem, : super(app, fileSystem,
@ -63,7 +63,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
File file, FileStat stat, RequestContext req, ResponseContext res) { File file, FileStat stat, RequestContext req, ResponseContext res) {
res.headers['accept-ranges'] = 'bytes'; res.headers['accept-ranges'] = 'bytes';
if (onlyInProduction == true && req.app.isProduction != true) { if (onlyInProduction == true && req.app.environment.isProduction != true) {
return super.serveFile(file, stat, req, res); return super.serveFile(file, stat, req, res);
} }
@ -105,12 +105,12 @@ class CachingVirtualDirectory extends VirtualDirectory {
return super.serveFile(file, stat, req, res); return super.serveFile(file, stat, req, res);
} }
return new Future.value(false); return Future.value(false);
} else if (ifRange) { } else if (ifRange) {
return super.serveFile(file, stat, req, res); return super.serveFile(file, stat, req, res);
} }
} catch (_) { } catch (_) {
throw new AngelHttpException.badRequest( throw AngelHttpException.badRequest(
message: message:
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.'); 'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
} }
@ -143,7 +143,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
if (!hasBeenModified) { if (!hasBeenModified) {
res.statusCode = 304; res.statusCode = 304;
setCachedHeaders(stat.modified, req, res); setCachedHeaders(stat.modified, req, res);
return new Future.value(false); return Future.value(false);
} }
} else { } else {
return super.serveFile(file, stat, req, res); return super.serveFile(file, stat, req, res);
@ -172,7 +172,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
..['last-modified'] = HttpDate.format(modified); ..['last-modified'] = HttpDate.format(modified);
if (maxAge != null) { if (maxAge != null) {
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0)); var expiry = DateTime.now().add(Duration(seconds: maxAge ?? 0));
res.headers['expires'] = HttpDate.format(expiry); res.headers['expires'] = HttpDate.format(expiry);
} }
} }

View file

@ -5,8 +5,8 @@ import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:range_header/range_header.dart'; import 'package:range_header/range_header.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
String _pathify(String path) { String _pathify(String path) {
var p = path.replaceAll(_straySlashes, ''); var p = path.replaceAll(_straySlashes, '');
@ -54,16 +54,16 @@ class VirtualDirectory {
VirtualDirectory(this.app, this.fileSystem, VirtualDirectory(this.app, this.fileSystem,
{Directory source, {Directory source,
this.indexFileNames: const ['index.html'], this.indexFileNames = const ['index.html'],
this.publicPath: '/', this.publicPath = '/',
this.callback, this.callback,
this.allowDirectoryListing: false, this.allowDirectoryListing = false,
this.useBuffer: false}) { this.useBuffer = false}) {
_prefix = publicPath.replaceAll(_straySlashes, ''); _prefix = publicPath.replaceAll(_straySlashes, '');
if (source != null) { if (source != null) {
_source = source; _source = source;
} else { } else {
String dirPath = app.isProduction ? './build/web' : './web'; String dirPath = app.environment.isProduction ? './build/web' : './web';
_source = fileSystem.directory(dirPath); _source = fileSystem.directory(dirPath);
} }
} }
@ -71,11 +71,11 @@ class VirtualDirectory {
/// Responds to incoming HTTP requests. /// Responds to incoming HTTP requests.
Future<bool> handleRequest(RequestContext req, ResponseContext res) { Future<bool> handleRequest(RequestContext req, ResponseContext res) {
if (req.method != 'GET' && req.method != 'HEAD') if (req.method != 'GET' && req.method != 'HEAD')
return new Future<bool>.value(true); return Future<bool>.value(true);
var path = req.uri.path.replaceAll(_straySlashes, ''); var path = req.uri.path.replaceAll(_straySlashes, '');
if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix)) if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix))
return new Future<bool>.value(true); return Future<bool>.value(true);
return servePath(path, req, res); return servePath(path, req, res);
} }
@ -91,11 +91,11 @@ class VirtualDirectory {
return (RequestContext req, ResponseContext res) { return (RequestContext req, ResponseContext res) {
var path = req.path.replaceAll(_straySlashes, ''); var path = req.path.replaceAll(_straySlashes, '');
if (path == vPath) return new Future<bool>.value(true); if (path == vPath) return Future<bool>.value(true);
if (accepts?.isNotEmpty == true) { if (accepts?.isNotEmpty == true) {
if (!accepts.any((x) => req.accepts(x, strict: true))) if (!accepts.any((x) => req.accepts(x, strict: true)))
return new Future<bool>.value(true); return Future<bool>.value(true);
} }
return servePath(vPath, req, res); return servePath(vPath, req, res);
@ -108,7 +108,7 @@ class VirtualDirectory {
if (_prefix.isNotEmpty) { if (_prefix.isNotEmpty) {
// Only replace the *first* incidence // Only replace the *first* incidence
// Resolve: https://github.com/angel-dart/angel/issues/41 // Resolve: https://github.com/angel-dart/angel/issues/41
path = path.replaceFirst(new RegExp('^' + _pathify(_prefix)), ''); path = path.replaceFirst(RegExp('^' + _pathify(_prefix)), '');
} }
if (path.isEmpty) path = '.'; if (path.isEmpty) path = '.';
@ -151,7 +151,7 @@ class VirtualDirectory {
} }
if (allowDirectoryListing == true) { if (allowDirectoryListing == true) {
res.contentType = new MediaType('text', 'html'); res.contentType = MediaType('text', 'html');
res res
..write('<!DOCTYPE html>') ..write('<!DOCTYPE html>')
..write('<html>') ..write('<html>')
@ -165,7 +165,7 @@ class VirtualDirectory {
List<FileSystemEntity> entities = await directory List<FileSystemEntity> entities = await directory
.list(followLinks: false) .list(followLinks: false)
.toList() .toList()
.then((l) => new List.from(l)); .then((l) => List.from(l));
entities.sort((a, b) { entities.sort((a, b) {
if (a is Directory) { if (a is Directory) {
if (b is Directory) return a.path.compareTo(b.path); if (b is Directory) return a.path.compareTo(b.path);
@ -213,8 +213,8 @@ class VirtualDirectory {
(mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) || (mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) ||
value?.contains('*/*') == true; value?.contains('*/*') == true;
if (!acceptable) if (!acceptable)
throw new AngelHttpException( throw AngelHttpException(
new UnsupportedError( UnsupportedError(
'Client requested $value, but server wanted to send $mimeType.'), 'Client requested $value, but server wanted to send $mimeType.'),
statusCode: 406, statusCode: 406,
message: '406 Not Acceptable'); message: '406 Not Acceptable');
@ -237,16 +237,16 @@ class VirtualDirectory {
res.headers['accept-ranges'] = 'bytes'; res.headers['accept-ranges'] = 'bytes';
_ensureContentTypeAllowed(type, req); _ensureContentTypeAllowed(type, req);
res.headers['accept-ranges'] = 'bytes'; res.headers['accept-ranges'] = 'bytes';
res.contentType = new MediaType.parse(type); res.contentType = MediaType.parse(type);
if (useBuffer == true) res.useBuffer(); if (useBuffer == true) res.useBuffer();
if (req.headers.value('range')?.startsWith('bytes=') != true) { if (req.headers.value('range')?.startsWith('bytes=') != true) {
await res.streamFile(file); await res.streamFile(file);
} else { } else {
var header = new RangeHeader.parse(req.headers.value('range')); var header = RangeHeader.parse(req.headers.value('range'));
var items = RangeHeader.foldItems(header.items); var items = RangeHeader.foldItems(header.items);
var totalFileSize = await file.length(); var totalFileSize = await file.length();
header = new RangeHeader(items); header = RangeHeader(items);
for (var item in header.items) { for (var item in header.items) {
bool invalid = false; bool invalid = false;
@ -257,23 +257,23 @@ class VirtualDirectory {
invalid = item.end == -1; invalid = item.end == -1;
if (invalid) { if (invalid) {
throw new AngelHttpException( throw AngelHttpException(
new Exception("Semantically invalid, or unbounded range."), Exception("Semantically invalid, or unbounded range."),
statusCode: 416, statusCode: 416,
message: "Semantically invalid, or unbounded range."); message: "Semantically invalid, or unbounded range.");
} }
// Ensure it's within range. // Ensure it's within range.
if (item.start >= totalFileSize || item.end >= totalFileSize) { if (item.start >= totalFileSize || item.end >= totalFileSize) {
throw new AngelHttpException( throw AngelHttpException(
new Exception("Given range $item is out of bounds."), Exception("Given range $item is out of bounds."),
statusCode: 416, statusCode: 416,
message: "Given range $item is out of bounds."); message: "Given range $item is out of bounds.");
} }
} }
if (header.items.isEmpty) { if (header.items.isEmpty) {
throw new AngelHttpException(null, throw AngelHttpException(null,
statusCode: 416, message: '`Range` header may not be empty.'); statusCode: 416, message: '`Range` header may not be empty.');
} else if (header.items.length == 1) { } else if (header.items.length == 1) {
var item = header.items[0]; var item = header.items[0];
@ -298,7 +298,7 @@ class VirtualDirectory {
} }
} }
res.contentType = new MediaType.parse( res.contentType = MediaType.parse(
app.mimeTypeResolver.lookup(file.path) ?? app.mimeTypeResolver.lookup(file.path) ??
'application/octet-stream'); 'application/octet-stream');
res.statusCode = 206; res.statusCode = 206;
@ -307,7 +307,7 @@ class VirtualDirectory {
await stream.pipe(res); await stream.pipe(res);
return false; return false;
} else { } else {
var transformer = new RangeHeaderTransformer( var transformer = RangeHeaderTransformer(
header, header,
app.mimeTypeResolver.lookup(file.path) ?? app.mimeTypeResolver.lookup(file.path) ??
'application/octet-stream', 'application/octet-stream',
@ -315,7 +315,7 @@ class VirtualDirectory {
res.statusCode = 206; res.statusCode = 206;
res.headers['content-length'] = res.headers['content-length'] =
transformer.computeContentLength(totalFileSize).toString(); transformer.computeContentLength(totalFileSize).toString();
res.contentType = new MediaType( res.contentType = MediaType(
'multipart', 'byteranges', {'boundary': transformer.boundary}); 'multipart', 'byteranges', {'boundary': transformer.boundary});
await file.openRead().transform(transformer).pipe(res); await file.openRead().transform(transformer).pipe(res);
return false; return false;

View file

@ -1,12 +1,12 @@
name: angel_static name: angel_static
description: Static server middleware for Angel. Also capable of serving Range responses. description: Static server middleware for Angel. Also capable of serving Range responses.
environment: environment:
sdk: ">=1.8.0 <3.0.0" sdk: ">=2.0.0 <3.0.0"
homepage: https://github.com/angel-dart/static homepage: https://github.com/angel-dart/static
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
version: 2.1.2+1 version: 2.1.2+1
dependencies: dependencies:
angel_framework: ^2.0.0-alpha angel_framework: ^2.0.0-rc.0
convert: ^2.0.0 convert: ^2.0.0
crypto: ^2.0.0 crypto: ^2.0.0
file: ^5.0.0 file: ^5.0.0
@ -18,4 +18,5 @@ dev_dependencies:
http: http:
logging: ^0.11.0 logging: ^0.11.0
matcher: ^0.12.0 matcher: ^0.12.0
pedantic: ^1.0.0
test: ^1.0.0 test: ^1.0.0

View file

@ -12,22 +12,22 @@ main() {
AngelHttp http; AngelHttp http;
Directory testDir = const LocalFileSystem().directory('test'); Directory testDir = const LocalFileSystem().directory('test');
String url; String url;
Client client = new Client(); Client client = Client();
setUp(() async { setUp(() async {
app = new Angel(); app = Angel();
http = new AngelHttp(app); http = AngelHttp(app);
app.logger = new Logger('angel')..onRecord.listen(print); app.logger = Logger('angel')..onRecord.listen(print);
app.fallback( app.fallback(
new VirtualDirectory(app, const LocalFileSystem(), VirtualDirectory(app, const LocalFileSystem(),
source: testDir, source: testDir,
publicPath: '/virtual', publicPath: '/virtual',
indexFileNames: ['index.txt']).handleRequest, indexFileNames: ['index.txt']).handleRequest,
); );
app.fallback( app.fallback(
new VirtualDirectory(app, const LocalFileSystem(), VirtualDirectory(app, const LocalFileSystem(),
source: testDir, source: testDir,
useBuffer: true, useBuffer: true,
indexFileNames: ['index.php', 'index.txt']).handleRequest, indexFileNames: ['index.php', 'index.txt']).handleRequest,

View file

@ -8,11 +8,11 @@ main() async {
Angel app; Angel app;
AngelHttp http; AngelHttp http;
Directory testDir = const LocalFileSystem().directory('test'); Directory testDir = const LocalFileSystem().directory('test');
app = new Angel(); app = Angel();
http = new AngelHttp(app); http = AngelHttp(app);
app.fallback( app.fallback(
new CachingVirtualDirectory(app, const LocalFileSystem(), CachingVirtualDirectory(app, const LocalFileSystem(),
source: testDir, source: testDir,
maxAge: 350, maxAge: 350,
onlyInProduction: false, onlyInProduction: false,

View file

@ -14,14 +14,14 @@ main() {
AngelHttp http; AngelHttp http;
Directory testDir = const LocalFileSystem().directory('test'); Directory testDir = const LocalFileSystem().directory('test');
String url; String url;
Client client = new Client(); Client client = Client();
setUp(() async { setUp(() async {
app = new Angel(); app = Angel();
http = new AngelHttp(app); http = AngelHttp(app);
app.fallback( app.fallback(
new CachingVirtualDirectory(app, const LocalFileSystem(), CachingVirtualDirectory(app, const LocalFileSystem(),
source: testDir, maxAge: 350, onlyInProduction: false, source: testDir, maxAge: 350, onlyInProduction: false,
//publicPath: '/virtual', //publicPath: '/virtual',
indexFileNames: ['index.txt']).handleRequest, indexFileNames: ['index.txt']).handleRequest,
@ -31,7 +31,7 @@ main() {
app.dumpTree(showMatchers: true); app.dumpTree(showMatchers: true);
app.logger = new Logger('angel_static') app.logger = Logger('angel_static')
..onRecord.listen((rec) { ..onRecord.listen((rec) {
print(rec); print(rec);
if (rec.error != null) print(rec.error); if (rec.error != null) print(rec.error);
@ -63,7 +63,7 @@ main() {
test('if-modified-since', () async { test('if-modified-since', () async {
var response = await client.get("$url", headers: { var response = await client.get("$url", headers: {
'if-modified-since': 'if-modified-since':
HttpDate.format(new DateTime.now().add(new Duration(days: 365))) HttpDate.format(DateTime.now().add(Duration(days: 365)))
}); });
print('Response status: ${response.statusCode}'); print('Response status: ${response.statusCode}');

View file

@ -23,11 +23,11 @@ main() async {
.readAsString(); .readAsString();
// Initialize app // Initialize app
var app = new Angel(); var app = Angel();
app.logger = new Logger('angel')..onRecord.listen(print); app.logger = Logger('angel')..onRecord.listen(print);
app.fallback( app.fallback(
new VirtualDirectory(app, const LocalFileSystem(), VirtualDirectory(app, const LocalFileSystem(),
source: swaggerUiDistDir, publicPath: 'swagger/') source: swaggerUiDistDir, publicPath: 'swagger/')
.handleRequest, .handleRequest,
); );

View file

@ -11,7 +11,7 @@ main() {
TestClient client; TestClient client;
setUp(() async { setUp(() async {
fileSystem = new MemoryFileSystem(); fileSystem = MemoryFileSystem();
var webDir = fileSystem.directory('web'); var webDir = fileSystem.directory('web');
await webDir.create(recursive: true); await webDir.create(recursive: true);
@ -19,9 +19,9 @@ main() {
var indexFile = webDir.childFile('index.html'); var indexFile = webDir.childFile('index.html');
await indexFile.writeAsString('index'); await indexFile.writeAsString('index');
app = new Angel(); app = Angel();
var vDir = new VirtualDirectory( var vDir = VirtualDirectory(
app, app,
fileSystem, fileSystem,
source: webDir, source: webDir,
@ -32,7 +32,7 @@ main() {
..fallback(vDir.pushState('index.html')) ..fallback(vDir.pushState('index.html'))
..fallback((req, res) => 'Fallback'); ..fallback((req, res) => 'Fallback');
app.logger = new Logger('push_state') app.logger = Logger('push_state')
..onRecord.listen( ..onRecord.listen(
(rec) { (rec) {
print(rec); print(rec);