Add 'packages/static/' from commit '6ec67dde7bd318c5e82fce66e3b22dfa3a378c24'
git-subtree-dir: packages/static git-subtree-mainline:17ad5f82c3
git-subtree-split:6ec67dde7b
This commit is contained in:
commit
f7c6ebf200
24 changed files with 1156 additions and 0 deletions
68
packages/static/.gitignore
vendored
Normal file
68
packages/static/.gitignore
vendored
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
## Directory-based project format:
|
||||||
|
.idea/
|
||||||
|
# if you remove the above rule, at least ignore the following:
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
# .idea/workspace.xml
|
||||||
|
# .idea/tasks.xml
|
||||||
|
# .idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
# .idea/dataSources.ids
|
||||||
|
# .idea/dataSources.xml
|
||||||
|
# .idea/sqlDataSources.xml
|
||||||
|
# .idea/dynamic.xml
|
||||||
|
# .idea/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
# .idea/gradle.xml
|
||||||
|
# .idea/libraries
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
# .idea/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
### Dart template
|
||||||
|
# Don’t commit the following directories created by pub.
|
||||||
|
.buildlog
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
packages
|
||||||
|
.packages
|
||||||
|
|
||||||
|
# Or the files created by dart2js.
|
||||||
|
*.dart.js
|
||||||
|
*.js_
|
||||||
|
*.js.deps
|
||||||
|
*.js.map
|
||||||
|
|
||||||
|
# Include when developing application packages.
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
|
.dart_tool
|
||||||
|
*.mp3
|
||||||
|
*.mp4
|
4
packages/static/.travis.yml
Normal file
4
packages/static/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
language: dart
|
||||||
|
dart:
|
||||||
|
- dev
|
||||||
|
- stable
|
74
packages/static/CHANGELOG.md
Normal file
74
packages/static/CHANGELOG.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# 2.1.3+2
|
||||||
|
* Prepare for upcoming change to File.openRead()
|
||||||
|
|
||||||
|
# 2.1.3+1
|
||||||
|
* Apply control flow lints.
|
||||||
|
|
||||||
|
# 2.1.3
|
||||||
|
* Apply lints.
|
||||||
|
* Pin to Dart `>=2.0.0 <3.0.0`.
|
||||||
|
* Use at least version `2.0.0-rc.0` of `angel_framework`.
|
||||||
|
|
||||||
|
# 2.1.2+1
|
||||||
|
* Fix a typo that prevented `Range` requests from working.
|
||||||
|
|
||||||
|
# 2.1.2
|
||||||
|
* Patch support for range+streaming in Caching server.
|
||||||
|
|
||||||
|
# 2.1.1
|
||||||
|
* URI-encode paths in directory listing. This produces correct URL's, always.
|
||||||
|
|
||||||
|
# 2.1.0
|
||||||
|
* Include support for the `Range` header.
|
||||||
|
* Use MD5 for etags, instead of a weak ETag.
|
||||||
|
|
||||||
|
# 2.0.2
|
||||||
|
* Fixed invalid HTML for directory listings.
|
||||||
|
|
||||||
|
# 2.0.1
|
||||||
|
* Remove use of `sendFile`.
|
||||||
|
* Add a `p.isWithin` check to ensure that paths do not escape the `source` directory.
|
||||||
|
* Handle `HEAD` requests.
|
||||||
|
|
||||||
|
# 2.0.0
|
||||||
|
* Upgrade dependencies to Angel 2 + file@5.
|
||||||
|
* Replace `useStream` with `useBuffer`.
|
||||||
|
* Remove `package:intl`, just use `HttpDate` instead.
|
||||||
|
|
||||||
|
# 1.3.0+1
|
||||||
|
* Dart 2 fixes.
|
||||||
|
* Enable optionally writing responses to the buffer instead of streaming.
|
||||||
|
|
||||||
|
# 1.3.0
|
||||||
|
* `pushState` uses `strict` mode when `accepts` is passed.
|
||||||
|
|
||||||
|
# 1.3.0-alpha+2
|
||||||
|
* Added an `accepts` option to `pushState`.
|
||||||
|
* Added optional directory listings.
|
||||||
|
|
||||||
|
# 1.3.0-alpha+1
|
||||||
|
* ETags once again only encode the first 50 bytes of files. Resolves [#27](https://github.com/angel-dart/static/issues/27).
|
||||||
|
|
||||||
|
# 1.3.0-alpha
|
||||||
|
* Removed file transformers.
|
||||||
|
* `VirtualDirectory` is no longer an `AngelPlugin`, and instead exposes a `handleRequest` middleware.
|
||||||
|
* Added `pushState` to `VirtualDirectory`.
|
||||||
|
|
||||||
|
# 1.2.5
|
||||||
|
* Fixed a bug where `onlyInProduction` was not properly adhered to.
|
||||||
|
* Fixed another bug where `Accept-Encoding` was not properly adhered to.
|
||||||
|
* Setting `maxAge` to `null` will now prevent a `CachingVirtualDirectory` from sending an `Expires` header.
|
||||||
|
* Pre-built assets can now be mass-deleted with `VirtualDirectory.cleanFromDisk()`.
|
||||||
|
Resolves [#22](https://github.com/angel-dart/static/issues/22).
|
||||||
|
|
||||||
|
# 1.2.4+1
|
||||||
|
Fixed a bug where `Accept-Encoding` was not properly adhered to.
|
||||||
|
|
||||||
|
# 1.2.4
|
||||||
|
Fixes https://github.com/angel-dart/angel/issues/44.
|
||||||
|
* MIME types will now default to `application/octet-stream`.
|
||||||
|
* When `streamToIO` is `true`, the body will only be sent gzipped if the request explicitly allows it.
|
||||||
|
|
||||||
|
# 1.2.3
|
||||||
|
Fixed #40 and #41, which dealt with paths being improperly served when using a
|
||||||
|
`publicPath`.
|
21
packages/static/LICENSE
Normal file
21
packages/static/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 angel-dart
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
70
packages/static/README.md
Normal file
70
packages/static/README.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# static
|
||||||
|
[![Pub](https://img.shields.io/pub/v/angel_static.svg)](https://pub.dartlang.org/packages/angel_static)
|
||||||
|
[![build status](https://travis-ci.org/angel-dart/static.svg?branch=master)](https://travis-ci.org/angel-dart/static)
|
||||||
|
|
||||||
|
Static server infrastructure for Angel.
|
||||||
|
|
||||||
|
*Can also handle `Range` requests now, making it suitable for media streaming, ex. music, video, etc.*
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
In `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
angel_static: ^2.0.0-alpha
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
To serve files from a directory, you need to create a `VirtualDirectory`.
|
||||||
|
Keep in mind that `angel_static` uses `package:file` instead of `dart:io`.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = Angel();
|
||||||
|
var fs = const LocalFileSystem();
|
||||||
|
|
||||||
|
// Normal static server
|
||||||
|
var vDir = VirtualDirectory(app, fs, source: Directory('./public'));
|
||||||
|
|
||||||
|
// Send Cache-Control, ETag, etc. as well
|
||||||
|
var vDir = CachingVirtualDirectory(app, fs, source: Directory('./public'));
|
||||||
|
|
||||||
|
// Mount the VirtualDirectory's request handler
|
||||||
|
app.fallback(vDir.handleRequest);
|
||||||
|
|
||||||
|
// Start your server!!!
|
||||||
|
await AngelHttp(app).startServer();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Push State
|
||||||
|
`VirtualDirectory` also exposes a `pushState` method that returns a
|
||||||
|
request handler that serves the file at a given path as a fallback, unless
|
||||||
|
the user is requesting that file. This can be very useful for SPA's.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Create VirtualDirectory as well
|
||||||
|
var vDir = CachingVirtualDirectory(...);
|
||||||
|
|
||||||
|
// Mount it
|
||||||
|
app.fallback(vDir.handleRequest);
|
||||||
|
|
||||||
|
// Fallback to index.html on 404
|
||||||
|
app.fallback(vDir.pushState('index.html'));
|
||||||
|
```
|
||||||
|
|
||||||
|
# Options
|
||||||
|
The `VirtualDirectory` API accepts a few named parameters:
|
||||||
|
- **source**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `web` (in development) or
|
||||||
|
`build/web` (in production), depending on your `ANGEL_ENV`.
|
||||||
|
- **indexFileNames**: A `List<String>` of filenames that should be served as index pages. Default is `['index.html']`.
|
||||||
|
- **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,
|
||||||
|
please include this.
|
||||||
|
- **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`,
|
||||||
|
then the callback's result will be sent to the user, instead of the file contents.
|
8
packages/static/analysis_options.yaml
Normal file
8
packages/static/analysis_options.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
- unnecessary_const
|
||||||
|
- unnecessary_new
|
40
packages/static/example/main.dart
Normal file
40
packages/static/example/main.dart
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
main(List<String> args) async {
|
||||||
|
var app = Angel();
|
||||||
|
var http = AngelHttp(app);
|
||||||
|
var fs = const LocalFileSystem();
|
||||||
|
var vDir = CachingVirtualDirectory(
|
||||||
|
app,
|
||||||
|
fs,
|
||||||
|
allowDirectoryListing: true,
|
||||||
|
source: args.isEmpty ? fs.currentDirectory : fs.directory(args[0]),
|
||||||
|
maxAge: const Duration(days: 24).inSeconds,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.mimeTypeResolver
|
||||||
|
..addExtension('', 'text/plain')
|
||||||
|
..addExtension('dart', 'text/dart')
|
||||||
|
..addExtension('lock', 'text/plain')
|
||||||
|
..addExtension('markdown', 'text/plain')
|
||||||
|
..addExtension('md', 'text/plain')
|
||||||
|
..addExtension('yaml', 'text/plain');
|
||||||
|
|
||||||
|
app.logger = Logger('example')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.fallback(vDir.handleRequest);
|
||||||
|
app.fallback((req, res) => throw AngelHttpException.notFound());
|
||||||
|
|
||||||
|
var server = await http.startServer('127.0.0.1', 3000);
|
||||||
|
print('Serving from ${vDir.source.path}');
|
||||||
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
|
}
|
4
packages/static/lib/angel_static.dart
Normal file
4
packages/static/lib/angel_static.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
library angel_static;
|
||||||
|
|
||||||
|
export 'src/cache.dart';
|
||||||
|
export 'src/virtual_directory.dart';
|
182
packages/static/lib/src/cache.dart
Normal file
182
packages/static/lib/src/cache.dart
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' show HttpDate;
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'virtual_directory.dart';
|
||||||
|
|
||||||
|
/// Returns a string representation of the given [CacheAccessLevel].
|
||||||
|
String accessLevelToString(CacheAccessLevel accessLevel) {
|
||||||
|
switch (accessLevel) {
|
||||||
|
case CacheAccessLevel.PRIVATE:
|
||||||
|
return 'private';
|
||||||
|
case CacheAccessLevel.PUBLIC:
|
||||||
|
return 'public';
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Unrecognized cache access level: $accessLevel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `VirtualDirectory` that also sets `Cache-Control` headers.
|
||||||
|
class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
|
final Map<String, String> _etags = {};
|
||||||
|
|
||||||
|
/// Either `PUBLIC` or `PRIVATE`.
|
||||||
|
final CacheAccessLevel accessLevel;
|
||||||
|
|
||||||
|
/// If `true`, responses will always have `private, max-age=0` as their `Cache-Control` header.
|
||||||
|
final bool noCache;
|
||||||
|
|
||||||
|
/// If `true` (default), `Cache-Control` headers will only be set if the application is in production mode.
|
||||||
|
final bool onlyInProduction;
|
||||||
|
|
||||||
|
/// If `true` (default), ETags will be computed and sent along with responses.
|
||||||
|
final bool useEtags;
|
||||||
|
|
||||||
|
/// The `max-age` for `Cache-Control`.
|
||||||
|
///
|
||||||
|
/// Set this to `null` to leave no `Expires` header on responses.
|
||||||
|
final int maxAge;
|
||||||
|
|
||||||
|
CachingVirtualDirectory(Angel app, FileSystem fileSystem,
|
||||||
|
{this.accessLevel = CacheAccessLevel.PUBLIC,
|
||||||
|
Directory source,
|
||||||
|
bool debug,
|
||||||
|
Iterable<String> indexFileNames,
|
||||||
|
this.maxAge = 0,
|
||||||
|
this.noCache = false,
|
||||||
|
this.onlyInProduction = false,
|
||||||
|
this.useEtags = true,
|
||||||
|
bool allowDirectoryListing,
|
||||||
|
bool useBuffer = false,
|
||||||
|
String publicPath,
|
||||||
|
callback(File file, RequestContext req, ResponseContext res)})
|
||||||
|
: super(app, fileSystem,
|
||||||
|
source: source,
|
||||||
|
indexFileNames: indexFileNames ?? ['index.html'],
|
||||||
|
publicPath: publicPath ?? '/',
|
||||||
|
callback: callback,
|
||||||
|
allowDirectoryListing: allowDirectoryListing,
|
||||||
|
useBuffer: useBuffer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> serveFile(
|
||||||
|
File file, FileStat stat, RequestContext req, ResponseContext res) {
|
||||||
|
res.headers['accept-ranges'] = 'bytes';
|
||||||
|
|
||||||
|
if (onlyInProduction == true && req.app.environment.isProduction != true) {
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldNotCache = noCache == true;
|
||||||
|
|
||||||
|
if (!shouldNotCache) {
|
||||||
|
shouldNotCache = req.headers.value('cache-control') == 'no-cache' ||
|
||||||
|
req.headers.value('pragma') == 'no-cache';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotCache) {
|
||||||
|
res.headers['cache-control'] = 'private, max-age=0, no-cache';
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
} else {
|
||||||
|
var ifModified = req.headers.ifModifiedSince;
|
||||||
|
bool ifRange = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ifModified = HttpDate.parse(req.headers.value('if-range'));
|
||||||
|
ifRange = true;
|
||||||
|
} catch (_) {
|
||||||
|
// Fail silently...
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifModified != null) {
|
||||||
|
try {
|
||||||
|
var ifModifiedSince = ifModified;
|
||||||
|
|
||||||
|
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
|
||||||
|
res.statusCode = 304;
|
||||||
|
setCachedHeaders(stat.modified, req, res);
|
||||||
|
|
||||||
|
if (useEtags && _etags.containsKey(file.absolute.path)) {
|
||||||
|
res.headers['ETag'] = _etags[file.absolute.path];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifRange) {
|
||||||
|
// Send the 206 like normal
|
||||||
|
res.statusCode = 206;
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future.value(false);
|
||||||
|
} else if (ifRange) {
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message:
|
||||||
|
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If-modified didn't work; try etags
|
||||||
|
|
||||||
|
if (useEtags == true) {
|
||||||
|
var etagsToMatchAgainst = req.headers['if-none-match'];
|
||||||
|
ifRange = false;
|
||||||
|
|
||||||
|
if (etagsToMatchAgainst?.isNotEmpty != true) {
|
||||||
|
etagsToMatchAgainst = req.headers['if-range'];
|
||||||
|
ifRange = etagsToMatchAgainst?.isNotEmpty == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (etagsToMatchAgainst?.isNotEmpty == true) {
|
||||||
|
bool hasBeenModified = false;
|
||||||
|
|
||||||
|
for (var etag in etagsToMatchAgainst) {
|
||||||
|
if (etag == '*') {
|
||||||
|
hasBeenModified = true;
|
||||||
|
} else {
|
||||||
|
hasBeenModified = !_etags.containsKey(file.absolute.path) ||
|
||||||
|
_etags[file.absolute.path] != etag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ifRange) {
|
||||||
|
if (!hasBeenModified) {
|
||||||
|
res.statusCode = 304;
|
||||||
|
setCachedHeaders(stat.modified, req, res);
|
||||||
|
return Future.value(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.lastModified().then((stamp) {
|
||||||
|
if (useEtags) {
|
||||||
|
res.headers['ETag'] = _etags[file.absolute.path] =
|
||||||
|
stamp.millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCachedHeaders(stat.modified, req, res);
|
||||||
|
return super.serveFile(file, stat, req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCachedHeaders(
|
||||||
|
DateTime modified, RequestContext req, ResponseContext res) {
|
||||||
|
var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC);
|
||||||
|
|
||||||
|
res.headers
|
||||||
|
..['cache-control'] = '$privacy, max-age=${maxAge ?? 0}'
|
||||||
|
..['last-modified'] = HttpDate.format(modified);
|
||||||
|
|
||||||
|
if (maxAge != null) {
|
||||||
|
var expiry = DateTime.now().add(Duration(seconds: maxAge ?? 0));
|
||||||
|
res.headers['expires'] = HttpDate.format(expiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CacheAccessLevel { PUBLIC, PRIVATE }
|
339
packages/static/lib/src/virtual_directory.dart
Normal file
339
packages/static/lib/src/virtual_directory.dart
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:range_header/range_header.dart';
|
||||||
|
|
||||||
|
final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||||
|
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
|
String _pathify(String path) {
|
||||||
|
var p = path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
|
Map<String, String> replace = {};
|
||||||
|
|
||||||
|
for (Match match in _param.allMatches(p)) {
|
||||||
|
if (match[3] != null) replace[match[0]] = ':${match[1]}';
|
||||||
|
}
|
||||||
|
|
||||||
|
replace.forEach((k, v) {
|
||||||
|
p = p.replaceAll(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A static server plug-in.
|
||||||
|
class VirtualDirectory {
|
||||||
|
String _prefix;
|
||||||
|
Directory _source;
|
||||||
|
|
||||||
|
/// The directory to serve files from.
|
||||||
|
Directory get source => _source;
|
||||||
|
|
||||||
|
/// An optional callback to run before serving files.
|
||||||
|
final Function(File file, RequestContext req, ResponseContext res) callback;
|
||||||
|
|
||||||
|
final Angel app;
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
|
/// Filenames to be resolved within directories as indices.
|
||||||
|
final Iterable<String> indexFileNames;
|
||||||
|
|
||||||
|
/// An optional public path to map requests to.
|
||||||
|
final String publicPath;
|
||||||
|
|
||||||
|
/// If `true` (default: `false`), then if a directory does not contain any of the specific [indexFileNames], a default directory listing will be served.
|
||||||
|
final bool allowDirectoryListing;
|
||||||
|
|
||||||
|
/// If `true` (default: `true`), then files will be opened as streams and piped into the request.
|
||||||
|
///
|
||||||
|
/// If not, the response buffer will be used instead.
|
||||||
|
final bool useBuffer;
|
||||||
|
|
||||||
|
VirtualDirectory(this.app, this.fileSystem,
|
||||||
|
{Directory source,
|
||||||
|
this.indexFileNames = const ['index.html'],
|
||||||
|
this.publicPath = '/',
|
||||||
|
this.callback,
|
||||||
|
this.allowDirectoryListing = false,
|
||||||
|
this.useBuffer = false}) {
|
||||||
|
_prefix = publicPath.replaceAll(_straySlashes, '');
|
||||||
|
if (source != null) {
|
||||||
|
_source = source;
|
||||||
|
} else {
|
||||||
|
String dirPath = app.environment.isProduction ? './build/web' : './web';
|
||||||
|
_source = fileSystem.directory(dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responds to incoming HTTP requests.
|
||||||
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
||||||
|
if (req.method != 'GET' && req.method != 'HEAD') {
|
||||||
|
return Future<bool>.value(true);
|
||||||
|
}
|
||||||
|
var path = req.uri.path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
|
if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix)) {
|
||||||
|
return Future<bool>.value(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return servePath(path, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handler that serves the file at the given path, unless the user has requested that path.
|
||||||
|
///
|
||||||
|
/// You can also limit this functionality to specific values of the `Accept` header, ex. `text/html`.
|
||||||
|
/// If [accepts] is `null`, OR at least one of the content types in [accepts] is present,
|
||||||
|
/// the view will be served.
|
||||||
|
RequestHandler pushState(String path, {Iterable accepts}) {
|
||||||
|
var vPath = path.replaceAll(_straySlashes, '');
|
||||||
|
if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
|
||||||
|
|
||||||
|
return (RequestContext req, ResponseContext res) {
|
||||||
|
var path = req.path.replaceAll(_straySlashes, '');
|
||||||
|
if (path == vPath) return Future<bool>.value(true);
|
||||||
|
|
||||||
|
if (accepts?.isNotEmpty == true) {
|
||||||
|
if (!accepts.any((x) => req.accepts(x, strict: true))) {
|
||||||
|
return Future<bool>.value(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servePath(vPath, req, res);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the file at the given virtual [path] to a response.
|
||||||
|
Future<bool> servePath(
|
||||||
|
String path, RequestContext req, ResponseContext res) async {
|
||||||
|
if (_prefix.isNotEmpty) {
|
||||||
|
// Only replace the *first* incidence
|
||||||
|
// Resolve: https://github.com/angel-dart/angel/issues/41
|
||||||
|
path = path.replaceFirst(RegExp('^' + _pathify(_prefix)), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isEmpty) path = '.';
|
||||||
|
path = path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
|
var absolute = source.absolute.uri.resolve(path).toFilePath();
|
||||||
|
var parent = source.absolute.uri.toFilePath();
|
||||||
|
|
||||||
|
if (!p.isWithin(parent, absolute) && !p.equals(parent, absolute)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stat = await fileSystem.stat(absolute);
|
||||||
|
return await serveStat(absolute, path, stat, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the file at the path given by the [stat] to a response.
|
||||||
|
Future<bool> serveStat(String absolute, String relative, FileStat stat,
|
||||||
|
RequestContext req, ResponseContext res) async {
|
||||||
|
if (stat.type == FileSystemEntityType.directory) {
|
||||||
|
return await serveDirectory(
|
||||||
|
fileSystem.directory(absolute), relative, stat, req, res);
|
||||||
|
} else if (stat.type == FileSystemEntityType.file) {
|
||||||
|
return await serveFile(fileSystem.file(absolute), stat, req, res);
|
||||||
|
} else if (stat.type == FileSystemEntityType.link) {
|
||||||
|
var link = fileSystem.link(absolute);
|
||||||
|
return await servePath(await link.resolveSymbolicLinks(), req, res);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serves the index file of a [directory], if it exists.
|
||||||
|
Future<bool> serveDirectory(Directory directory, String relative,
|
||||||
|
FileStat stat, RequestContext req, ResponseContext res) async {
|
||||||
|
for (String indexFileName in indexFileNames) {
|
||||||
|
final index =
|
||||||
|
fileSystem.file(directory.absolute.uri.resolve(indexFileName));
|
||||||
|
if (await index.exists()) {
|
||||||
|
return await serveFile(index, stat, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowDirectoryListing == true) {
|
||||||
|
res.contentType = MediaType('text', 'html');
|
||||||
|
res
|
||||||
|
..write('<!DOCTYPE html>')
|
||||||
|
..write('<html>')
|
||||||
|
..write(
|
||||||
|
'<head><meta name="viewport" content="width=device-width,initial-scale=1">')
|
||||||
|
..write('<style>ul { list-style-type: none; }</style>')
|
||||||
|
..write('</head><body>');
|
||||||
|
|
||||||
|
res.write('<li><a href="..">..</a></li>');
|
||||||
|
|
||||||
|
List<FileSystemEntity> entities = await directory
|
||||||
|
.list(followLinks: false)
|
||||||
|
.toList()
|
||||||
|
.then((l) => List.from(l));
|
||||||
|
entities.sort((a, b) {
|
||||||
|
if (a is Directory) {
|
||||||
|
if (b is Directory) return a.path.compareTo(b.path);
|
||||||
|
return -1;
|
||||||
|
} else if (a is File) {
|
||||||
|
if (b is Directory) {
|
||||||
|
return 1;
|
||||||
|
} else if (b is File) return a.path.compareTo(b.path);
|
||||||
|
return -1;
|
||||||
|
} else if (b is Link) return a.path.compareTo(b.path);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var entity in entities) {
|
||||||
|
var stub = p.basename(entity.path);
|
||||||
|
var href = stub;
|
||||||
|
String type;
|
||||||
|
|
||||||
|
if (entity is File) {
|
||||||
|
type = '[File]';
|
||||||
|
} else if (entity is Directory) {
|
||||||
|
type = '[Directory]';
|
||||||
|
} else if (entity is Link) type = '[Link]';
|
||||||
|
|
||||||
|
if (relative.isNotEmpty) href = '/' + relative + '/' + stub;
|
||||||
|
|
||||||
|
if (entity is Directory) href += '/';
|
||||||
|
href = Uri.encodeFull(href);
|
||||||
|
|
||||||
|
res.write('<li><a href="$href">$type $stub</a></li>');
|
||||||
|
}
|
||||||
|
|
||||||
|
res..write('</body></html>');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureContentTypeAllowed(String mimeType, RequestContext req) {
|
||||||
|
var value = req.headers.value('accept');
|
||||||
|
bool acceptable = value == null ||
|
||||||
|
value?.isNotEmpty != true ||
|
||||||
|
(mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) ||
|
||||||
|
value?.contains('*/*') == true;
|
||||||
|
if (!acceptable) {
|
||||||
|
throw AngelHttpException(
|
||||||
|
UnsupportedError(
|
||||||
|
'Client requested $value, but server wanted to send $mimeType.'),
|
||||||
|
statusCode: 406,
|
||||||
|
message: '406 Not Acceptable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the contents of a file to a response.
|
||||||
|
Future<bool> serveFile(
|
||||||
|
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
||||||
|
res.headers['accept-ranges'] = 'bytes';
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
return await req.app.executeHandler(
|
||||||
|
(RequestContext req, ResponseContext res) => callback(file, req, res),
|
||||||
|
req,
|
||||||
|
res);
|
||||||
|
}
|
||||||
|
|
||||||
|
var type =
|
||||||
|
app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream';
|
||||||
|
res.headers['accept-ranges'] = 'bytes';
|
||||||
|
_ensureContentTypeAllowed(type, req);
|
||||||
|
res.headers['accept-ranges'] = 'bytes';
|
||||||
|
res.contentType = MediaType.parse(type);
|
||||||
|
if (useBuffer == true) res.useBuffer();
|
||||||
|
|
||||||
|
if (req.headers.value('range')?.startsWith('bytes=') != true) {
|
||||||
|
await res.streamFile(file);
|
||||||
|
} else {
|
||||||
|
var header = RangeHeader.parse(req.headers.value('range'));
|
||||||
|
var items = RangeHeader.foldItems(header.items);
|
||||||
|
var totalFileSize = await file.length();
|
||||||
|
header = RangeHeader(items);
|
||||||
|
|
||||||
|
for (var item in header.items) {
|
||||||
|
bool invalid = false;
|
||||||
|
|
||||||
|
if (item.start != -1) {
|
||||||
|
invalid = item.end != -1 && item.end < item.start;
|
||||||
|
} else {
|
||||||
|
invalid = item.end == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalid) {
|
||||||
|
throw AngelHttpException(
|
||||||
|
Exception("Semantically invalid, or unbounded range."),
|
||||||
|
statusCode: 416,
|
||||||
|
message: "Semantically invalid, or unbounded range.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's within range.
|
||||||
|
if (item.start >= totalFileSize || item.end >= totalFileSize) {
|
||||||
|
throw AngelHttpException(
|
||||||
|
Exception("Given range $item is out of bounds."),
|
||||||
|
statusCode: 416,
|
||||||
|
message: "Given range $item is out of bounds.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.items.isEmpty) {
|
||||||
|
throw AngelHttpException(null,
|
||||||
|
statusCode: 416, message: '`Range` header may not be empty.');
|
||||||
|
} else if (header.items.length == 1) {
|
||||||
|
var item = header.items[0];
|
||||||
|
Stream<Uint8List> stream;
|
||||||
|
int len = 0, total = totalFileSize;
|
||||||
|
|
||||||
|
if (item.start == -1) {
|
||||||
|
if (item.end == -1) {
|
||||||
|
len = total;
|
||||||
|
stream = file.openRead();
|
||||||
|
} else {
|
||||||
|
len = item.end + 1;
|
||||||
|
stream = file.openRead(0, item.end + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (item.end == -1) {
|
||||||
|
len = total - item.start;
|
||||||
|
stream = file.openRead(item.start);
|
||||||
|
} else {
|
||||||
|
len = item.end - item.start + 1;
|
||||||
|
stream = file.openRead(item.start, item.end + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.contentType = MediaType.parse(
|
||||||
|
app.mimeTypeResolver.lookup(file.path) ??
|
||||||
|
'application/octet-stream');
|
||||||
|
res.statusCode = 206;
|
||||||
|
res.headers['content-length'] = len.toString();
|
||||||
|
res.headers['content-range'] = 'bytes ' + item.toContentRange(total);
|
||||||
|
await stream.cast<List<int>>().pipe(res);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
var transformer = RangeHeaderTransformer(
|
||||||
|
header,
|
||||||
|
app.mimeTypeResolver.lookup(file.path) ??
|
||||||
|
'application/octet-stream',
|
||||||
|
await file.length());
|
||||||
|
res.statusCode = 206;
|
||||||
|
res.headers['content-length'] =
|
||||||
|
transformer.computeContentLength(totalFileSize).toString();
|
||||||
|
res.contentType = MediaType(
|
||||||
|
'multipart', 'byteranges', {'boundary': transformer.boundary});
|
||||||
|
await file
|
||||||
|
.openRead()
|
||||||
|
.cast<List<int>>()
|
||||||
|
.transform(transformer)
|
||||||
|
.pipe(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
22
packages/static/pubspec.yaml
Normal file
22
packages/static/pubspec.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: angel_static
|
||||||
|
description: Static server middleware for Angel. Also capable of serving Range responses.
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.0.0 <3.0.0"
|
||||||
|
homepage: https://github.com/angel-dart/static
|
||||||
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
|
version: 2.1.3+2
|
||||||
|
dependencies:
|
||||||
|
angel_framework: ^2.0.0-rc.0
|
||||||
|
convert: ^2.0.0
|
||||||
|
crypto: ^2.0.0
|
||||||
|
file: ^5.0.0
|
||||||
|
http_parser: ^3.0.0
|
||||||
|
path: ^1.4.2
|
||||||
|
range_header: ^2.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
angel_test: ^2.0.0-alpha
|
||||||
|
http:
|
||||||
|
logging: ^0.11.0
|
||||||
|
matcher: ^0.12.0
|
||||||
|
pedantic: ^1.0.0
|
||||||
|
test: ^1.0.0
|
2
packages/static/test/HELLO.md
Normal file
2
packages/static/test/HELLO.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# hello
|
||||||
|
world!
|
82
packages/static/test/all_test.dart
Normal file
82
packages/static/test/all_test.dart
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.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:logging/logging.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
Angel app;
|
||||||
|
AngelHttp http;
|
||||||
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
|
String url;
|
||||||
|
Client client = Client();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
app = Angel();
|
||||||
|
http = AngelHttp(app);
|
||||||
|
app.logger = Logger('angel')..onRecord.listen(print);
|
||||||
|
|
||||||
|
app.fallback(
|
||||||
|
VirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: testDir,
|
||||||
|
publicPath: '/virtual',
|
||||||
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.fallback(
|
||||||
|
VirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: testDir,
|
||||||
|
useBuffer: true,
|
||||||
|
indexFileNames: ['index.php', 'index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.fallback((req, res) => 'Fallback');
|
||||||
|
|
||||||
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
|
var server = await http.startServer();
|
||||||
|
url = "http://${server.address.host}:${server.port}";
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (http.server != null) await http.server.close(force: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can serve files, with correct Content-Type', () async {
|
||||||
|
var response = await client.get("$url/sample.txt");
|
||||||
|
expect(response.body, equals("Hello world"));
|
||||||
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can serve child directories', () async {
|
||||||
|
var response = await client.get("$url/nested");
|
||||||
|
expect(response.body, equals("Bird"));
|
||||||
|
expect(response.headers['content-type'], contains("text/plain"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-existent files are skipped', () async {
|
||||||
|
var response = await client.get("$url/nonexist.ent");
|
||||||
|
expect(response.body, equals('"Fallback"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can match index files', () async {
|
||||||
|
var response = await client.get(url);
|
||||||
|
expect(response.body, equals("index!"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('virtualRoots can match index', () async {
|
||||||
|
var response = await client.get("$url/virtual");
|
||||||
|
expect(response.body, equals("index!"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chrome accept', () async {
|
||||||
|
var response = await client.get("$url/virtual", headers: {
|
||||||
|
'accept':
|
||||||
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
||||||
|
});
|
||||||
|
expect(response.body, equals("index!"));
|
||||||
|
});
|
||||||
|
}
|
28
packages/static/test/cache_sample.dart
Normal file
28
packages/static/test/cache_sample.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
Angel app;
|
||||||
|
AngelHttp http;
|
||||||
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
|
app = Angel();
|
||||||
|
http = AngelHttp(app);
|
||||||
|
|
||||||
|
app.fallback(
|
||||||
|
CachingVirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: testDir,
|
||||||
|
maxAge: 350,
|
||||||
|
onlyInProduction: false,
|
||||||
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('*', (req, res) => 'Fallback');
|
||||||
|
|
||||||
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
|
var server = await http.startServer();
|
||||||
|
print('Open at http://${server.address.host}:${server.port}');
|
||||||
|
}
|
77
packages/static/test/cache_test.dart
Normal file
77
packages/static/test/cache_test.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import 'dart:io' show HttpDate;
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.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:logging/logging.dart';
|
||||||
|
import 'package:matcher/matcher.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
Angel app;
|
||||||
|
AngelHttp http;
|
||||||
|
Directory testDir = const LocalFileSystem().directory('test');
|
||||||
|
String url;
|
||||||
|
Client client = Client();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
app = Angel();
|
||||||
|
http = AngelHttp(app);
|
||||||
|
|
||||||
|
app.fallback(
|
||||||
|
CachingVirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: testDir, maxAge: 350, onlyInProduction: false,
|
||||||
|
//publicPath: '/virtual',
|
||||||
|
indexFileNames: ['index.txt']).handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('*', (req, res) => 'Fallback');
|
||||||
|
|
||||||
|
app.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
|
app.logger = Logger('angel_static')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = await http.startServer();
|
||||||
|
url = "http://${server.address.host}:${server.port}";
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
if (http.server != null) await http.server.close(force: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sets etag, cache-control, expires, last-modified', () async {
|
||||||
|
var response = await client.get("$url");
|
||||||
|
|
||||||
|
print('Response status: ${response.statusCode}');
|
||||||
|
print('Response body: ${response.body}');
|
||||||
|
print('Response headers: ${response.headers}');
|
||||||
|
|
||||||
|
expect(response.statusCode, equals(200));
|
||||||
|
expect(
|
||||||
|
['etag', 'cache-control', 'expires', 'last-modified'],
|
||||||
|
everyElement(predicate(
|
||||||
|
response.headers.containsKey, 'contained in response headers')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if-modified-since', () async {
|
||||||
|
var response = await client.get("$url", headers: {
|
||||||
|
'if-modified-since':
|
||||||
|
HttpDate.format(DateTime.now().add(Duration(days: 365)))
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Response status: ${response.statusCode}');
|
||||||
|
|
||||||
|
expect(response.statusCode, equals(304));
|
||||||
|
expect(
|
||||||
|
['cache-control', 'expires', 'last-modified'],
|
||||||
|
everyElement(predicate(
|
||||||
|
response.headers.containsKey, 'contained in response headers')));
|
||||||
|
});
|
||||||
|
}
|
1
packages/static/test/foo.mustache
Normal file
1
packages/static/test/foo.mustache
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>{{foo}}</h1>
|
1
packages/static/test/index.txt
Normal file
1
packages/static/test/index.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
index!
|
64
packages/static/test/issue41_test.dart
Normal file
64
packages/static/test/issue41_test.dart
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_static/angel_static.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';
|
||||||
|
|
||||||
|
final Directory swaggerUiDistDir =
|
||||||
|
const LocalFileSystem().directory('test/node_modules/swagger-ui-dist');
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
TestClient client;
|
||||||
|
String swaggerUiCssContents, swaggerTestJsContents;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
// Load file contents
|
||||||
|
swaggerUiCssContents = await const LocalFileSystem()
|
||||||
|
.file(swaggerUiDistDir.uri.resolve('swagger-ui.css'))
|
||||||
|
.readAsString();
|
||||||
|
swaggerTestJsContents = await const LocalFileSystem()
|
||||||
|
.file(swaggerUiDistDir.uri.resolve('test.js'))
|
||||||
|
.readAsString();
|
||||||
|
|
||||||
|
// Initialize app
|
||||||
|
var app = Angel();
|
||||||
|
app.logger = Logger('angel')..onRecord.listen(print);
|
||||||
|
|
||||||
|
app.fallback(
|
||||||
|
VirtualDirectory(app, const LocalFileSystem(),
|
||||||
|
source: swaggerUiDistDir, publicPath: 'swagger/')
|
||||||
|
.handleRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.dumpTree();
|
||||||
|
client = await connectTo(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => client.close());
|
||||||
|
|
||||||
|
test('prefix is not replaced in file paths', () async {
|
||||||
|
var response = await client.get('/swagger/swagger-ui.css');
|
||||||
|
print('Response: ${response.body}');
|
||||||
|
expect(response, hasBody(swaggerUiCssContents));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get a file without prefix in name', () async {
|
||||||
|
var response = await client.get('/swagger/test.js');
|
||||||
|
print('Response: ${response.body}');
|
||||||
|
expect(response, hasBody(swaggerTestJsContents));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trailing slash at root', () async {
|
||||||
|
var response = await client.get('/swagger');
|
||||||
|
var body1 = response.body;
|
||||||
|
print('Response #1: $body1');
|
||||||
|
|
||||||
|
response = await client.get('/swagger/');
|
||||||
|
var body2 = response.body;
|
||||||
|
print('Response #2: $body2');
|
||||||
|
|
||||||
|
expect(body1, body2);
|
||||||
|
});
|
||||||
|
}
|
1
packages/static/test/nested/index.txt
Normal file
1
packages/static/test/nested/index.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Bird
|
10
packages/static/test/node_modules/swagger-ui-dist/index.html
generated
vendored
Normal file
10
packages/static/test/node_modules/swagger-ui-dist/index.html
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Swagger...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
<i>Hooray for testing...</i>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
packages/static/test/node_modules/swagger-ui-dist/swagger-ui.css
generated
vendored
Normal file
3
packages/static/test/node_modules/swagger-ui-dist/swagger-ui.css
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
html, body {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
1
packages/static/test/node_modules/swagger-ui-dist/test.js
generated
vendored
Normal file
1
packages/static/test/node_modules/swagger-ui-dist/test.js
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log('foo');
|
53
packages/static/test/push_state_test.dart
Normal file
53
packages/static/test/push_state_test.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:file/memory.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
Angel app;
|
||||||
|
MemoryFileSystem fileSystem;
|
||||||
|
TestClient client;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
fileSystem = MemoryFileSystem();
|
||||||
|
|
||||||
|
var webDir = fileSystem.directory('web');
|
||||||
|
await webDir.create(recursive: true);
|
||||||
|
|
||||||
|
var indexFile = webDir.childFile('index.html');
|
||||||
|
await indexFile.writeAsString('index');
|
||||||
|
|
||||||
|
app = Angel();
|
||||||
|
|
||||||
|
var vDir = VirtualDirectory(
|
||||||
|
app,
|
||||||
|
fileSystem,
|
||||||
|
source: webDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
app
|
||||||
|
..fallback(vDir.handleRequest)
|
||||||
|
..fallback(vDir.pushState('index.html'))
|
||||||
|
..fallback((req, res) => 'Fallback');
|
||||||
|
|
||||||
|
app.logger = Logger('push_state')
|
||||||
|
..onRecord.listen(
|
||||||
|
(rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
client = await connectTo(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => client.close());
|
||||||
|
|
||||||
|
test('serves as fallback', () async {
|
||||||
|
var response = await client.get('/nope');
|
||||||
|
expect(response.body, 'index');
|
||||||
|
});
|
||||||
|
}
|
1
packages/static/test/sample.txt
Normal file
1
packages/static/test/sample.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Hello world
|
Loading…
Reference in a new issue