waiting on angel_test
This commit is contained in:
parent
d30edcdef6
commit
393c4bff02
15 changed files with 200 additions and 835 deletions
|
@ -1,3 +1,8 @@
|
||||||
|
# 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 a bug where `onlyInProduction` was not properly adhered to.
|
||||||
* Fixed another bug where `Accept-Encoding` was not properly adhered to.
|
* Fixed another bug where `Accept-Encoding` was not properly adhered to.
|
||||||
|
|
112
README.md
112
README.md
|
@ -1,5 +1,4 @@
|
||||||
# static
|
# static
|
||||||
|
|
||||||
[](https://pub.dartlang.org/packages/angel_static)
|
[](https://pub.dartlang.org/packages/angel_static)
|
||||||
[](https://travis-ci.org/angel-dart/static)
|
[](https://travis-ci.org/angel-dart/static)
|
||||||
|
|
||||||
|
@ -10,12 +9,11 @@ In `pubspec.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_static: ^1.2.0
|
angel_static: ^1.3.0
|
||||||
```
|
```
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
To serve files from a directory, your app needs to have a
|
To serve files from a directory, you need to create a `VirtualDirectory`.
|
||||||
`VirtualDirectory` mounted on it.
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
@ -23,27 +21,36 @@ import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_static/angel_static.dart';
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
final app = new Angel();
|
var app = new Angel();
|
||||||
|
|
||||||
// Normal static server
|
// Normal static server
|
||||||
await app.configure(new VirtualDirectory(source: new Directory('./public')));
|
var vDir = new VirtualDirectory(source: new Directory('./public'));
|
||||||
|
|
||||||
// Send Cache-Control, ETag, etc. as well
|
// Send Cache-Control, ETag, etc. as well
|
||||||
await app.configure(new CachingVirtualDirectory(source: new Directory('./public')));
|
var vDir = new CachingVirtualDirectory(source: new Directory('./public'));
|
||||||
|
|
||||||
|
// Mount the VirtualDirectory's request handler
|
||||||
|
app.use(vDir.handleRequest);
|
||||||
|
|
||||||
|
// Start your server!!!
|
||||||
await app.startServer();
|
await app.startServer();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Push State Example
|
# Push State
|
||||||
```dart
|
`VirtualDirectory` also exposes a `pushState` method that returns a
|
||||||
var vDir = new VirtualDirectory(...);
|
request handler that serves the file at a given path as a fallback, unless
|
||||||
var indexFile = new File.fromUri(vDir.source.uri.resolve('index.html'));
|
the user is requesting that file. This can be very useful for SPA's.
|
||||||
|
|
||||||
app.after.add((req, ResponseContext res) {
|
```dart
|
||||||
// Fallback to index.html on 404
|
// Create VirtualDirectory as well
|
||||||
return res.sendFile(indexFile);
|
var vDir = new CachingVirtualDirectory(...);
|
||||||
});
|
|
||||||
|
// Mount it
|
||||||
|
app.use(vDir.handleRequest);
|
||||||
|
|
||||||
|
// Fallback to index.html on 404
|
||||||
|
app.use(vDir.pushState('index.html'));
|
||||||
```
|
```
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
|
@ -54,80 +61,5 @@ The `VirtualDirectory` API accepts a few named parameters:
|
||||||
- **publicPath**: To serve index files, you need to specify the virtual path under which
|
- **publicPath**: To serve index files, you need to specify the virtual path under which
|
||||||
angel_static is serving your files. If you are not serving static files at the site root,
|
angel_static is serving your files. If you are not serving static files at the site root,
|
||||||
please include this.
|
please include this.
|
||||||
- **debug**: Print verbose debug output.
|
|
||||||
- **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`,
|
- **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.
|
||||||
- **streamToIO**: If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.. Default is `false`.
|
|
||||||
|
|
||||||
# Transformers
|
|
||||||
`angel_static` now supports *transformers*. Similarly to `pub serve`, or `package:build`, these
|
|
||||||
let you dynamically compile assets before sending them to users. For example, in development, you might
|
|
||||||
consider using transformers to compile CSS files, or to even replace `pub serve`.
|
|
||||||
Transformers are supported by `VirtualDirectory` and `CachingVirtualDirectory`.
|
|
||||||
|
|
||||||
To create a transformer:
|
|
||||||
```dart
|
|
||||||
class MinifierTransformer extends FileTransformer {
|
|
||||||
/// Use this to declare outputs, and indicate if your transformer
|
|
||||||
/// will compile a file.
|
|
||||||
@override
|
|
||||||
FileInfo declareOutput(FileInfo file) {
|
|
||||||
// For example, we might only want to minify HTML files.
|
|
||||||
if (!file.extensions.endsWith('.min.html'))
|
|
||||||
return null;
|
|
||||||
else return file.changeExtension('.min.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Actually compile the asset here.
|
|
||||||
@override
|
|
||||||
FutureOr<FileInfo> transform(FileInfo file) async {
|
|
||||||
return file
|
|
||||||
.changeExtension('.min.html')
|
|
||||||
.changeContent(
|
|
||||||
file.content
|
|
||||||
.transform(UTF8.decoder)
|
|
||||||
.transform(const LineSplitter()
|
|
||||||
.transform(UTF8.encoder))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To use it:
|
|
||||||
```dart
|
|
||||||
configureServer(Angel app) async {
|
|
||||||
var vDir = new CachingVirtualDirectory(
|
|
||||||
transformers: [new MinifierTransformer()]
|
|
||||||
);
|
|
||||||
await app.configure(vDir);
|
|
||||||
|
|
||||||
// It is suggested that you await `transformersLoaded`.
|
|
||||||
// Otherwise, you may receive 404's on paths that should send a compiled asset.
|
|
||||||
await vDir.transformersLoaded;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-building
|
|
||||||
You can pre-build all your assets with one command:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
configureServer(Angel app) async {
|
|
||||||
var vDir = new VirtualDirectory(transformers: [...]);
|
|
||||||
await app.configure(vDir);
|
|
||||||
|
|
||||||
// Build if in production
|
|
||||||
if (app.isProduction) {
|
|
||||||
await vDir.buildToDisk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## In Production
|
|
||||||
By default, transformers are disabled in production mode.
|
|
||||||
To force-enable them:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
configureServer(Angel app) async {
|
|
||||||
var vDir = new VirtualDirectory(useTransformersInProduction: true, transformers: [...]);
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,7 +1,4 @@
|
||||||
library angel_static;
|
library angel_static;
|
||||||
|
|
||||||
export 'src/cache.dart';
|
export 'src/cache.dart';
|
||||||
export 'src/file_info.dart';
|
|
||||||
export 'src/file_transformer.dart';
|
|
||||||
export 'src/serve_static.dart';
|
|
||||||
export 'src/virtual_directory.dart';
|
export 'src/virtual_directory.dart';
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:typed_data';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:async/async.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'file_info.dart';
|
|
||||||
import 'file_transformer.dart';
|
|
||||||
import 'virtual_directory.dart';
|
import 'virtual_directory.dart';
|
||||||
|
|
||||||
final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
||||||
|
@ -14,15 +13,9 @@ final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
||||||
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
|
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
|
||||||
String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT';
|
String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT';
|
||||||
|
|
||||||
/// Generates an ETag from the given buffer.
|
/// Generates a weak ETag from the given buffer.
|
||||||
String generateEtag(List<int> buf, {bool weak: true, Hash hash}) {
|
String weakEtag(List<int> buf) {
|
||||||
if (weak == false) {
|
return 'W/${buf.length}' + BASE64URL.encode(buf);
|
||||||
Hash h = hash ?? md5;
|
|
||||||
return new String.fromCharCodes(h.convert(buf).bytes);
|
|
||||||
} else {
|
|
||||||
// length + first 50 bytes as base64url
|
|
||||||
return 'W/${buf.length}' + BASE64URL.encode(buf.take(50).toList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string representation of the given [CacheAccessLevel].
|
/// Returns a string representation of the given [CacheAccessLevel].
|
||||||
|
@ -37,18 +30,13 @@ String accessLevelToString(CacheAccessLevel accessLevel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A static server plug-in that also sets `Cache-Control` headers.
|
/// A `VirtualDirectory` that also sets `Cache-Control` headers.
|
||||||
class CachingVirtualDirectory extends VirtualDirectory {
|
class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
final Map<String, String> _etags = {};
|
final Map<String, String> _etags = {};
|
||||||
|
|
||||||
/// Either `PUBLIC` or `PRIVATE`.
|
/// Either `PUBLIC` or `PRIVATE`.
|
||||||
final CacheAccessLevel accessLevel;
|
final CacheAccessLevel accessLevel;
|
||||||
|
|
||||||
/// Used to generate strong ETags, if [useWeakEtags] is false.
|
|
||||||
///
|
|
||||||
/// Default: `md5`.
|
|
||||||
final Hash hash;
|
|
||||||
|
|
||||||
/// If `true`, responses will always have `private, max-age=0` as their `Cache-Control` header.
|
/// If `true`, responses will always have `private, max-age=0` as their `Cache-Control` header.
|
||||||
final bool noCache;
|
final bool noCache;
|
||||||
|
|
||||||
|
@ -58,37 +46,27 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
/// If `true` (default), ETags will be computed and sent along with responses.
|
/// If `true` (default), ETags will be computed and sent along with responses.
|
||||||
final bool useEtags;
|
final bool useEtags;
|
||||||
|
|
||||||
/// If `false` (default: `true`), ETags will be generated via MD5 hash.
|
|
||||||
final bool useWeakEtags;
|
|
||||||
|
|
||||||
/// The `max-age` for `Cache-Control`.
|
/// The `max-age` for `Cache-Control`.
|
||||||
///
|
///
|
||||||
/// Set this to `null` to leave no `Expires` header on responses.
|
/// Set this to `null` to leave no `Expires` header on responses.
|
||||||
final int maxAge;
|
final int maxAge;
|
||||||
|
|
||||||
CachingVirtualDirectory(
|
CachingVirtualDirectory(Angel app, FileSystem fileSystem,
|
||||||
{this.accessLevel: CacheAccessLevel.PUBLIC,
|
{this.accessLevel: CacheAccessLevel.PUBLIC,
|
||||||
Directory source,
|
Directory source,
|
||||||
bool debug,
|
bool debug,
|
||||||
this.hash,
|
|
||||||
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,
|
||||||
this.useWeakEtags: true,
|
|
||||||
String publicPath,
|
String publicPath,
|
||||||
StaticFileCallback callback,
|
callback(File file, RequestContext req, ResponseContext res)})
|
||||||
bool streamToIO: false,
|
: super(app, fileSystem,
|
||||||
Iterable<FileTransformer> transformers: const []})
|
|
||||||
: super(
|
|
||||||
source: source,
|
source: source,
|
||||||
debug: debug == true,
|
|
||||||
indexFileNames: indexFileNames ?? ['index.html'],
|
indexFileNames: indexFileNames ?? ['index.html'],
|
||||||
publicPath: publicPath ?? '/',
|
publicPath: publicPath ?? '/',
|
||||||
callback: callback,
|
callback: callback);
|
||||||
streamToIO: streamToIO == true,
|
|
||||||
transformers: transformers ?? []);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> serveFile(
|
Future<bool> serveFile(
|
||||||
|
@ -100,17 +78,16 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
bool shouldNotCache = noCache == true;
|
bool shouldNotCache = noCache == true;
|
||||||
|
|
||||||
if (!shouldNotCache) {
|
if (!shouldNotCache) {
|
||||||
shouldNotCache =
|
shouldNotCache = req.headers.value('cache-control') == 'no-cache' ||
|
||||||
req.headers.value(HttpHeaders.CACHE_CONTROL) == 'no-cache' ||
|
req.headers.value('pragma') == 'no-cache';
|
||||||
req.headers.value(HttpHeaders.PRAGMA) == 'no-cache';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldNotCache) {
|
if (shouldNotCache) {
|
||||||
res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache';
|
res.headers['cache-control'] = 'private, max-age=0, no-cache';
|
||||||
return super.serveFile(file, stat, req, res);
|
return super.serveFile(file, stat, req, res);
|
||||||
} else {
|
} else {
|
||||||
if (useEtags == true) {
|
if (useEtags == true) {
|
||||||
var etags = req.headers[HttpHeaders.IF_NONE_MATCH];
|
var etags = req.headers['if-none-match'];
|
||||||
|
|
||||||
if (etags?.isNotEmpty == true) {
|
if (etags?.isNotEmpty == true) {
|
||||||
bool hasBeenModified = false;
|
bool hasBeenModified = false;
|
||||||
|
@ -125,7 +102,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBeenModified) {
|
if (hasBeenModified) {
|
||||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
res.statusCode = 304;
|
||||||
setCachedHeaders(stat.modified, req, res);
|
setCachedHeaders(stat.modified, req, res);
|
||||||
return new Future.value(false);
|
return new Future.value(false);
|
||||||
}
|
}
|
||||||
|
@ -137,11 +114,11 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
var ifModifiedSince = req.headers.ifModifiedSince;
|
var ifModifiedSince = req.headers.ifModifiedSince;
|
||||||
|
|
||||||
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
|
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
|
||||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
res.statusCode = 304;
|
||||||
setCachedHeaders(stat.modified, req, res);
|
setCachedHeaders(stat.modified, req, res);
|
||||||
|
|
||||||
if (_etags.containsKey(file.absolute.path))
|
if (_etags.containsKey(file.absolute.path))
|
||||||
res.headers[HttpHeaders.ETAG] = _etags[file.absolute.path];
|
res.headers['ETag'] = _etags[file.absolute.path];
|
||||||
|
|
||||||
return new Future.value(false);
|
return new Future.value(false);
|
||||||
}
|
}
|
||||||
|
@ -151,25 +128,38 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.readAsBytes().then((buf) {
|
var queue = new StreamQueue(file.openRead());
|
||||||
var etag = _etags[file.absolute.path] =
|
|
||||||
generateEtag(buf, weak: useWeakEtags != false, hash: hash);
|
return new Future<bool>(() async {
|
||||||
|
var buf = new Uint8List(50), hanging = <int>[];
|
||||||
|
int added = 0;
|
||||||
|
|
||||||
|
while (added < 50) {
|
||||||
|
var deficit = 50 - added;
|
||||||
|
var next = await queue.next;
|
||||||
|
|
||||||
|
for (int i = 0; i < deficit; i++) {
|
||||||
|
buf[added + i] = next[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.length > deficit) {
|
||||||
|
hanging.addAll(next.skip(deficit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var etag = _etags[file.absolute.path] = weakEtag(buf);
|
||||||
|
|
||||||
|
res.statusCode = 200;
|
||||||
res.headers
|
res.headers
|
||||||
..[HttpHeaders.ETAG] = etag
|
..['ETag'] = etag
|
||||||
..[HttpHeaders.CONTENT_TYPE] =
|
..['content-type'] =
|
||||||
lookupMimeType(file.path) ?? 'application/octet-stream';
|
lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||||
setCachedHeaders(stat.modified, req, res);
|
setCachedHeaders(stat.modified, req, res);
|
||||||
|
|
||||||
if (useWeakEtags == false) {
|
res.add(buf);
|
||||||
res
|
res.add(hanging);
|
||||||
..statusCode = 200
|
|
||||||
..willCloseItself = false
|
|
||||||
..buffer.add(buf)
|
|
||||||
..end();
|
|
||||||
return new Future.value(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.serveFile(file, stat, req, res);
|
return queue.rest.pipe(res).then((_) => false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,80 +169,14 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC);
|
var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC);
|
||||||
|
|
||||||
res.headers
|
res.headers
|
||||||
..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}'
|
..['cache-control'] = '$privacy, max-age=${maxAge ?? 0}'
|
||||||
..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(modified);
|
..['last-modified'] = formatDateForHttp(modified);
|
||||||
|
|
||||||
if (maxAge != null) {
|
if (maxAge != null) {
|
||||||
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0));
|
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0));
|
||||||
res.headers[HttpHeaders.EXPIRES] = formatDateForHttp(expiry);
|
res.headers['expires'] = formatDateForHttp(expiry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> serveAsset(
|
|
||||||
FileInfo fileInfo, RequestContext req, ResponseContext res) {
|
|
||||||
if (onlyInProduction == true && req.app.isProduction != true) {
|
|
||||||
return super.serveAsset(fileInfo, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool shouldNotCache = noCache == true;
|
|
||||||
|
|
||||||
if (!shouldNotCache) {
|
|
||||||
shouldNotCache =
|
|
||||||
req.headers.value(HttpHeaders.CACHE_CONTROL) == 'no-cache' ||
|
|
||||||
req.headers.value(HttpHeaders.PRAGMA) == 'no-cache';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldNotCache) {
|
|
||||||
res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache';
|
|
||||||
return super.serveAsset(fileInfo, req, res);
|
|
||||||
} else {
|
|
||||||
if (useEtags == true) {
|
|
||||||
var etags = req.headers[HttpHeaders.IF_NONE_MATCH];
|
|
||||||
|
|
||||||
if (etags?.isNotEmpty == true) {
|
|
||||||
bool hasBeenModified = false;
|
|
||||||
|
|
||||||
for (var etag in etags) {
|
|
||||||
if (etag == '*')
|
|
||||||
hasBeenModified = true;
|
|
||||||
else {
|
|
||||||
hasBeenModified = _etags.containsKey(fileInfo.filename) &&
|
|
||||||
_etags[fileInfo.filename] == etag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasBeenModified) {
|
|
||||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
|
||||||
setCachedHeaders(fileInfo.lastModified, req, res);
|
|
||||||
return new Future.value(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.headers.ifModifiedSince != null) {
|
|
||||||
try {
|
|
||||||
var ifModifiedSince = req.headers.ifModifiedSince;
|
|
||||||
|
|
||||||
if (fileInfo.lastModified != null &&
|
|
||||||
ifModifiedSince.compareTo(fileInfo.lastModified) >= 0) {
|
|
||||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
|
||||||
setCachedHeaders(fileInfo.lastModified, req, res);
|
|
||||||
|
|
||||||
if (_etags.containsKey(fileInfo.filename))
|
|
||||||
res.headers[HttpHeaders.ETAG] = _etags[fileInfo.filename];
|
|
||||||
|
|
||||||
return new Future.value(false);
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
throw new AngelHttpException.badRequest(
|
|
||||||
message: 'Invalid date for If-Modified-Since header.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.serveAsset(fileInfo, req, res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CacheAccessLevel { PUBLIC, PRIVATE }
|
enum CacheAccessLevel { PUBLIC, PRIVATE }
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:mime/mime.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
/// Represents information about a file, regardless of whether it exists in the filesystem
|
|
||||||
/// or in memory.
|
|
||||||
abstract class FileInfo {
|
|
||||||
/// Returns the content of the file.
|
|
||||||
Stream<List<int>> get content;
|
|
||||||
|
|
||||||
/// This file's extension.
|
|
||||||
String get extension;
|
|
||||||
|
|
||||||
/// The name of the file.
|
|
||||||
String get filename;
|
|
||||||
|
|
||||||
/// The time when this file was last modified.
|
|
||||||
DateTime get lastModified;
|
|
||||||
|
|
||||||
/// The file's MIME type.
|
|
||||||
String get mimeType;
|
|
||||||
|
|
||||||
/// Creates a [FileInfo] instance representing a physical file.
|
|
||||||
factory FileInfo.fromFile(File file) => new _FileInfoImpl(
|
|
||||||
() => file.openRead(),
|
|
||||||
file.absolute.path,
|
|
||||||
lookupMimeType(file.path) ?? 'application/octet-stream' ?? 'application/octet-stream',
|
|
||||||
file.statSync().modified);
|
|
||||||
|
|
||||||
/// Creates a [FileInfo] describing a file that might not even exists to begin with.
|
|
||||||
factory FileInfo.hypothetical(String hypotheticalFileName) =>
|
|
||||||
new _FileInfoImpl(null, hypotheticalFileName,
|
|
||||||
lookupMimeType(hypotheticalFileName) ?? 'application/octet-stream', null);
|
|
||||||
|
|
||||||
/// Returns an identical instance, but with a different filename.
|
|
||||||
FileInfo changeFilename(String newFilename);
|
|
||||||
|
|
||||||
/// Returns an identical instance, but with a different extension.
|
|
||||||
FileInfo changeExtension(String newExtension);
|
|
||||||
|
|
||||||
/// Returns an identical instance, but with a different content.
|
|
||||||
FileInfo changeContent(Stream<List<int>> newContent);
|
|
||||||
|
|
||||||
/// Returns an identical instance, but with differnet content, set to the given String.
|
|
||||||
FileInfo changeText(String newText, {Encoding encoding: UTF8});
|
|
||||||
|
|
||||||
/// Returns an identical instance, but with a different MIME type.
|
|
||||||
FileInfo changeMimeType(String newMimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FileInfoImpl implements FileInfo {
|
|
||||||
@override
|
|
||||||
Stream<List<int>> get content => getContent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String filename, mimeType;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final DateTime lastModified;
|
|
||||||
|
|
||||||
final Function getContent;
|
|
||||||
|
|
||||||
_FileInfoImpl(Stream<List<int>> this.getContent(), this.filename,
|
|
||||||
this.mimeType, this.lastModified);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get extension => p.extension(filename);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo changeFilename(String newFilename) => new _FileInfoImpl(
|
|
||||||
getContent,
|
|
||||||
newFilename,
|
|
||||||
lookupMimeType(newFilename) ?? mimeType ?? 'application/octet-stream',
|
|
||||||
lastModified);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo changeExtension(String newExtension) =>
|
|
||||||
changeFilename(p.withoutExtension(filename) + newExtension);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo changeContent(Stream<List<int>> newContent) =>
|
|
||||||
new _FileInfoImpl(() => newContent, filename, mimeType, lastModified);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo changeText(String newText, {Encoding encoding: UTF8}) =>
|
|
||||||
changeContent(new Stream<List<int>>.fromIterable(
|
|
||||||
[(encoding ?? UTF8).encode(newText)]));
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo changeMimeType(String newMimeType) =>
|
|
||||||
new _FileInfoImpl(getContent, filename, newMimeType, lastModified);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'file_info.dart';
|
|
||||||
|
|
||||||
/// A class capable of transforming inputs into new outputs, on-the-fly.
|
|
||||||
///
|
|
||||||
/// Ex. A transformer that compiles Stylus files.
|
|
||||||
abstract class FileTransformer {
|
|
||||||
/// Changes the name of a [file] into what it will be once it is transformed.
|
|
||||||
///
|
|
||||||
/// If this transformer will not be consume the file, then return `null`.
|
|
||||||
FileInfo declareOutput(FileInfo file);
|
|
||||||
|
|
||||||
/// Transforms an input [file] into a new representation.
|
|
||||||
FutureOr<FileInfo> transform(FileInfo file);
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
@deprecated
|
|
||||||
RequestMiddleware serveStatic(
|
|
||||||
{Directory sourceDirectory,
|
|
||||||
List<String> indexFileNames: const ['index.html'],
|
|
||||||
String virtualRoot: '/'}) {
|
|
||||||
throw new Exception(
|
|
||||||
'The `serveStatic` API is now deprecated. Please update your application to use the new `VirtualDirectory` API.');
|
|
||||||
}
|
|
|
@ -1,15 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_route/angel_route.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:cli_util/cli_logging.dart' as cli;
|
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:pool/pool.dart';
|
|
||||||
import 'package:watcher/watcher.dart';
|
|
||||||
import 'file_info.dart';
|
|
||||||
import 'file_transformer.dart';
|
|
||||||
|
|
||||||
typedef StaticFileCallback(File file, RequestContext req, ResponseContext res);
|
|
||||||
|
|
||||||
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
@ -31,24 +23,18 @@ String _pathify(String path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A static server plug-in.
|
/// A static server plug-in.
|
||||||
class VirtualDirectory implements AngelPlugin {
|
class VirtualDirectory {
|
||||||
final bool debug;
|
|
||||||
Angel _app;
|
|
||||||
String _prefix;
|
String _prefix;
|
||||||
Directory _source;
|
Directory _source;
|
||||||
final Completer<Map<String, String>> _transformerLoad =
|
|
||||||
new Completer<Map<String, String>>();
|
|
||||||
final Map<String, String> _transformerMap = {};
|
|
||||||
Pool _transformerMapMutex;
|
|
||||||
final List<FileTransformer> _transformers = [];
|
|
||||||
List<FileTransformer> _transformersCache;
|
|
||||||
StreamSubscription<WatchEvent> _watch;
|
|
||||||
|
|
||||||
/// The directory to serve files from.
|
/// The directory to serve files from.
|
||||||
Directory get source => _source;
|
Directory get source => _source;
|
||||||
|
|
||||||
/// An optional callback to run before serving files.
|
/// An optional callback to run before serving files.
|
||||||
final StaticFileCallback callback;
|
final Function(File file, RequestContext req, ResponseContext res) callback;
|
||||||
|
|
||||||
|
final Angel app;
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
/// Filenames to be resolved within directories as indices.
|
/// Filenames to be resolved within directories as indices.
|
||||||
final Iterable<String> indexFileNames;
|
final Iterable<String> indexFileNames;
|
||||||
|
@ -56,125 +42,46 @@ class VirtualDirectory implements AngelPlugin {
|
||||||
/// An optional public path to map requests to.
|
/// An optional public path to map requests to.
|
||||||
final String publicPath;
|
final String publicPath;
|
||||||
|
|
||||||
/// If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.
|
VirtualDirectory(this.app, this.fileSystem,
|
||||||
final bool streamToIO;
|
|
||||||
|
|
||||||
/// A collection of [FileTransformer] instances that will be used to dynamically compile assets, if any. **READ-ONLY**.
|
|
||||||
List<FileTransformer> get transformers =>
|
|
||||||
_transformersCache ??
|
|
||||||
(_transformersCache =
|
|
||||||
new List<FileTransformer>.unmodifiable(_transformers));
|
|
||||||
|
|
||||||
/// If `true` (default: `false`), then transformers will not be disabled in production.
|
|
||||||
final bool useTransformersInProduction;
|
|
||||||
|
|
||||||
/// Completes when all [transformers] are loaded.
|
|
||||||
Future<Map<String, String>> get transformersLoaded {
|
|
||||||
if ((!_app.isProduction || useTransformersInProduction == true) &&
|
|
||||||
!_transformerLoad.isCompleted)
|
|
||||||
return _transformerLoad.future;
|
|
||||||
else
|
|
||||||
return new Future.value(_transformerMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualDirectory(
|
|
||||||
{Directory source,
|
{Directory source,
|
||||||
this.debug: false,
|
|
||||||
this.indexFileNames: const ['index.html'],
|
this.indexFileNames: const ['index.html'],
|
||||||
this.publicPath: '/',
|
this.publicPath: '/',
|
||||||
this.callback,
|
this.callback}) {
|
||||||
this.streamToIO: false,
|
|
||||||
this.useTransformersInProduction: false,
|
|
||||||
Iterable<FileTransformer> transformers: const []}) {
|
|
||||||
_prefix = publicPath.replaceAll(_straySlashes, '');
|
_prefix = publicPath.replaceAll(_straySlashes, '');
|
||||||
this._transformers.addAll(transformers ?? []);
|
|
||||||
|
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
_source = source;
|
_source = source;
|
||||||
} else {
|
} else {
|
||||||
String dirPath = Platform.environment['ANGEL_ENV'] == 'production'
|
String dirPath = app.isProduction ? './build/web' : './web';
|
||||||
? './build/web'
|
_source = fileSystem.directory(dirPath);
|
||||||
: './web';
|
|
||||||
_source = new Directory(dirPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
call(Angel app) async {
|
/// Responds to incoming HTTP requests.
|
||||||
serve(_app = app);
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
||||||
app.justBeforeStop.add((_) => close());
|
if (req.method != 'GET') return new Future<bool>.value(true);
|
||||||
|
var path = req.path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
|
if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix))
|
||||||
|
return new Future<bool>.value(true);
|
||||||
|
|
||||||
|
return servePath(path, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
void serve(Router router) {
|
/// A handler that serves the file at the given path, unless the user has requested that path.
|
||||||
// _printDebug('Source directory: ${source.absolute.path}');
|
RequestMiddleware pushState(String path) {
|
||||||
// _printDebug('Public path prefix: "$_prefix"');
|
var vPath = path.replaceAll(_straySlashes, '');
|
||||||
//router.get('$publicPath/*',
|
if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
|
||||||
router.get('$_prefix/*', (RequestContext req, ResponseContext res) async {
|
|
||||||
|
return (RequestContext req, ResponseContext res) {
|
||||||
var path = req.path.replaceAll(_straySlashes, '');
|
var path = req.path.replaceAll(_straySlashes, '');
|
||||||
return servePath(path, req, res);
|
if (path == vPath) return new Future<bool>.value(true);
|
||||||
});
|
return servePath(vPath, req, res);
|
||||||
|
};
|
||||||
if ((!_app.isProduction || useTransformersInProduction == true) &&
|
|
||||||
_transformers.isNotEmpty) {
|
|
||||||
// Create mutex, and watch for file changes
|
|
||||||
_transformerMapMutex = new Pool(1);
|
|
||||||
_transformerMapMutex.request().then((resx) {
|
|
||||||
_buildTransformerMap().then((_) => resx.release());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close() async {
|
/// Writes the file at the given virtual [path] to a response.
|
||||||
if (!_transformerLoad.isCompleted && _transformers.isNotEmpty) {
|
Future<bool> servePath(
|
||||||
_transformerLoad.completeError(new StateError(
|
String path, RequestContext req, ResponseContext res) async {
|
||||||
'VirtualDirectory was closed before all transformers loaded.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
_transformerMapMutex?.close();
|
|
||||||
_watch?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _buildTransformerMap() async {
|
|
||||||
print('VirtualDirectory is loading transformers...');
|
|
||||||
|
|
||||||
await for (var entity in source.list(recursive: true)) {
|
|
||||||
if (entity is File) {
|
|
||||||
_applyTransformers(entity.absolute.uri.toFilePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('VirtualDirectory finished loading transformers.');
|
|
||||||
_transformerLoad.complete(_transformerMap);
|
|
||||||
|
|
||||||
_watch =
|
|
||||||
new DirectoryWatcher(source.absolute.path).events.listen((e) async {
|
|
||||||
_transformerMapMutex.withResource(() {
|
|
||||||
_applyTransformers(e.path);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _applyTransformers(String originalAbsolutePath) {
|
|
||||||
FileInfo file = new FileInfo.fromFile(new File(originalAbsolutePath));
|
|
||||||
FileInfo outFile = file;
|
|
||||||
var wasClaimed = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
wasClaimed = false;
|
|
||||||
for (var transformer in _transformers) {
|
|
||||||
var claimed = transformer.declareOutput(outFile);
|
|
||||||
if (claimed != null) {
|
|
||||||
outFile = claimed;
|
|
||||||
wasClaimed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (wasClaimed);
|
|
||||||
|
|
||||||
var finalName = outFile.filename;
|
|
||||||
if (finalName?.isNotEmpty == true && outFile != file)
|
|
||||||
_transformerMap[finalName] = originalAbsolutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
servePath(String path, RequestContext req, ResponseContext res) async {
|
|
||||||
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
|
||||||
|
@ -185,63 +92,41 @@ class VirtualDirectory implements AngelPlugin {
|
||||||
path = path.replaceAll(_straySlashes, '');
|
path = path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
var absolute = source.absolute.uri.resolve(path).toFilePath();
|
var absolute = source.absolute.uri.resolve(path).toFilePath();
|
||||||
var stat = await FileStat.stat(absolute);
|
var stat = await fileSystem.stat(absolute);
|
||||||
return await serveStat(absolute, stat, req, res);
|
return await serveStat(absolute, stat, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the file at the path given by the [stat] to a response.
|
||||||
Future<bool> serveStat(String absolute, FileStat stat, RequestContext req,
|
Future<bool> serveStat(String absolute, FileStat stat, RequestContext req,
|
||||||
ResponseContext res) async {
|
ResponseContext res) async {
|
||||||
if (stat.type == FileSystemEntityType.DIRECTORY)
|
if (stat.type == FileSystemEntityType.DIRECTORY)
|
||||||
return await serveDirectory(new Directory(absolute), stat, req, res);
|
return await serveDirectory(
|
||||||
|
fileSystem.directory(absolute), stat, req, res);
|
||||||
else if (stat.type == FileSystemEntityType.FILE)
|
else if (stat.type == FileSystemEntityType.FILE)
|
||||||
return await serveFile(new File(absolute), stat, req, res);
|
return await serveFile(fileSystem.file(absolute), stat, req, res);
|
||||||
else if (stat.type == FileSystemEntityType.LINK) {
|
else if (stat.type == FileSystemEntityType.LINK) {
|
||||||
var link = new Link(absolute);
|
var link = fileSystem.link(absolute);
|
||||||
return await servePath(await link.resolveSymbolicLinks(), req, res);
|
return await servePath(await link.resolveSymbolicLinks(), req, res);
|
||||||
} else if (_transformerMapMutex != null) {
|
|
||||||
var resx = await _transformerMapMutex.request();
|
|
||||||
if (!_transformerMap.containsKey(absolute)) return true;
|
|
||||||
var sourceFile = new File(_transformerMap[absolute]);
|
|
||||||
resx.release();
|
|
||||||
if (!await sourceFile.exists())
|
|
||||||
return true;
|
|
||||||
else {
|
|
||||||
return await serveAsset(new FileInfo.fromFile(sourceFile), req, res);
|
|
||||||
}
|
|
||||||
} else
|
} else
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serves the index file of a [directory], if it exists.
|
||||||
Future<bool> serveDirectory(Directory directory, FileStat stat,
|
Future<bool> serveDirectory(Directory directory, FileStat stat,
|
||||||
RequestContext req, ResponseContext res) async {
|
RequestContext req, ResponseContext res) async {
|
||||||
for (String indexFileName in indexFileNames) {
|
for (String indexFileName in indexFileNames) {
|
||||||
final index =
|
final index =
|
||||||
new File.fromUri(directory.absolute.uri.resolve(indexFileName));
|
fileSystem.file(directory.absolute.uri.resolve(indexFileName));
|
||||||
if (await index.exists()) {
|
if (await index.exists()) {
|
||||||
return await serveFile(index, stat, req, res);
|
return await serveFile(index, stat, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to compile an asset
|
|
||||||
if (_transformerMap.isNotEmpty &&
|
|
||||||
_transformerMap.containsKey(index.absolute.path)) {
|
|
||||||
return await serveAsset(
|
|
||||||
new FileInfo.fromFile(
|
|
||||||
new File(_transformerMap[index.absolute.path])),
|
|
||||||
req,
|
|
||||||
res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _acceptsGzip(RequestContext req) {
|
|
||||||
var h = req.headers.value(HttpHeaders.ACCEPT_ENCODING)?.toLowerCase();
|
|
||||||
return h?.contains('*') == true || h?.contains('gzip') == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
|
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
|
||||||
var value = req.headers.value(HttpHeaders.ACCEPT);
|
var value = req.headers.value('accept');
|
||||||
bool acceptable = value == null ||
|
bool acceptable = value == null ||
|
||||||
value?.isNotEmpty != true ||
|
value?.isNotEmpty != true ||
|
||||||
(mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) ||
|
(mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) ||
|
||||||
|
@ -250,14 +135,13 @@ class VirtualDirectory implements AngelPlugin {
|
||||||
throw new AngelHttpException(
|
throw new AngelHttpException(
|
||||||
new UnsupportedError(
|
new UnsupportedError(
|
||||||
'Client requested $value, but server wanted to send $mimeType.'),
|
'Client requested $value, but server wanted to send $mimeType.'),
|
||||||
statusCode: HttpStatus.NOT_ACCEPTABLE,
|
statusCode: 406,
|
||||||
message: '406 Not Acceptable');
|
message: '406 Not Acceptable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the contents of a file to a response.
|
||||||
Future<bool> serveFile(
|
Future<bool> serveFile(
|
||||||
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
||||||
// _printDebug('Sending file ${file.absolute.path}...');
|
|
||||||
// _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path) ?? 'application/octet-stream'}');
|
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
|
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
|
@ -268,178 +152,9 @@ class VirtualDirectory implements AngelPlugin {
|
||||||
|
|
||||||
var type = lookupMimeType(file.path) ?? 'application/octet-stream';
|
var type = lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||||
_ensureContentTypeAllowed(type, req);
|
_ensureContentTypeAllowed(type, req);
|
||||||
res.headers[HttpHeaders.CONTENT_TYPE] = type;
|
res.headers['content-type'] = type;
|
||||||
|
|
||||||
if (streamToIO == true) {
|
await file.openRead().pipe(res);
|
||||||
res
|
|
||||||
..io.headers.set(HttpHeaders.CONTENT_TYPE,
|
|
||||||
lookupMimeType(file.path) ?? 'application/octet-stream')
|
|
||||||
..end()
|
|
||||||
..willCloseItself = true;
|
|
||||||
|
|
||||||
if (_acceptsGzip(req))
|
|
||||||
res.io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip');
|
|
||||||
|
|
||||||
Stream<List<int>> stream = _acceptsGzip(req)
|
|
||||||
? file.openRead().transform(GZIP.encoder)
|
|
||||||
: file.openRead();
|
|
||||||
await stream.pipe(res.io);
|
|
||||||
} else {
|
|
||||||
if (_acceptsGzip(req)) {
|
|
||||||
res.io.headers
|
|
||||||
..set(HttpHeaders.CONTENT_TYPE,
|
|
||||||
lookupMimeType(file.path) ?? 'application/octet-stream')
|
|
||||||
..set(HttpHeaders.CONTENT_ENCODING, 'gzip');
|
|
||||||
await file.openRead().transform(GZIP.encoder).forEach(res.buffer.add);
|
|
||||||
res.end();
|
|
||||||
} else
|
|
||||||
await res.sendFile(file);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> serveAsset(
|
|
||||||
FileInfo fileInfo, RequestContext req, ResponseContext res) async {
|
|
||||||
var file = await compileAsset(fileInfo);
|
|
||||||
if (file == null) return true;
|
|
||||||
_ensureContentTypeAllowed(file.mimeType, req);
|
|
||||||
res.headers[HttpHeaders.CONTENT_TYPE] = file.mimeType;
|
|
||||||
res.statusCode = 200;
|
|
||||||
|
|
||||||
if (streamToIO == true) {
|
|
||||||
res
|
|
||||||
..statusCode = 200
|
|
||||||
..io.headers.set(HttpHeaders.CONTENT_TYPE,
|
|
||||||
lookupMimeType(file.filename) ?? 'application/octet-stream')
|
|
||||||
..end()
|
|
||||||
..willCloseItself = true;
|
|
||||||
|
|
||||||
if (_acceptsGzip(req))
|
|
||||||
res.io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip');
|
|
||||||
|
|
||||||
Stream<List<int>> stream = _acceptsGzip(req)
|
|
||||||
? file.content.transform(GZIP.encoder)
|
|
||||||
: file.content;
|
|
||||||
await stream.pipe(res.io);
|
|
||||||
} else {
|
|
||||||
if (_acceptsGzip(req)) {
|
|
||||||
res.io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip');
|
|
||||||
await file.content.transform(GZIP.encoder).forEach(res.buffer.add);
|
|
||||||
} else
|
|
||||||
await file.content.forEach(res.buffer.add);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies all [_transformers] to an input [file], if any.
|
|
||||||
Future<FileInfo> compileAsset(FileInfo file) async {
|
|
||||||
var iterations = 0;
|
|
||||||
FileInfo result = file;
|
|
||||||
bool wasTransformed = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
wasTransformed = false;
|
|
||||||
String originalName = file.filename;
|
|
||||||
for (var transformer in _transformers) {
|
|
||||||
if (++iterations >= 100) {
|
|
||||||
print('VirtualDirectory has tried 100 times to compile ${file
|
|
||||||
.filename}. Perhaps one of your transformers is not changing the output file\'s extension.');
|
|
||||||
throw new AngelHttpException(new StackOverflowError(),
|
|
||||||
statusCode: 500);
|
|
||||||
} else if (iterations < 100) iterations++;
|
|
||||||
var claimed = transformer.declareOutput(result);
|
|
||||||
if (claimed != null) {
|
|
||||||
result = await transformer.transform(result);
|
|
||||||
wasTransformed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't re-compile infinitely...
|
|
||||||
if (result.filename == originalName) wasTransformed = false;
|
|
||||||
} while (wasTransformed);
|
|
||||||
|
|
||||||
return result == file ? null : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds assets to disk using [transformers].
|
|
||||||
Future buildToDisk() async {
|
|
||||||
var l = new cli.Logger.standard();
|
|
||||||
print('Building assets in "${source.absolute.path}"...');
|
|
||||||
|
|
||||||
await for (var entity in source.list(recursive: true)) {
|
|
||||||
if (entity is File) {
|
|
||||||
var p = l.progress('Building "${entity.absolute.path}"');
|
|
||||||
|
|
||||||
try {
|
|
||||||
var asset = new FileInfo.fromFile(entity);
|
|
||||||
var compiled = await compileAsset(asset);
|
|
||||||
if (compiled == null)
|
|
||||||
p.finish(
|
|
||||||
message: '"${entity.absolute
|
|
||||||
.path}" did not require compilation; skipping it.');
|
|
||||||
else {
|
|
||||||
var outFile = new File(compiled.filename);
|
|
||||||
if (!await outFile.exists()) await outFile.create(recursive: true);
|
|
||||||
var sink = outFile.openWrite();
|
|
||||||
await compiled.content.pipe(sink);
|
|
||||||
p.finish(
|
|
||||||
message:
|
|
||||||
'Built "${entity.absolute.path}" to "${compiled.filename}".',
|
|
||||||
showTiming: true);
|
|
||||||
}
|
|
||||||
} on AngelHttpException {
|
|
||||||
// Ignore 500
|
|
||||||
} catch (e, st) {
|
|
||||||
p.finish(message: 'Failed to build "${entity.absolute.path}".');
|
|
||||||
stderr..writeln(e)..writeln(st);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Build of assets in "${source.absolute.path}" complete.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes any pre-built assets.
|
|
||||||
Future cleanFromDisk() async {
|
|
||||||
var l = new cli.Logger.standard();
|
|
||||||
print('Cleaning assets in "${source.absolute.path}"...');
|
|
||||||
|
|
||||||
await for (var entity in source.list(recursive: true)) {
|
|
||||||
if (entity is File) {
|
|
||||||
var p = l.progress('Checking "${entity.absolute.path}"');
|
|
||||||
|
|
||||||
try {
|
|
||||||
var asset = new FileInfo.fromFile(entity);
|
|
||||||
var compiled = await compileAsset(asset);
|
|
||||||
if (compiled == null)
|
|
||||||
p.finish(
|
|
||||||
message: '"${entity.absolute
|
|
||||||
.path}" did not require compilation; skipping it.');
|
|
||||||
else {
|
|
||||||
var outFile = new File(compiled.filename);
|
|
||||||
if (await outFile.exists()) {
|
|
||||||
await outFile.delete();
|
|
||||||
p.finish(
|
|
||||||
message: 'Deleted "${compiled
|
|
||||||
.filename}", which was the output of "${entity.absolute
|
|
||||||
.path}".',
|
|
||||||
showTiming: true);
|
|
||||||
} else {
|
|
||||||
p.finish(
|
|
||||||
message:
|
|
||||||
'Output "${compiled.filename}" of "${entity.absolute.path}" does not exist.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on AngelHttpException {
|
|
||||||
// Ignore 500
|
|
||||||
} catch (e, st) {
|
|
||||||
p.finish(message: 'Failed to delete "${entity.absolute.path}".');
|
|
||||||
stderr..writeln(e)..writeln(st);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Purge of assets in "${source.absolute.path}" complete.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
10
pubspec.yaml
10
pubspec.yaml
|
@ -4,18 +4,14 @@ environment:
|
||||||
sdk: ">=1.19.0"
|
sdk: ">=1.19.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: 1.2.5
|
version: 1.3.0-alpha
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ^1.0.0-dev
|
angel_framework: ^1.1.0-alpha
|
||||||
cli_util: ^0.1.1
|
file: ^2.0.0
|
||||||
crypto: ^2.0.0
|
|
||||||
intl: ">=0.0.0 <1.0.0"
|
intl: ">=0.0.0 <1.0.0"
|
||||||
mime: ^0.9.3
|
mime: ^0.9.3
|
||||||
path: ^1.4.2
|
path: ^1.4.2
|
||||||
pool: ^1.0.0
|
|
||||||
watcher: ^0.9.7
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
angel_diagnostics: ^1.0.0
|
|
||||||
angel_test: ^1.0.0
|
angel_test: ^1.0.0
|
||||||
http: ^0.11.3
|
http: ^0.11.3
|
||||||
mustache4dart: ^1.1.0
|
mustache4dart: ^1.1.0
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_diagnostics/angel_diagnostics.dart';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_static/angel_static.dart';
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
import 'package:http/http.dart' show Client;
|
import 'package:http/http.dart' show Client;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
Angel app;
|
Angel app;
|
||||||
Directory testDir = new Directory('test');
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
String url;
|
String url;
|
||||||
Client client = new Client();
|
Client client = new Client();
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
app = new Angel();
|
app = new Angel();
|
||||||
|
app.logger = new Logger('angel')..onRecord.listen(print);
|
||||||
|
|
||||||
await app.configure(new VirtualDirectory(
|
app.use(
|
||||||
debug: true,
|
new VirtualDirectory(app, const LocalFileSystem(),
|
||||||
source: testDir,
|
source: testDir,
|
||||||
publicPath: '/virtual',
|
publicPath: '/virtual',
|
||||||
indexFileNames: ['index.txt']));
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
await app.configure(new VirtualDirectory(
|
app.use(
|
||||||
debug: true,
|
new VirtualDirectory(app, const LocalFileSystem(),
|
||||||
source: testDir,
|
source: testDir,
|
||||||
streamToIO: true,
|
indexFileNames: ['index.php', 'index.txt']).handleRequest,
|
||||||
indexFileNames: ['index.php', 'index.txt']));
|
);
|
||||||
|
|
||||||
app.after.add('Fallback');
|
app.use('Fallback');
|
||||||
|
|
||||||
app.dumpTree(showMatchers: true);
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
await app.configure(logRequests());
|
var server = await app.startServer();
|
||||||
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
url = "http://${server.address.host}:${server.port}";
|
||||||
url = "http://${app.httpServer.address.host}:${app.httpServer.port}";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
@ -42,13 +44,13 @@ main() {
|
||||||
test('can serve files, with correct Content-Type', () async {
|
test('can serve files, with correct Content-Type', () async {
|
||||||
var response = await client.get("$url/sample.txt");
|
var response = await client.get("$url/sample.txt");
|
||||||
expect(response.body, equals("Hello world"));
|
expect(response.body, equals("Hello world"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain"));
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can serve child directories', () async {
|
test('can serve child directories', () async {
|
||||||
var response = await client.get("$url/nested");
|
var response = await client.get("$url/nested");
|
||||||
expect(response.body, equals("Bird"));
|
expect(response.body, equals("Bird"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain"));
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('non-existent files are skipped', () async {
|
test('non-existent files are skipped', () async {
|
||||||
|
@ -68,7 +70,7 @@ main() {
|
||||||
|
|
||||||
test('chrome accept', () async {
|
test('chrome accept', () async {
|
||||||
var response = await client.get("$url/virtual", headers: {
|
var response = await client.get("$url/virtual", headers: {
|
||||||
HttpHeaders.ACCEPT:
|
'accept':
|
||||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
||||||
});
|
});
|
||||||
expect(response.body, equals("index!"));
|
expect(response.body, equals("index!"));
|
||||||
|
@ -76,25 +78,25 @@ main() {
|
||||||
|
|
||||||
test('can gzip: just gzip', () async {
|
test('can gzip: just gzip', () async {
|
||||||
var response = await client
|
var response = await client
|
||||||
.get("$url/sample.txt", headers: {HttpHeaders.ACCEPT_ENCODING: 'gzip'});
|
.get("$url/sample.txt", headers: {'accept-encoding': 'gzip'});
|
||||||
expect(response.body, equals("Hello world"));
|
expect(response.body, equals("Hello world"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain"));
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_ENCODING], 'gzip');
|
expect(response.headers['content-encoding'], 'gzip');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can gzip: wildcard', () async {
|
test('can gzip: wildcard', () async {
|
||||||
var response = await client
|
var response = await client
|
||||||
.get("$url/sample.txt", headers: {HttpHeaders.ACCEPT_ENCODING: 'foo, *'});
|
.get("$url/sample.txt", headers: {'accept-encoding': 'foo, *'});
|
||||||
expect(response.body, equals("Hello world"));
|
expect(response.body, equals("Hello world"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain"));
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_ENCODING], 'gzip');
|
expect(response.headers['content-encoding'], 'gzip');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can gzip: gzip and friends', () async {
|
test('can gzip: gzip and friends', () async {
|
||||||
var response = await client
|
var response = await client.get("$url/sample.txt",
|
||||||
.get("$url/sample.txt", headers: {HttpHeaders.ACCEPT_ENCODING: 'gzip, deflate, br'});
|
headers: {'accept-encoding': 'gzip, deflate, br'});
|
||||||
expect(response.body, equals("Hello world"));
|
expect(response.body, equals("Hello world"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain"));
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
expect(response.headers[HttpHeaders.CONTENT_ENCODING], 'gzip');
|
expect(response.headers['content-encoding'], 'gzip');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_static/angel_static.dart';
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
Angel app;
|
Angel app;
|
||||||
Directory testDir = new Directory('test');
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
app = new Angel(debug: true);
|
app = new Angel();
|
||||||
|
|
||||||
await app.configure(new CachingVirtualDirectory(
|
app.use(
|
||||||
source: testDir,
|
new CachingVirtualDirectory(app, const LocalFileSystem(),
|
||||||
maxAge: 350,
|
source: testDir,
|
||||||
onlyInProduction: false,
|
maxAge: 350,
|
||||||
// useWeakEtags: false,
|
onlyInProduction: false,
|
||||||
//publicPath: '/virtual',
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
indexFileNames: ['index.txt']));
|
);
|
||||||
|
|
||||||
app.get('*', 'Fallback');
|
app.get('*', 'Fallback');
|
||||||
|
|
||||||
app.dumpTree(showMatchers: true);
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
var server = await app.startServer();
|
||||||
print('Open at http://${app.httpServer.address.host}:${app.httpServer.port}');
|
print('Open at http://${server.address.host}:${server.port}');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,33 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_static/angel_static.dart';
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
import 'package:http/http.dart' show Client;
|
import 'package:http/http.dart' show Client;
|
||||||
import 'package:matcher/matcher.dart';
|
import 'package:matcher/matcher.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
Angel app;
|
Angel app;
|
||||||
Directory testDir = new Directory('test');
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
String url;
|
String url;
|
||||||
Client client = new Client();
|
Client client = new Client();
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
app = new Angel(debug: true);
|
app = new Angel();
|
||||||
|
|
||||||
await app.configure(new CachingVirtualDirectory(
|
app.use(
|
||||||
source: testDir, maxAge: 350, onlyInProduction: false,
|
new CachingVirtualDirectory(app, const LocalFileSystem(),
|
||||||
//publicPath: '/virtual',
|
source: testDir, maxAge: 350, onlyInProduction: false,
|
||||||
indexFileNames: ['index.txt']));
|
//publicPath: '/virtual',
|
||||||
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
app.get('*', 'Fallback');
|
app.get('*', 'Fallback');
|
||||||
|
|
||||||
app.dumpTree(showMatchers: true);
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
var server = await app.startServer();
|
||||||
url = "http://${app.httpServer.address.host}:${app.httpServer.port}";
|
url = "http://${server.address.host}:${server.port}";
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
@ -40,19 +43,14 @@ main() {
|
||||||
|
|
||||||
expect(response.statusCode, equals(200));
|
expect(response.statusCode, equals(200));
|
||||||
expect(
|
expect(
|
||||||
[
|
['ETag', 'cache-control', 'expires', 'last-modified'],
|
||||||
HttpHeaders.ETAG,
|
|
||||||
HttpHeaders.CACHE_CONTROL,
|
|
||||||
HttpHeaders.EXPIRES,
|
|
||||||
HttpHeaders.LAST_MODIFIED
|
|
||||||
],
|
|
||||||
everyElement(predicate(
|
everyElement(predicate(
|
||||||
response.headers.containsKey, 'contained in response headers')));
|
response.headers.containsKey, 'contained in response headers')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('if-modified-since', () async {
|
test('if-modified-since', () async {
|
||||||
var response = await client.get("$url", headers: {
|
var response = await client.get("$url", headers: {
|
||||||
HttpHeaders.IF_MODIFIED_SINCE:
|
'if-modified-since':
|
||||||
formatDateForHttp(new DateTime.now().add(new Duration(days: 365)))
|
formatDateForHttp(new DateTime.now().add(new Duration(days: 365)))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,11 +58,7 @@ main() {
|
||||||
|
|
||||||
expect(response.statusCode, equals(304));
|
expect(response.statusCode, equals(304));
|
||||||
expect(
|
expect(
|
||||||
[
|
['cache-control', 'expires', 'last-modified'],
|
||||||
HttpHeaders.CACHE_CONTROL,
|
|
||||||
HttpHeaders.EXPIRES,
|
|
||||||
HttpHeaders.LAST_MODIFIED
|
|
||||||
],
|
|
||||||
everyElement(predicate(
|
everyElement(predicate(
|
||||||
response.headers.containsKey, 'contained in response headers')));
|
response.headers.containsKey, 'contained in response headers')));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_diagnostics/angel_diagnostics.dart';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_static/angel_static.dart';
|
import 'package:angel_static/angel_static.dart';
|
||||||
import 'package:angel_test/angel_test.dart';
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
final Directory swaggerUiDistDir =
|
final Directory swaggerUiDistDir =
|
||||||
new Directory('test/node_modules/swagger-ui-dist');
|
const LocalFileSystem().directory('test/node_modules/swagger-ui-dist');
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
TestClient client;
|
TestClient client;
|
||||||
|
@ -15,19 +15,22 @@ main() async {
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
// Load file contents
|
// Load file contents
|
||||||
swaggerUiCssContents =
|
swaggerUiCssContents = await const LocalFileSystem()
|
||||||
await new File.fromUri(swaggerUiDistDir.uri.resolve('swagger-ui.css'))
|
.file(swaggerUiDistDir.uri.resolve('swagger-ui.css'))
|
||||||
.readAsString();
|
.readAsString();
|
||||||
swaggerTestJsContents =
|
swaggerTestJsContents = await const LocalFileSystem()
|
||||||
await new File.fromUri(swaggerUiDistDir.uri.resolve('test.js'))
|
.file(swaggerUiDistDir.uri.resolve('test.js'))
|
||||||
.readAsString();
|
.readAsString();
|
||||||
|
|
||||||
// Initialize app
|
// Initialize app
|
||||||
var app = new Angel();
|
var app = new Angel();
|
||||||
await Future.forEach([
|
app.logger = new Logger('angel')..onRecord.listen(print);
|
||||||
new VirtualDirectory(source: swaggerUiDistDir, publicPath: 'swagger/'),
|
|
||||||
logRequests()
|
app.use(
|
||||||
], app.configure);
|
new VirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: swaggerUiDistDir, publicPath: 'swagger/')
|
||||||
|
.handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
app.dumpTree();
|
app.dumpTree();
|
||||||
client = await connectTo(app);
|
client = await connectTo(app);
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_static/angel_static.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:mustache4dart/mustache4dart.dart' as ms;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
TestClient client, client2;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
var app = new Angel();
|
|
||||||
var vDir = new CachingVirtualDirectory(
|
|
||||||
source: new Directory('test'),
|
|
||||||
transformers: [new ExtensionTransformer()]);
|
|
||||||
await app.configure(vDir);
|
|
||||||
await vDir.transformersLoaded.then((map) {
|
|
||||||
print('Loaded transformer map: $map');
|
|
||||||
});
|
|
||||||
client = await connectTo(app);
|
|
||||||
|
|
||||||
var app2 = new Angel();
|
|
||||||
var vDir2 = new CachingVirtualDirectory(
|
|
||||||
source: new Directory('test'),
|
|
||||||
transformers: [
|
|
||||||
new MustacheTransformer({'foo': 'bar'})
|
|
||||||
]);
|
|
||||||
await app2.configure(vDir2);
|
|
||||||
await vDir2.transformersLoaded.then((map) {
|
|
||||||
print('Loaded transformer map2: $map');
|
|
||||||
});
|
|
||||||
client2 = await connectTo(app2);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close().then((_) => client2.close()));
|
|
||||||
|
|
||||||
test('foo', () async {
|
|
||||||
var response = await client.get('/index.ext');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasBody('.txt'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('request twice in a row', () async {
|
|
||||||
var response = await client2.get('/foo.html');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
print('Response headers: ${response.headers}');
|
|
||||||
expect(response, hasBody('<h1>bar</h1>'));
|
|
||||||
|
|
||||||
var response2 = await client2.get('/foo.html');
|
|
||||||
expect(response2, hasHeader(HttpHeaders.CONTENT_TYPE, ContentType.HTML.mimeType));
|
|
||||||
print('Response2: ${response2.body}');
|
|
||||||
expect(response2, hasBody('<h1>bar</h1>'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtensionTransformer implements FileTransformer {
|
|
||||||
@override
|
|
||||||
FileInfo declareOutput(FileInfo file) {
|
|
||||||
return file.extension == '.ext' ? null : file.changeExtension('.ext');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<FileInfo> transform(FileInfo file) =>
|
|
||||||
file.changeText(file.extension).changeExtension('.ext');
|
|
||||||
}
|
|
||||||
|
|
||||||
class MustacheTransformer implements FileTransformer {
|
|
||||||
final Map<String, dynamic> locals;
|
|
||||||
|
|
||||||
MustacheTransformer(this.locals);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FileInfo declareOutput(FileInfo file) =>
|
|
||||||
file.extension == '.mustache' ? file.changeExtension('.html') : null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<FileInfo> transform(FileInfo file) async {
|
|
||||||
var template = await file.content.transform(UTF8.decoder).join();
|
|
||||||
var compiled = ms.render(template, locals ?? {});
|
|
||||||
return file.changeExtension('.html').changeText(compiled);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue