diff --git a/packages/static/CHANGELOG.md b/packages/static/CHANGELOG.md index d7b9783d..c8a4c2b0 100644 --- a/packages/static/CHANGELOG.md +++ b/packages/static/CHANGELOG.md @@ -1,80 +1,110 @@ -# 4.0.0 +# Change Log + +## 4.0.1 + +* Fixed `push_state_test` unit test failure on Windows +* Fixed NNBD related issues +* Added logging to `VirtualDirectory` and `CachingVirtualDirectory` to log exception + +## 4.0.0 + * Migrated to support Dart SDK 2.12.x NNBD -# 3.0.0 +## 3.0.0 + * Migrated to work with Dart SDK 2.12.x Non NNBD -# 2.1.3+2 +## 2.1.3+2 + * Prepare for upcoming change to File.openRead() -# 2.1.3+1 +## 2.1.3+1 + * Apply control flow lints. -# 2.1.3 +## 2.1.3 + * Apply lints. * Pin to Dart `>=2.0.0 <3.0.0`. * Use at least version `2.0.0-rc.0` of `angel_framework`. -# 2.1.2+1 +## 2.1.2+1 + * Fix a typo that prevented `Range` requests from working. -# 2.1.2 +## 2.1.2 + * Patch support for range+streaming in Caching server. -# 2.1.1 +## 2.1.1 + * URI-encode paths in directory listing. This produces correct URL's, always. -# 2.1.0 +## 2.1.0 + * Include support for the `Range` header. * Use MD5 for etags, instead of a weak ETag. -# 2.0.2 +## 2.0.2 + * Fixed invalid HTML for directory listings. -# 2.0.1 +## 2.0.1 + * Remove use of `sendFile`. * Add a `p.isWithin` check to ensure that paths do not escape the `source` directory. * Handle `HEAD` requests. -# 2.0.0 +## 2.0.0 + * Upgrade dependencies to Angel 2 + file@5. * Replace `useStream` with `useBuffer`. * Remove `package:intl`, just use `HttpDate` instead. -# 1.3.0+1 +## 1.3.0+1 + * Dart 2 fixes. * Enable optionally writing responses to the buffer instead of streaming. -# 1.3.0 +## 1.3.0 + * `pushState` uses `strict` mode when `accepts` is passed. -# 1.3.0-alpha+2 +## 1.3.0-alpha+2 + * Added an `accepts` option to `pushState`. * Added optional directory listings. -# 1.3.0-alpha+1 +## 1.3.0-alpha+1 + * ETags once again only encode the first 50 bytes of files. Resolves [#27](https://github.com/angel-dart/static/issues/27). -# 1.3.0-alpha +## 1.3.0-alpha + * Removed file transformers. * `VirtualDirectory` is no longer an `AngelPlugin`, and instead exposes a `handleRequest` middleware. * Added `pushState` to `VirtualDirectory`. -# 1.2.5 +## 1.2.5 + * Fixed a bug where `onlyInProduction` was not properly adhered to. * Fixed another bug where `Accept-Encoding` was not properly adhered to. * Setting `maxAge` to `null` will now prevent a `CachingVirtualDirectory` from sending an `Expires` header. * Pre-built assets can now be mass-deleted with `VirtualDirectory.cleanFromDisk()`. Resolves [#22](https://github.com/angel-dart/static/issues/22). -# 1.2.4+1 +## 1.2.4+1 + Fixed a bug where `Accept-Encoding` was not properly adhered to. -# 1.2.4 -Fixes https://github.com/angel-dart/angel/issues/44. +## 1.2.4 + +Fixes . + * MIME types will now default to `application/octet-stream`. * When `streamToIO` is `true`, the body will only be sent gzipped if the request explicitly allows it. -# 1.2.3 +## 1.2.3 + Fixed #40 and #41, which dealt with paths being improperly served when using a `publicPath`. diff --git a/packages/static/README.md b/packages/static/README.md index dabe366c..31276083 100644 --- a/packages/static/README.md +++ b/packages/static/README.md @@ -1,16 +1,17 @@ -# angel3_static +# Angel3 Static Files Service + [![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_static) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) [![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) [![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/static/LICENSE) - -Static server infrastructure for Angel. +This package supports serving static files such as html, css and js for [Angel3 framework](https://pub.dartlang.org/packages/angel3). *Can also handle `Range` requests now, making it suitable for media streaming, ex. music, video, etc.* -# Installation +## Installation + In `pubspec.yaml`: ```yaml @@ -18,9 +19,9 @@ dependencies: angel3_static: ^4.0.0 ``` -# Usage -To serve files from a directory, you need to create a `VirtualDirectory`. -Keep in mind that `angel3_static` uses `package:file` instead of `dart:io`. +## Usage + +To serve files from a directory, you need to create a `VirtualDirectory`. Keep in mind that `angel3_static` uses `package:file` instead of `dart:io`. ```dart import 'package:angel3_framework/angel3_framework.dart'; @@ -46,10 +47,9 @@ void main() async { } ``` -# Push State -`VirtualDirectory` also exposes a `pushState` method that returns a -request handler that serves the file at a given path as a fallback, unless -the user is requesting that file. This can be very useful for SPA's. +## Push State + +`VirtualDirectory` also exposes a `pushState` method that returns a request handler that serves the file at a given path as a fallback, unless the user is requesting that file. This can be very useful for SPA's. ```dart // Create VirtualDirectory as well @@ -62,8 +62,10 @@ app.fallback(vDir.handleRequest); app.fallback(vDir.pushState('index.html')); ``` -# Options +## Options + The `VirtualDirectory` API accepts a few named parameters: + - **source**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `web` (in development) or `build/web` (in production), depending on your `ANGEL_ENV`. - **indexFileNames**: A `List` of filenames that should be served as index pages. Default is `['index.html']`. @@ -71,4 +73,4 @@ The `VirtualDirectory` API accepts a few named parameters: 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. \ No newline at end of file +then the callback's result will be sent to the user, instead of the file contents. diff --git a/packages/static/lib/src/cache.dart b/packages/static/lib/src/cache.dart index b25e0840..b85a2845 100644 --- a/packages/static/lib/src/cache.dart +++ b/packages/static/lib/src/cache.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io' show HttpDate; import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/file.dart'; +import 'package:logging/logging.dart'; import 'virtual_directory.dart'; /// Returns a string representation of the given [CacheAccessLevel]. @@ -18,6 +19,8 @@ String accessLevelToString(CacheAccessLevel accessLevel) { /// A `VirtualDirectory` that also sets `Cache-Control` headers. class CachingVirtualDirectory extends VirtualDirectory { + final _log = Logger('CachingVirtualDirectory'); + final Map _etags = {}; /// Either `PUBLIC` or `PRIVATE`. @@ -40,20 +43,20 @@ class CachingVirtualDirectory extends VirtualDirectory { CachingVirtualDirectory(Angel app, FileSystem fileSystem, {this.accessLevel = CacheAccessLevel.PUBLIC, Directory? source, - bool? debug, - Iterable? indexFileNames, + bool debug = false, + Iterable indexFileNames = const ['index.html'], this.maxAge = 0, this.noCache = false, this.onlyInProduction = false, this.useEtags = true, - bool? allowDirectoryListing, + bool allowDirectoryListing = false, bool useBuffer = false, - String? publicPath, + String publicPath = '/', Function(File file, RequestContext req, ResponseContext res)? callback}) : super(app, fileSystem, source: source, - indexFileNames: indexFileNames ?? ['index.html'], - publicPath: publicPath ?? '/', + indexFileNames: indexFileNames, + publicPath: publicPath, callback: callback, allowDirectoryListing: allowDirectoryListing, useBuffer: useBuffer); @@ -63,27 +66,35 @@ class CachingVirtualDirectory extends VirtualDirectory { File file, FileStat stat, RequestContext req, ResponseContext res) { res.headers['accept-ranges'] = 'bytes'; - if (onlyInProduction == true && req.app!.environment.isProduction != true) { + if (onlyInProduction == true && req.app?.environment.isProduction != true) { return super.serveFile(file, stat, req, res); } + if (req.headers == null) { + _log.severe('Missing headers in the RequestContext'); + throw ArgumentError('Missing headers in the RequestContext'); + } + var reqHeaders = req.headers!; + var shouldNotCache = noCache == true; if (!shouldNotCache) { - shouldNotCache = req.headers!.value('cache-control') == 'no-cache' || - req.headers!.value('pragma') == 'no-cache'; + shouldNotCache = reqHeaders.value('cache-control') == 'no-cache' || + reqHeaders.value('pragma') == 'no-cache'; } if (shouldNotCache) { res.headers['cache-control'] = 'private, max-age=0, no-cache'; return super.serveFile(file, stat, req, res); } else { - var ifModified = req.headers!.ifModifiedSince; + var ifModified = reqHeaders.ifModifiedSince; var ifRange = false; try { - ifModified = HttpDate.parse(req.headers!.value('if-range')!); - ifRange = true; + if (reqHeaders.value('if-range') != null) { + ifModified = HttpDate.parse(reqHeaders.value('if-range')!); + ifRange = true; + } } catch (_) { // Fail silently... } @@ -97,7 +108,9 @@ class CachingVirtualDirectory extends VirtualDirectory { setCachedHeaders(stat.modified, req, res); if (useEtags && _etags.containsKey(file.absolute.path)) { - res.headers['ETag'] = _etags[file.absolute.path]!; + if (_etags[file.absolute.path] != null) { + res.headers['ETag'] = _etags[file.absolute.path]!; + } } if (ifRange) { @@ -111,6 +124,8 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } } catch (_) { + _log.severe( + 'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.'); throw AngelHttpException.badRequest( message: 'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.'); @@ -120,18 +135,18 @@ class CachingVirtualDirectory extends VirtualDirectory { // If-modified didn't work; try etags if (useEtags == true) { - var etagsToMatchAgainst = req.headers!['if-none-match']; + var etagsToMatchAgainst = reqHeaders['if-none-match'] ?? []; ifRange = false; - if (etagsToMatchAgainst?.isNotEmpty != true) { - etagsToMatchAgainst = req.headers!['if-range']; - ifRange = etagsToMatchAgainst?.isNotEmpty == true; + if (etagsToMatchAgainst.isEmpty) { + etagsToMatchAgainst = reqHeaders['if-range'] ?? []; + ifRange = etagsToMatchAgainst.isNotEmpty; } - if (etagsToMatchAgainst?.isNotEmpty == true) { + if (etagsToMatchAgainst.isNotEmpty) { var hasBeenModified = false; - for (var etag in etagsToMatchAgainst!) { + for (var etag in etagsToMatchAgainst) { if (etag == '*') { hasBeenModified = true; } else { diff --git a/packages/static/lib/src/virtual_directory.dart b/packages/static/lib/src/virtual_directory.dart index 9df95ec1..f958ba6a 100644 --- a/packages/static/lib/src/virtual_directory.dart +++ b/packages/static/lib/src/virtual_directory.dart @@ -3,6 +3,7 @@ import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/file.dart'; import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart' as p; +import 'package:logging/logging.dart'; import 'package:angel3_range_header/angel3_range_header.dart'; final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); @@ -28,11 +29,13 @@ String _pathify(String path) { /// A static server plug-in. class VirtualDirectory { - String? _prefix; - Directory? _source; + final _log = Logger('VirtualDirectory'); + + late String _prefix; + late Directory _source; /// The directory to serve files from. - Directory? get source => _source; + Directory get source => _source; /// An optional callback to run before serving files. final Function(File file, RequestContext req, ResponseContext res)? callback; @@ -47,7 +50,7 @@ class VirtualDirectory { final String publicPath; /// If `true` (default: `false`), then if a directory does not contain any of the specific [indexFileNames], a default directory listing will be served. - final bool? allowDirectoryListing; + final bool allowDirectoryListing; /// If `true` (default: `true`), then files will be opened as streams and piped into the request. /// @@ -77,7 +80,7 @@ class VirtualDirectory { } var path = req.uri!.path.replaceAll(_straySlashes, ''); - if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix!)) { + if (_prefix.isNotEmpty == true && !path.startsWith(_prefix)) { return Future.value(true); } @@ -91,7 +94,7 @@ class VirtualDirectory { /// the view will be served. RequestHandler pushState(String path, {Iterable? accepts}) { var vPath = path.replaceAll(_straySlashes, ''); - if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath'; + if (_prefix.isNotEmpty == true) vPath = '$_prefix/$vPath'; return (RequestContext req, ResponseContext res) { var path = req.path.replaceAll(_straySlashes, ''); @@ -110,22 +113,28 @@ class VirtualDirectory { /// Writes the file at the given virtual [path] to a response. Future servePath( String path, RequestContext req, ResponseContext res) async { - if (_prefix!.isNotEmpty) { + if (_prefix.isNotEmpty) { // Only replace the *first* incidence // Resolve: https://github.com/angel-dart/angel/issues/41 - path = path.replaceFirst(RegExp('^' + _pathify(_prefix!)), ''); + path = path.replaceFirst(RegExp('^' + _pathify(_prefix)), ''); } if (path.isEmpty) path = '.'; path = path.replaceAll(_straySlashes, ''); - var absolute = source!.absolute.uri.resolve(path).toFilePath(); - var parent = source!.absolute.uri.toFilePath(); + var absolute = source.absolute.uri.resolve(path).toFilePath(); + var parent = source.absolute.uri.toFilePath(); if (!p.isWithin(parent, absolute) && !p.equals(parent, absolute)) { return true; } + // Replace the separator when running on Windows with file system + // detected as Linux + if (absolute.contains('\\') && fileSystem.path.separator == '/') { + absolute = absolute.replaceAll('\\', '/'); + } + var stat = await fileSystem.stat(absolute); return await serveStat(absolute, path, stat, req, res); } @@ -199,8 +208,8 @@ class VirtualDirectory { }); for (var entity in entities) { - String? stub; - String? type; + String stub; + String type; if (entity is File) { type = '[File]'; @@ -213,23 +222,24 @@ class VirtualDirectory { stub = p.basename(entity.path); } else { //TODO: Handle unknown type - + _log.severe('Unknown file entity. Not a file, directory or link.'); + type = '[]'; + stub = ''; } var href = stub; if (relative.isNotEmpty) { - stub ??= ''; href = '/' + relative + '/' + stub; } if (entity is Directory) { - if (href == null) { + if (href == '') { href = '/'; } else { href += '/'; } } - href = Uri.encodeFull(href!); + href = Uri.encodeFull(href); res.write('
  • $type $stub
  • '); } @@ -242,12 +252,13 @@ class VirtualDirectory { } void _ensureContentTypeAllowed(String mimeType, RequestContext req) { - var value = req.headers!.value('accept'); + var value = req.headers?.value('accept'); var acceptable = value == null || value.isNotEmpty != true || (mimeType.isNotEmpty == true && value.contains(mimeType) == true) || value.contains('*/*') == true; if (!acceptable) { + _log.severe('Mime type [$value] is not supported'); throw AngelHttpException( UnsupportedError( 'Client requested $value, but server wanted to send $mimeType.'), @@ -262,11 +273,12 @@ class VirtualDirectory { res.headers['accept-ranges'] = 'bytes'; if (callback != null) { - return await req.app!.executeHandler( - (RequestContext req, ResponseContext res) => - callback!(file, req, res), - req, - res); + return await req.app?.executeHandler( + (RequestContext req, ResponseContext res) => + callback!(file, req, res), + req, + res) ?? + true; } var type = @@ -277,14 +289,21 @@ class VirtualDirectory { res.contentType = MediaType.parse(type); if (useBuffer == true) res.useBuffer(); - if (req.headers!.value('range')?.startsWith('bytes=') != true) { + if (req.headers == null) { + _log.severe('Missing headers in the RequestContext'); + throw ArgumentError('Missing headers in the RequestContext'); + } + var reqHeaders = req.headers!; + + if (reqHeaders.value('range')?.startsWith('bytes=') != true) { await res.streamFile(file); } else { - var header = RangeHeader.parse(req.headers!.value('range')!); + var header = RangeHeader.parse(reqHeaders.value('range')!); var items = RangeHeader.foldItems(header.items); - var totalFileSize = await file.length(); header = RangeHeader(items); + var totalFileSize = await file.length(); + for (var item in header.items) { var invalid = false; diff --git a/packages/static/pubspec.yaml b/packages/static/pubspec.yaml index 81fa4f45..11bc1faa 100644 --- a/packages/static/pubspec.yaml +++ b/packages/static/pubspec.yaml @@ -1,21 +1,22 @@ name: angel3_static description: Static server middleware for Angel. Also capable of serving Range responses. -version: 4.0.0 -homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/static +version: 4.0.1 +homepage: https://github.com/dukefirehawk/angel +repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/static environment: sdk: '>=2.12.0 <3.0.0' dependencies: - angel3_framework: ^4.0.0 + angel3_framework: ^4.1.0 angel3_range_header: ^3.0.0 convert: ^3.0.0 crypto: ^3.0.1 file: ^6.1.0 http_parser: ^4.0.0 path: ^1.8.0 + logging: ^1.0.1 dev_dependencies: angel3_test: ^4.0.0 http: ^0.13.2 - logging: ^1.0.1 matcher: ^0.12.10 pedantic: ^1.11.0 test: ^1.17.4 diff --git a/packages/static/test/web/index.html b/packages/static/test/web/index.html new file mode 100644 index 00000000..b9579c3a --- /dev/null +++ b/packages/static/test/web/index.html @@ -0,0 +1,10 @@ + + + + Push Test + + +

    Hello!

    + Hooray for testing... + +