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']`.

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')!);
if (reqHeaders.value('if-range') != null) {
ifModified = HttpDate.parse(reqHeaders.value('if-range')!);
ifRange = true;
}
} catch (_) {
// Fail silently...
}
@ -97,8 +108,10 @@ class CachingVirtualDirectory extends VirtualDirectory {
setCachedHeaders(stat.modified, req, res);
if (useEtags && _etags.containsKey(file.absolute.path)) {
if (_etags[file.absolute.path] != null) {
res.headers['ETag'] = _etags[file.absolute.path]!;
}
}
if (ifRange) {
// Send the 206 like normal
@ -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(
return await req.app?.executeHandler(
(RequestContext req, ResponseContext res) =>
callback!(file, req, res),
req,
res);
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>