Add 'packages/static/' from commit '6ec67dde7bd318c5e82fce66e3b22dfa3a378c24'

git-subtree-dir: packages/static
git-subtree-mainline: 17ad5f82c3
git-subtree-split: 6ec67dde7b
This commit is contained in:
Tobe O 2020-02-15 18:21:58 -05:00
commit f7c6ebf200
24 changed files with 1156 additions and 0 deletions

68
packages/static/.gitignore vendored Normal file
View 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
# Dont 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

View file

@ -0,0 +1,4 @@
language: dart
dart:
- dev
- stable

View 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
View 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
View 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.

View file

@ -0,0 +1,8 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
linter:
rules:
- unnecessary_const
- unnecessary_new

View 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}');
}

View file

@ -0,0 +1,4 @@
library angel_static;
export 'src/cache.dart';
export 'src/virtual_directory.dart';

View 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 }

View 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;
}
}

View 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

View file

@ -0,0 +1,2 @@
# hello
world!

View 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!"));
});
}

View 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}');
}

View 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')));
});
}

View file

@ -0,0 +1 @@
<h1>{{foo}}</h1>

View file

@ -0,0 +1 @@
index!

View 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);
});
}

View file

@ -0,0 +1 @@
Bird

View file

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

View file

@ -0,0 +1,3 @@
html, body {
font-weight: bold;
}

View file

@ -0,0 +1 @@
console.log('foo');

View 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');
});
}

View file

@ -0,0 +1 @@
Hello world