Updated static

This commit is contained in:
thomashii 2021-07-04 11:22:19 +08:00
parent 89d710fe28
commit cadbc1c4f7
6 changed files with 160 additions and 83 deletions

View file

@ -1,80 +1,110 @@
# 4.0.0
# Change Log
## 4.0.1
* Fixed `push_state_test` unit test failure on Windows
* Fixed NNBD related issues
* Added logging to `VirtualDirectory` and `CachingVirtualDirectory` to log exception
## 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
## 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.1.3+2
## 2.1.3+2
* Prepare for upcoming change to File.openRead()
# 2.1.3+1
## 2.1.3+1
* Apply control flow lints.
# 2.1.3
## 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.
# 2.1.2
## 2.1.2
* Patch support for range+streaming in Caching server.
# 2.1.1
## 2.1.1
* URI-encode paths in directory listing. This produces correct URL's, always.
# 2.1.0
## 2.1.0
* Include support for the `Range` header.
* Use MD5 for etags, instead of a weak ETag.
# 2.0.2
## 2.0.2
* Fixed invalid HTML for directory listings.
# 2.0.1
## 2.0.1
* Remove use of `sendFile`.
* Add a `p.isWithin` check to ensure that paths do not escape the `source` directory.
* Handle `HEAD` requests.
# 2.0.0
## 2.0.0
* Upgrade dependencies to Angel 2 + file@5.
* Replace `useStream` with `useBuffer`.
* Remove `package:intl`, just use `HttpDate` instead.
# 1.3.0+1
## 1.3.0+1
* Dart 2 fixes.
* Enable optionally writing responses to the buffer instead of streaming.
# 1.3.0
## 1.3.0
* `pushState` uses `strict` mode when `accepts` is passed.
# 1.3.0-alpha+2
## 1.3.0-alpha+2
* Added an `accepts` option to `pushState`.
* Added optional directory listings.
# 1.3.0-alpha+1
## 1.3.0-alpha+1
* ETags once again only encode the first 50 bytes of files. Resolves [#27](https://github.com/angel-dart/static/issues/27).
# 1.3.0-alpha
## 1.3.0-alpha
* Removed file transformers.
* `VirtualDirectory` is no longer an `AngelPlugin`, and instead exposes a `handleRequest` middleware.
* Added `pushState` to `VirtualDirectory`.
# 1.2.5
## 1.2.5
* Fixed a bug where `onlyInProduction` was not properly adhered to.
* Fixed another bug where `Accept-Encoding` was not properly adhered to.
* Setting `maxAge` to `null` will now prevent a `CachingVirtualDirectory` from sending an `Expires` header.
* Pre-built assets can now be mass-deleted with `VirtualDirectory.cleanFromDisk()`.
Resolves [#22](https://github.com/angel-dart/static/issues/22).
# 1.2.4+1
## 1.2.4+1
Fixed a bug where `Accept-Encoding` was not properly adhered to.
# 1.2.4
Fixes https://github.com/angel-dart/angel/issues/44.
## 1.2.4
Fixes <https://github.com/angel-dart/angel/issues/44>.
* MIME types will now default to `application/octet-stream`.
* When `streamToIO` is `true`, the body will only be sent gzipped if the request explicitly allows it.
# 1.2.3
## 1.2.3
Fixed #40 and #41, which dealt with paths being improperly served when using a
`publicPath`.

View file

@ -1,16 +1,17 @@
# angel3_static
# Angel3 Static Files Service
[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_static)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/static/LICENSE)
Static server infrastructure for Angel.
This package supports serving static files such as html, css and js for [Angel3 framework](https://pub.dartlang.org/packages/angel3).
*Can also handle `Range` requests now, making it suitable for media streaming, ex. music, video, etc.*
# Installation
## Installation
In `pubspec.yaml`:
```yaml
@ -18,9 +19,9 @@ dependencies:
angel3_static: ^4.0.0
```
# Usage
To serve files from a directory, you need to create a `VirtualDirectory`.
Keep in mind that `angel3_static` uses `package:file` instead of `dart:io`.
## Usage
To serve files from a directory, you need to create a `VirtualDirectory`. Keep in mind that `angel3_static` uses `package:file` instead of `dart:io`.
```dart
import 'package:angel3_framework/angel3_framework.dart';
@ -46,10 +47,9 @@ void main() async {
}
```
# Push State
`VirtualDirectory` also exposes a `pushState` method that returns a
request handler that serves the file at a given path as a fallback, unless
the user is requesting that file. This can be very useful for SPA's.
## Push State
`VirtualDirectory` also exposes a `pushState` method that returns a request handler that serves the file at a given path as a fallback, unless the user is requesting that file. This can be very useful for SPA's.
```dart
// Create VirtualDirectory as well
@ -62,8 +62,10 @@ app.fallback(vDir.handleRequest);
app.fallback(vDir.pushState('index.html'));
```
# Options
## Options
The `VirtualDirectory` API accepts a few named parameters:
- **source**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `web` (in development) or
`build/web` (in production), depending on your `ANGEL_ENV`.
- **indexFileNames**: A `List<String>` of filenames that should be served as index pages. Default is `['index.html']`.
@ -71,4 +73,4 @@ The `VirtualDirectory` API accepts a few named parameters:
angel_static is serving your files. If you are not serving static files at the site root,
please include this.
- **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`,
then the callback's result will be sent to the user, instead of the file contents.
then the callback's result will be sent to the user, instead of the file contents.

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io' show HttpDate;
import 'package:angel3_framework/angel3_framework.dart';
import 'package:file/file.dart';
import 'package:logging/logging.dart';
import 'virtual_directory.dart';
/// Returns a string representation of the given [CacheAccessLevel].
@ -18,6 +19,8 @@ String accessLevelToString(CacheAccessLevel accessLevel) {
/// A `VirtualDirectory` that also sets `Cache-Control` headers.
class CachingVirtualDirectory extends VirtualDirectory {
final _log = Logger('CachingVirtualDirectory');
final Map<String, String> _etags = {};
/// Either `PUBLIC` or `PRIVATE`.
@ -40,20 +43,20 @@ class CachingVirtualDirectory extends VirtualDirectory {
CachingVirtualDirectory(Angel app, FileSystem fileSystem,
{this.accessLevel = CacheAccessLevel.PUBLIC,
Directory? source,
bool? debug,
Iterable<String>? indexFileNames,
bool debug = false,
Iterable<String> indexFileNames = const ['index.html'],
this.maxAge = 0,
this.noCache = false,
this.onlyInProduction = false,
this.useEtags = true,
bool? allowDirectoryListing,
bool allowDirectoryListing = false,
bool useBuffer = false,
String? publicPath,
String publicPath = '/',
Function(File file, RequestContext req, ResponseContext res)? callback})
: super(app, fileSystem,
source: source,
indexFileNames: indexFileNames ?? ['index.html'],
publicPath: publicPath ?? '/',
indexFileNames: indexFileNames,
publicPath: publicPath,
callback: callback,
allowDirectoryListing: allowDirectoryListing,
useBuffer: useBuffer);
@ -63,27 +66,35 @@ class CachingVirtualDirectory extends VirtualDirectory {
File file, FileStat stat, RequestContext req, ResponseContext res) {
res.headers['accept-ranges'] = 'bytes';
if (onlyInProduction == true && req.app!.environment.isProduction != true) {
if (onlyInProduction == true && req.app?.environment.isProduction != true) {
return super.serveFile(file, stat, req, res);
}
if (req.headers == null) {
_log.severe('Missing headers in the RequestContext');
throw ArgumentError('Missing headers in the RequestContext');
}
var reqHeaders = req.headers!;
var shouldNotCache = noCache == true;
if (!shouldNotCache) {
shouldNotCache = req.headers!.value('cache-control') == 'no-cache' ||
req.headers!.value('pragma') == 'no-cache';
shouldNotCache = reqHeaders.value('cache-control') == 'no-cache' ||
reqHeaders.value('pragma') == 'no-cache';
}
if (shouldNotCache) {
res.headers['cache-control'] = 'private, max-age=0, no-cache';
return super.serveFile(file, stat, req, res);
} else {
var ifModified = req.headers!.ifModifiedSince;
var ifModified = reqHeaders.ifModifiedSince;
var ifRange = false;
try {
ifModified = HttpDate.parse(req.headers!.value('if-range')!);
ifRange = true;
if (reqHeaders.value('if-range') != null) {
ifModified = HttpDate.parse(reqHeaders.value('if-range')!);
ifRange = true;
}
} catch (_) {
// Fail silently...
}
@ -97,7 +108,9 @@ class CachingVirtualDirectory extends VirtualDirectory {
setCachedHeaders(stat.modified, req, res);
if (useEtags && _etags.containsKey(file.absolute.path)) {
res.headers['ETag'] = _etags[file.absolute.path]!;
if (_etags[file.absolute.path] != null) {
res.headers['ETag'] = _etags[file.absolute.path]!;
}
}
if (ifRange) {
@ -111,6 +124,8 @@ class CachingVirtualDirectory extends VirtualDirectory {
return super.serveFile(file, stat, req, res);
}
} catch (_) {
_log.severe(
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
throw AngelHttpException.badRequest(
message:
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
@ -120,18 +135,18 @@ class CachingVirtualDirectory extends VirtualDirectory {
// If-modified didn't work; try etags
if (useEtags == true) {
var etagsToMatchAgainst = req.headers!['if-none-match'];
var etagsToMatchAgainst = reqHeaders['if-none-match'] ?? [];
ifRange = false;
if (etagsToMatchAgainst?.isNotEmpty != true) {
etagsToMatchAgainst = req.headers!['if-range'];
ifRange = etagsToMatchAgainst?.isNotEmpty == true;
if (etagsToMatchAgainst.isEmpty) {
etagsToMatchAgainst = reqHeaders['if-range'] ?? [];
ifRange = etagsToMatchAgainst.isNotEmpty;
}
if (etagsToMatchAgainst?.isNotEmpty == true) {
if (etagsToMatchAgainst.isNotEmpty) {
var hasBeenModified = false;
for (var etag in etagsToMatchAgainst!) {
for (var etag in etagsToMatchAgainst) {
if (etag == '*') {
hasBeenModified = true;
} else {

View file

@ -3,6 +3,7 @@ import 'package:angel3_framework/angel3_framework.dart';
import 'package:file/file.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;
import 'package:logging/logging.dart';
import 'package:angel3_range_header/angel3_range_header.dart';
final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
@ -28,11 +29,13 @@ String _pathify(String path) {
/// A static server plug-in.
class VirtualDirectory {
String? _prefix;
Directory? _source;
final _log = Logger('VirtualDirectory');
late String _prefix;
late Directory _source;
/// The directory to serve files from.
Directory? get source => _source;
Directory get source => _source;
/// An optional callback to run before serving files.
final Function(File file, RequestContext req, ResponseContext res)? callback;
@ -47,7 +50,7 @@ class VirtualDirectory {
final String publicPath;
/// If `true` (default: `false`), then if a directory does not contain any of the specific [indexFileNames], a default directory listing will be served.
final bool? allowDirectoryListing;
final bool allowDirectoryListing;
/// If `true` (default: `true`), then files will be opened as streams and piped into the request.
///
@ -77,7 +80,7 @@ class VirtualDirectory {
}
var path = req.uri!.path.replaceAll(_straySlashes, '');
if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix!)) {
if (_prefix.isNotEmpty == true && !path.startsWith(_prefix)) {
return Future<bool>.value(true);
}
@ -91,7 +94,7 @@ class VirtualDirectory {
/// the view will be served.
RequestHandler pushState(String path, {Iterable? accepts}) {
var vPath = path.replaceAll(_straySlashes, '');
if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
if (_prefix.isNotEmpty == true) vPath = '$_prefix/$vPath';
return (RequestContext req, ResponseContext res) {
var path = req.path.replaceAll(_straySlashes, '');
@ -110,22 +113,28 @@ class VirtualDirectory {
/// Writes the file at the given virtual [path] to a response.
Future<bool> servePath(
String path, RequestContext req, ResponseContext res) async {
if (_prefix!.isNotEmpty) {
if (_prefix.isNotEmpty) {
// Only replace the *first* incidence
// Resolve: https://github.com/angel-dart/angel/issues/41
path = path.replaceFirst(RegExp('^' + _pathify(_prefix!)), '');
path = path.replaceFirst(RegExp('^' + _pathify(_prefix)), '');
}
if (path.isEmpty) path = '.';
path = path.replaceAll(_straySlashes, '');
var absolute = source!.absolute.uri.resolve(path).toFilePath();
var parent = source!.absolute.uri.toFilePath();
var absolute = source.absolute.uri.resolve(path).toFilePath();
var parent = source.absolute.uri.toFilePath();
if (!p.isWithin(parent, absolute) && !p.equals(parent, absolute)) {
return true;
}
// Replace the separator when running on Windows with file system
// detected as Linux
if (absolute.contains('\\') && fileSystem.path.separator == '/') {
absolute = absolute.replaceAll('\\', '/');
}
var stat = await fileSystem.stat(absolute);
return await serveStat(absolute, path, stat, req, res);
}
@ -199,8 +208,8 @@ class VirtualDirectory {
});
for (var entity in entities) {
String? stub;
String? type;
String stub;
String type;
if (entity is File) {
type = '[File]';
@ -213,23 +222,24 @@ class VirtualDirectory {
stub = p.basename(entity.path);
} else {
//TODO: Handle unknown type
_log.severe('Unknown file entity. Not a file, directory or link.');
type = '[]';
stub = '';
}
var href = stub;
if (relative.isNotEmpty) {
stub ??= '';
href = '/' + relative + '/' + stub;
}
if (entity is Directory) {
if (href == null) {
if (href == '') {
href = '/';
} else {
href += '/';
}
}
href = Uri.encodeFull(href!);
href = Uri.encodeFull(href);
res.write('<li><a href="$href">$type $stub</a></li>');
}
@ -242,12 +252,13 @@ class VirtualDirectory {
}
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
var value = req.headers!.value('accept');
var value = req.headers?.value('accept');
var acceptable = value == null ||
value.isNotEmpty != true ||
(mimeType.isNotEmpty == true && value.contains(mimeType) == true) ||
value.contains('*/*') == true;
if (!acceptable) {
_log.severe('Mime type [$value] is not supported');
throw AngelHttpException(
UnsupportedError(
'Client requested $value, but server wanted to send $mimeType.'),
@ -262,11 +273,12 @@ class VirtualDirectory {
res.headers['accept-ranges'] = 'bytes';
if (callback != null) {
return await req.app!.executeHandler(
(RequestContext req, ResponseContext res) =>
callback!(file, req, res),
req,
res);
return await req.app?.executeHandler(
(RequestContext req, ResponseContext res) =>
callback!(file, req, res),
req,
res) ??
true;
}
var type =
@ -277,14 +289,21 @@ class VirtualDirectory {
res.contentType = MediaType.parse(type);
if (useBuffer == true) res.useBuffer();
if (req.headers!.value('range')?.startsWith('bytes=') != true) {
if (req.headers == null) {
_log.severe('Missing headers in the RequestContext');
throw ArgumentError('Missing headers in the RequestContext');
}
var reqHeaders = req.headers!;
if (reqHeaders.value('range')?.startsWith('bytes=') != true) {
await res.streamFile(file);
} else {
var header = RangeHeader.parse(req.headers!.value('range')!);
var header = RangeHeader.parse(reqHeaders.value('range')!);
var items = RangeHeader.foldItems(header.items);
var totalFileSize = await file.length();
header = RangeHeader(items);
var totalFileSize = await file.length();
for (var item in header.items) {
var invalid = false;

View file

@ -1,21 +1,22 @@
name: angel3_static
description: Static server middleware for Angel. Also capable of serving Range responses.
version: 4.0.0
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/static
version: 4.0.1
homepage: https://github.com/dukefirehawk/angel
repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/static
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
angel3_framework: ^4.0.0
angel3_framework: ^4.1.0
angel3_range_header: ^3.0.0
convert: ^3.0.0
crypto: ^3.0.1
file: ^6.1.0
http_parser: ^4.0.0
path: ^1.8.0
logging: ^1.0.1
dev_dependencies:
angel3_test: ^4.0.0
http: ^0.13.2
logging: ^1.0.1
matcher: ^0.12.10
pedantic: ^1.11.0
test: ^1.17.4

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Push Test</title>
</head>
<body>
<h1>Hello!</h1>
<i>Hooray for testing...</i>
</body>
</html>