1.2.0
This commit is contained in:
parent
fa3474404c
commit
8bd228c033
10 changed files with 556 additions and 33 deletions
2
.analysis-options
Normal file
2
.analysis-options
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
75
README.md
75
README.md
|
@ -10,7 +10,7 @@ In `pubspec.yaml`:
|
|||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel_static: ^1.1.0
|
||||
angel_static: ^1.2.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
@ -58,3 +58,76 @@ The `VirtualDirectory` API accepts a few named parameters:
|
|||
- **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.
|
||||
- **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 {
|
||||
/// 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,5 +1,7 @@
|
|||
library angel_static;
|
||||
|
||||
export 'src/cache.dart';
|
||||
export 'src/file_info.dart';
|
||||
export 'src/file_transformer.dart';
|
||||
export 'src/serve_static.dart';
|
||||
export 'src/virtual_directory.dart';
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:angel_framework/angel_framework.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'file_info.dart';
|
||||
import 'file_transformer.dart';
|
||||
import 'virtual_directory.dart';
|
||||
|
||||
final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
||||
|
@ -75,14 +77,16 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
|||
this.useWeakEtags: true,
|
||||
String publicPath,
|
||||
StaticFileCallback callback,
|
||||
bool streamToIO: false})
|
||||
bool streamToIO: false,
|
||||
Iterable<FileTransformer> transformers: const []})
|
||||
: super(
|
||||
source: source,
|
||||
debug: debug == true,
|
||||
indexFileNames: indexFileNames ?? ['index.html'],
|
||||
publicPath: publicPath ?? '/',
|
||||
callback: callback,
|
||||
streamToIO: streamToIO == true);
|
||||
streamToIO: streamToIO == true,
|
||||
transformers: transformers ?? []);
|
||||
|
||||
@override
|
||||
Future<bool> serveFile(
|
||||
|
@ -112,7 +116,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
|||
|
||||
if (hasBeenModified) {
|
||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
||||
setCachedHeaders(file, stat, req, res);
|
||||
setCachedHeaders(stat.modified, req, res);
|
||||
return new Future.value(false);
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +128,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
|||
|
||||
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
|
||||
res.statusCode = HttpStatus.NOT_MODIFIED;
|
||||
setCachedHeaders(file, stat, req, res);
|
||||
setCachedHeaders(stat.modified, req, res);
|
||||
|
||||
if (_etags.containsKey(file.absolute.path))
|
||||
res.headers[HttpHeaders.ETAG] = _etags[file.absolute.path];
|
||||
|
@ -143,7 +147,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
|||
res.headers
|
||||
..[HttpHeaders.ETAG] = etag
|
||||
..[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
|
||||
setCachedHeaders(file, stat, req, res);
|
||||
setCachedHeaders(stat.modified, req, res);
|
||||
|
||||
if (useWeakEtags == false) {
|
||||
res
|
||||
|
@ -160,14 +164,71 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
|||
}
|
||||
|
||||
void setCachedHeaders(
|
||||
File file, FileStat stat, RequestContext req, ResponseContext res) {
|
||||
DateTime modified, RequestContext req, ResponseContext res) {
|
||||
var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC);
|
||||
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0));
|
||||
|
||||
res.headers
|
||||
..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}'
|
||||
..[HttpHeaders.EXPIRES] = formatDateForHttp(expiry)
|
||||
..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(stat.modified);
|
||||
..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(modified);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> serveAsset(
|
||||
FileInfo fileInfo, RequestContext req, ResponseContext res) {
|
||||
if (onlyInProduction == true && req.app.isProduction == true) {
|
||||
return super.serveAsset(fileInfo, req, res);
|
||||
}
|
||||
|
||||
if (noCache == true) {
|
||||
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[HttpHeaders.IF_MODIFIED_SINCE] != null) {
|
||||
try {
|
||||
var ifModifiedSince = req.headers.ifModifiedSince;
|
||||
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
91
lib/src/file_info.dart
Normal file
91
lib/src/file_info.dart
Normal file
|
@ -0,0 +1,91 @@
|
|||
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),
|
||||
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), 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, mimeType, 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);
|
||||
}
|
15
lib/src/file_transformer.dart
Normal file
15
lib/src/file_transformer.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
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);
|
||||
}
|
|
@ -2,7 +2,12 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:cli_util/cli_logging.dart' as cli;
|
||||
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);
|
||||
|
||||
|
@ -28,8 +33,16 @@ String _pathify(String path) {
|
|||
/// A static server plug-in.
|
||||
class VirtualDirectory implements AngelPlugin {
|
||||
final bool debug;
|
||||
Angel _app;
|
||||
String _prefix;
|
||||
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.
|
||||
Directory get source => _source;
|
||||
|
@ -46,14 +59,35 @@ class VirtualDirectory implements AngelPlugin {
|
|||
/// If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.
|
||||
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,
|
||||
this.debug: false,
|
||||
this.indexFileNames: const ['index.html'],
|
||||
this.publicPath: '/',
|
||||
this.callback,
|
||||
this.streamToIO: false}) {
|
||||
this.streamToIO: false,
|
||||
this.useTransformersInProduction: false,
|
||||
Iterable<FileTransformer> transformers: const []}) {
|
||||
_prefix = publicPath.replaceAll(_straySlashes, '');
|
||||
this._transformers.addAll(transformers ?? []);
|
||||
|
||||
if (source != null) {
|
||||
_source = source;
|
||||
|
@ -65,20 +99,79 @@ class VirtualDirectory implements AngelPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
_printDebug(msg) {
|
||||
if (debug) print(msg);
|
||||
call(Angel app) async {
|
||||
serve(_app = app);
|
||||
app.justBeforeStop.add((_) => close());
|
||||
}
|
||||
|
||||
call(Angel app) async => serve(app);
|
||||
|
||||
void serve(Router router) {
|
||||
_printDebug('Source directory: ${source.absolute.path}');
|
||||
_printDebug('Public path prefix: "$_prefix"');
|
||||
// _printDebug('Source directory: ${source.absolute.path}');
|
||||
// _printDebug('Public path prefix: "$_prefix"');
|
||||
router.get('$publicPath/*',
|
||||
(RequestContext req, ResponseContext res) async {
|
||||
var path = req.path.replaceAll(_straySlashes, '');
|
||||
return servePath(path, 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 {
|
||||
if (!_transformerLoad.isCompleted) {
|
||||
_transformerLoad.completeError(new StateError(
|
||||
'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 {
|
||||
|
@ -95,23 +188,44 @@ class VirtualDirectory implements AngelPlugin {
|
|||
|
||||
Future<bool> serveStat(String absolute, FileStat stat, RequestContext req,
|
||||
ResponseContext res) async {
|
||||
if (stat.type == FileSystemEntityType.NOT_FOUND)
|
||||
return true;
|
||||
else if (stat.type == FileSystemEntityType.DIRECTORY)
|
||||
if (stat.type == FileSystemEntityType.DIRECTORY)
|
||||
return await serveDirectory(new Directory(absolute), stat, req, res);
|
||||
else if (stat.type == FileSystemEntityType.FILE)
|
||||
return await serveFile(new File(absolute), stat, req, res);
|
||||
else if (stat.type == FileSystemEntityType.LINK) {
|
||||
var link = new Link(absolute);
|
||||
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
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> serveFile(
|
||||
Future<bool> serveDirectory(Directory directory, FileStat stat,
|
||||
RequestContext req, ResponseContext res) async {
|
||||
for (String indexFileName in indexFileNames) {
|
||||
final index =
|
||||
new File.fromUri(directory.absolute.uri.resolve(indexFileName));
|
||||
if (await index.exists()) {
|
||||
return await serveFile(index, stat, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> serveFileOld(
|
||||
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
||||
_printDebug('Sending file ${file.absolute.path}...');
|
||||
_printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}');
|
||||
// _printDebug('Sending file ${file.absolute.path}...');
|
||||
// _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}');
|
||||
res.statusCode = 200;
|
||||
|
||||
if (callback != null) {
|
||||
|
@ -135,16 +249,131 @@ class VirtualDirectory implements AngelPlugin {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<bool> serveDirectory(Directory directory, FileStat stat,
|
||||
RequestContext req, ResponseContext res) async {
|
||||
for (String indexFileName in indexFileNames) {
|
||||
final index =
|
||||
new File.fromUri(directory.absolute.uri.resolve(indexFileName));
|
||||
if (await index.exists()) {
|
||||
return await serveFile(index, stat, req, res);
|
||||
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
|
||||
var value = req.headers.value(HttpHeaders.ACCEPT);
|
||||
bool acceptable = value == null ||
|
||||
value.isEmpty ||
|
||||
value.contains(mimeType) ||
|
||||
value.contains('*/*');
|
||||
if (!acceptable)
|
||||
throw new AngelHttpException(
|
||||
new UnsupportedError(
|
||||
'Client requested $value, but server wanted to send $mimeType.'),
|
||||
statusCode: HttpStatus.NOT_ACCEPTABLE,
|
||||
message: '406 Not Acceptable');
|
||||
}
|
||||
|
||||
Future<bool> serveFile(
|
||||
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
||||
// _printDebug('Sending file ${file.absolute.path}...');
|
||||
// _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}');
|
||||
res.statusCode = 200;
|
||||
|
||||
if (callback != null) {
|
||||
var r = callback(file, req, res);
|
||||
r = r is Future ? await r : r;
|
||||
if (r != null && r != true) return r;
|
||||
}
|
||||
|
||||
var type = lookupMimeType(file.path);
|
||||
_ensureContentTypeAllowed(type, req);
|
||||
res.headers[HttpHeaders.CONTENT_TYPE] = type;
|
||||
|
||||
if (streamToIO == true) {
|
||||
res
|
||||
..io.headers.set(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path))
|
||||
..io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip')
|
||||
..end()
|
||||
..willCloseItself = true;
|
||||
|
||||
await file.openRead().transform(GZIP.encoder).pipe(res.io);
|
||||
} else
|
||||
await res.sendFile(file);
|
||||
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, file.mimeType)
|
||||
..io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip')
|
||||
..end()
|
||||
..willCloseItself = true;
|
||||
await file.content.transform(GZIP.encoder).pipe(res.io);
|
||||
} 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
print('Build of assets in "${source.absolute.path}" complete.');
|
||||
}
|
||||
}
|
||||
|
|
13
pubspec.yaml
13
pubspec.yaml
|
@ -2,13 +2,20 @@ name: angel_static
|
|||
description: Static server middleware for Angel.
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
homepage: https://github.com/angel-dart/angel_static
|
||||
author: thosakwe <thosakwe@gmail.com>
|
||||
version: 1.1.4+2
|
||||
homepage: https://github.com/angel-dart/static
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
version: 1.2.0
|
||||
dependencies:
|
||||
angel_framework: ^1.0.0-dev
|
||||
cli_util: ^0.1.1
|
||||
crypto: ^2.0.0
|
||||
intl: ">=0.0.0 <1.0.0"
|
||||
mime: ^0.9.3
|
||||
path: ^1.4.2
|
||||
pool: ^1.0.0
|
||||
watcher: ^0.9.7
|
||||
dev_dependencies:
|
||||
angel_diagnostics: ^1.0.0
|
||||
angel_test: ^1.0.0
|
||||
http: ^0.11.3
|
||||
test: ^0.12.13
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_diagnostics/angel_diagnostics.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_static/angel_static.dart';
|
||||
import 'package:http/http.dart' show Client;
|
||||
|
@ -24,10 +25,11 @@ main() {
|
|||
source: testDir,
|
||||
indexFileNames: ['index.php', 'index.txt']));
|
||||
|
||||
app.get('*', 'Fallback');
|
||||
app.after.add('Fallback');
|
||||
|
||||
app.dumpTree(showMatchers: true);
|
||||
|
||||
await app.configure(logRequests());
|
||||
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
|
||||
url = "http://${app.httpServer.address.host}:${app.httpServer.port}";
|
||||
});
|
||||
|
|
41
test/transformer_test.dart
Normal file
41
test/transformer_test.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'dart:async';
|
||||
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:test/test.dart';
|
||||
|
||||
main() {
|
||||
TestClient client;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('foo', () async {
|
||||
var response = await client.get('/index.ext');
|
||||
print('Response: ${response.body}');
|
||||
expect(response, hasBody('.txt'));
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
Loading…
Reference in a new issue