From 58d5d0b2e78c039b825e181a1d2acde912fced7e Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 21 Apr 2016 17:27:22 -0400 Subject: [PATCH 01/67] First commit --- .gitignore | 65 +++++++++++++++++++++++++++++++++++++++++++ README.md | 29 +++++++++++++++++++ lib/angel_static.dart | 26 +++++++++++++++++ pubspec.yaml | 11 ++++++++ test/all_tests.dart | 0 test/sample.txt | 1 + 6 files changed, 132 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/angel_static.dart create mode 100644 pubspec.yaml create mode 100644 test/all_tests.dart create mode 100644 test/sample.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..917abe7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# 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 + diff --git a/README.md b/README.md new file mode 100644 index 00000000..f792efc4 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# angel_static +Static server middleware for Angel. + +# Installation +In `pubspec.yaml`: + + dependencies: + angel_framework: ^0.0.0-dev + angel_static: ^1.0.0-beta + +# Usage +As with all Angel middleware, this can be used simply via a function +call within the route declaration, or registered under a name and invoked +under that same name. + +```dart +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; + +main() async { + Angel angel = new Angel(); + angel.registerMiddleware("static", serveStatic(new Directory("build/web"))); + angel.get("*", "static"); + + await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 8080); +} +``` + diff --git a/lib/angel_static.dart b/lib/angel_static.dart new file mode 100644 index 00000000..8fd5f499 --- /dev/null +++ b/lib/angel_static.dart @@ -0,0 +1,26 @@ +library angel_static; + +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mime/mime.dart' show lookupMimeType; + +/// Serves files statically from a given directory. +Middleware serveStatic(Directory sourceDirectory) { + return (RequestContext req, ResponseContext res) async { + String requested = req.path.replaceAll(new RegExp(r'^\/'), ''); + File file = new File.fromUri( + sourceDirectory.absolute.uri.resolve(requested)); + if (await file.exists()) { + res + ..willCloseItself = true + ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) + ..status(200); + await res.streamFile(file); + await res.underlyingResponse.close(); + return false; + } + + return true; + }; +} + diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..0404f607 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,11 @@ +name: angel_static +description: Static server middleware for Angel. +homepage: https://github.com/angel-dart/angel_static +author: thosakwe +version: 1.0.0-beta +dependencies: + angel_framework: ^0.0.0-dev + mime: ^0.9.3 +dev_dependencies: + http: ^0.11.3 + test: ^0.12.13 \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart new file mode 100644 index 00000000..e69de29b diff --git a/test/sample.txt b/test/sample.txt new file mode 100644 index 00000000..70c379b6 --- /dev/null +++ b/test/sample.txt @@ -0,0 +1 @@ +Hello world \ No newline at end of file From 232a78dec84589bf3fbae0face05476b35d9108f Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 21 Apr 2016 17:29:34 -0400 Subject: [PATCH 02/67] Initial commit --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..eb4ce33e --- /dev/null +++ b/LICENSE @@ -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. From 55b159b5bd23f9504ff5cb7ff95c1f95f48c4e89 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 21 Apr 2016 17:36:02 -0400 Subject: [PATCH 03/67] Released by accident, oopsie --- pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0404f607..4004fde8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,8 +4,8 @@ homepage: https://github.com/angel-dart/angel_static author: thosakwe version: 1.0.0-beta dependencies: - angel_framework: ^0.0.0-dev - mime: ^0.9.3 + angel_framework: ">=0.0.0-dev < 0.1.0" + mime: ">= 0.9.3 < 0.10.0" dev_dependencies: - http: ^0.11.3 - test: ^0.12.13 \ No newline at end of file + http: ">= 0.11.3 < 0.12.0" + test: ">= 0.12.13 < 0.13.0" From 9892f43749c1c639c7b8d03e6b0964a54362abaf Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 18:44:05 -0400 Subject: [PATCH 04/67] Tests are a go --- pubspec.yaml | 2 +- test/all_tests.dart | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4004fde8..80a0a7b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.0.0-beta +version: 1.0.0-beta.1 dependencies: angel_framework: ">=0.0.0-dev < 0.1.0" mime: ">= 0.9.3 < 0.10.0" diff --git a/test/all_tests.dart b/test/all_tests.dart index e69de29b..e1861de3 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -0,0 +1,37 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:http/http.dart' show Client; +import 'package:test/test.dart'; + +main() { + group('angel_static', () { + Angel angel; + String url; + Client client = new Client(); + + setUp(() async { + angel = new Angel(); + angel.registerMiddleware("static", serveStatic(new Directory("test"))); + angel.get("*", "Fallback", middleware: ["static"]); + + await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; + }); + + tearDown(() async { + await angel.httpServer.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[HttpHeaders.CONTENT_TYPE], equals("text/plain")); + }); + + test('non-existent files are skipped', () async { + var response = await client.get("$url/nonexist.ent"); + expect(response.body, equals('"Fallback"')); + }); + }); +} \ No newline at end of file From 3bff493d36edb3b52f328e832addb7f517a05243 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 21 Apr 2016 22:03:30 -0400 Subject: [PATCH 05/67] Default to build/web or web --- lib/angel_static.dart | 9 ++++++++- pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/angel_static.dart b/lib/angel_static.dart index 8fd5f499..1aabc93b 100644 --- a/lib/angel_static.dart +++ b/lib/angel_static.dart @@ -5,7 +5,14 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:mime/mime.dart' show lookupMimeType; /// Serves files statically from a given directory. -Middleware serveStatic(Directory sourceDirectory) { +Middleware serveStatic([Directory sourceDirectory]) { + if (sourceDirectory == null) { + String dirPath = Platform.environment['ANGEL_ENV'] == 'production' + ? './build/web' + : './web'; + sourceDirectory = new Directory(dirPath); + } + return (RequestContext req, ResponseContext res) async { String requested = req.path.replaceAll(new RegExp(r'^\/'), ''); File file = new File.fromUri( diff --git a/pubspec.yaml b/pubspec.yaml index 80a0a7b8..bbfd262b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.0.0-beta.1 +version: 1.0.0-beta.2 dependencies: angel_framework: ">=0.0.0-dev < 0.1.0" mime: ">= 0.9.3 < 0.10.0" From 74e01cf57674a671a57333273eb1d030e73384c3 Mon Sep 17 00:00:00 2001 From: regiostech Date: Mon, 2 May 2016 19:11:25 -0400 Subject: [PATCH 06/67] 1.0.0 --- README.md | 11 ++++++++++- lib/angel_static.dart | 40 ++++++++++++++++++++++++++++++++-------- pubspec.yaml | 2 +- test/all_tests.dart | 19 ++++++++++++++++++- test/index.txt | 1 + 5 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 test/index.txt diff --git a/README.md b/README.md index f792efc4..9501e580 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,19 @@ import 'package:angel_static/angel_static.dart'; main() async { Angel angel = new Angel(); - angel.registerMiddleware("static", serveStatic(new Directory("build/web"))); + angel.registerMiddleware("static", serveStatic()); + angel.get('/virtual*', serveStatic(virtualRoot: '/virtual')); angel.get("*", "static"); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 8080); } ``` +# Options +`serveStatic` accepts two named parameters. +- **sourceDirectory**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `build` or + `build/web`, depending on your `ANGEL_ENV`. +- **indexFileNames**: A `List _sendFile(File file, ResponseContext res) async { + res + ..willCloseItself = true + ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) + ..status(200); + await res.streamFile(file); + await res.underlyingResponse.close(); + return false; +} + /// Serves files statically from a given directory. -Middleware serveStatic([Directory sourceDirectory]) { +RequestMiddleware serveStatic({ +Directory sourceDirectory, +List indexFileNames: const['index.html'], +String virtualRoot: '/' +}) { if (sourceDirectory == null) { String dirPath = Platform.environment['ANGEL_ENV'] == 'production' ? './build/web' @@ -13,18 +28,27 @@ Middleware serveStatic([Directory sourceDirectory]) { sourceDirectory = new Directory(dirPath); } + RegExp requestingIndex = new RegExp(r'^((\/)|(\\))*$'); + return (RequestContext req, ResponseContext res) async { String requested = req.path.replaceAll(new RegExp(r'^\/'), ''); File file = new File.fromUri( sourceDirectory.absolute.uri.resolve(requested)); if (await file.exists()) { - res - ..willCloseItself = true - ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) - ..status(200); - await res.streamFile(file); - await res.underlyingResponse.close(); - return false; + return await _sendFile(file, res); + } + + // Try to resolve index + String relative = req.path.replaceFirst(virtualRoot, "") + .replaceAll(new RegExp(r'^\/+'), ""); + if (requestingIndex.hasMatch(relative) || relative.isEmpty) { + for (String indexFileName in indexFileNames) { + file = + new File.fromUri(sourceDirectory.absolute.uri.resolve(indexFileName)); + if (await file.exists()) { + return await _sendFile(file, res); + } + } } return true; diff --git a/pubspec.yaml b/pubspec.yaml index bbfd262b..e0ac55cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.0.0-beta.2 +version: 1.0.0 dependencies: angel_framework: ">=0.0.0-dev < 0.1.0" mime: ">= 0.9.3 < 0.10.0" diff --git a/test/all_tests.dart b/test/all_tests.dart index e1861de3..da7c9412 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -12,7 +12,14 @@ main() { setUp(() async { angel = new Angel(); - angel.registerMiddleware("static", serveStatic(new Directory("test"))); + angel.registerMiddleware( + "static", serveStatic(sourceDirectory: new Directory("test"), + indexFileNames: ['index.php', 'index.txt'])); + angel.get('/virtual/*', "Fallback", + middleware: [serveStatic(sourceDirectory: new Directory("test"), + virtualRoot: '/virtual', + indexFileNames: ['index.txt']) + ]); angel.get("*", "Fallback", middleware: ["static"]); await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); @@ -33,5 +40,15 @@ main() { 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!")); + }); }); } \ No newline at end of file diff --git a/test/index.txt b/test/index.txt new file mode 100644 index 00000000..cb655f03 --- /dev/null +++ b/test/index.txt @@ -0,0 +1 @@ +index! \ No newline at end of file From 61bd521dba43937ff8471649f69a893d73f967af Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 19:18:12 -0400 Subject: [PATCH 07/67] Updated Angel dep --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index e0ac55cb..d0c0ad31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,9 +2,9 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.0.0 +version: 1.0.1 dependencies: - angel_framework: ">=0.0.0-dev < 0.1.0" + angel_framework: ">=1.0.0-dev < 2.0.0" mime: ">= 0.9.3 < 0.10.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" From 63be4d3608c7517b37eb4364d79d78a3c43e9063 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 21 Jun 2016 19:18:34 -0400 Subject: [PATCH 08/67] Updated Angel dep --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9501e580..92546e61 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Static server middleware for Angel. In `pubspec.yaml`: dependencies: - angel_framework: ^0.0.0-dev - angel_static: ^1.0.0-beta + angel_framework: ^1.0.0-dev + angel_static: ^1.0.0 # Usage As with all Angel middleware, this can be used simply via a function From d2e8d413917a97da46837aab21f628480203e73a Mon Sep 17 00:00:00 2001 From: Tobe O Date: Fri, 24 Jun 2016 18:23:32 -0400 Subject: [PATCH 09/67] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92546e61..b816184a 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ main() async { # Options `serveStatic` accepts two named parameters. -- **sourceDirectory**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `build` or - `build/web`, depending on your `ANGEL_ENV`. +- **sourceDirectory**: 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 Date: Fri, 1 Jul 2016 16:33:42 -0400 Subject: [PATCH 10/67] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b816184a..abf9ebdd 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ Static server middleware for Angel. # Installation In `pubspec.yaml`: - dependencies: - angel_framework: ^1.0.0-dev - angel_static: ^1.0.0 +```yaml +dependencies: + angel_framework: ^1.0.0-dev + angel_static: ^1.0.0 +``` # Usage As with all Angel middleware, this can be used simply via a function From cbe8deccdc959221375ebbe9ee2a641202dc38ad Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 23 Nov 2016 04:35:47 -0500 Subject: [PATCH 11/67] Create .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3939d628 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart From 3112f212b38fa8f5d0e483ee2dbee67dc466e84b Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 23 Nov 2016 04:37:28 -0500 Subject: [PATCH 12/67] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index abf9ebdd..b101adbf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # angel_static + +![version 1.0.1](https://img.shields.io/badge/version-1.0.1-green.svg) +![build status](https://travis-ci.org/angel-dart/static.svg?branch=master) + Static server middleware for Angel. # Installation From eef450cfc938f7379ff97a55e61f6e23f0992fb3 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 23 Nov 2016 04:41:41 -0500 Subject: [PATCH 13/67] Rename all_tests.dart to all_test.dart --- test/{all_tests.dart => all_test.dart} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{all_tests.dart => all_test.dart} (99%) diff --git a/test/all_tests.dart b/test/all_test.dart similarity index 99% rename from test/all_tests.dart rename to test/all_test.dart index da7c9412..23d1e389 100644 --- a/test/all_tests.dart +++ b/test/all_test.dart @@ -51,4 +51,4 @@ main() { expect(response.body, equals("index!")); }); }); -} \ No newline at end of file +} From e7d5f6428f9d22e214ff22e04653d282fdb5e45a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 12:22:23 -0500 Subject: [PATCH 14/67] Will be finished when router can flatten properly... --- README.md | 30 ++++++----- lib/angel_static.dart | 57 +-------------------- lib/src/serve_static.dart | 11 ++++ lib/src/virtual_directory.dart | 94 ++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- test/all_test.dart | 86 +++++++++++++++++-------------- 6 files changed, 172 insertions(+), 108 deletions(-) create mode 100644 lib/src/serve_static.dart create mode 100644 lib/src/virtual_directory.dart diff --git a/README.md b/README.md index b101adbf..f7b4f14a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -![version 1.0.1](https://img.shields.io/badge/version-1.0.1-green.svg) +![version 1.1.0-dev](https://img.shields.io/badge/version-1.1.0--dev-red.svg) ![build status](https://travis-ci.org/angel-dart/static.svg?branch=master) Static server middleware for Angel. @@ -11,13 +11,12 @@ In `pubspec.yaml`: ```yaml dependencies: angel_framework: ^1.0.0-dev - angel_static: ^1.0.0 + angel_static: ^1.1.0-dev ``` # Usage -As with all Angel middleware, this can be used simply via a function -call within the route declaration, or registered under a name and invoked -under that same name. +To serve files from a directory, your app needs to have a +`VirtualDirectory` mounted on it. ```dart import 'dart:io'; @@ -25,20 +24,23 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_static/angel_static.dart'; main() async { - Angel angel = new Angel(); - angel.registerMiddleware("static", serveStatic()); - angel.get('/virtual*', serveStatic(virtualRoot: '/virtual')); - angel.get("*", "static"); - - await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 8080); + final app = new Angel(); + + app.mount('/virtual', new VirtualDirectory( + source: new Directory('./public'), + publicPath: '/virtual')); + app.mount('/', new VirtualDirectory(source: new Directory('./public'))); + + await app.startServer(); } ``` # Options -`serveStatic` accepts two named parameters. -- **sourceDirectory**: A `Directory` containing the files to be served. If left null, then Angel will serve either from `web` (in development) or +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 _sendFile(File file, ResponseContext res) async { - res - ..willCloseItself = true - ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) - ..status(200); - await res.streamFile(file); - await res.underlyingResponse.close(); - return false; -} - -/// Serves files statically from a given directory. -RequestMiddleware serveStatic({ -Directory sourceDirectory, -List indexFileNames: const['index.html'], -String virtualRoot: '/' -}) { - if (sourceDirectory == null) { - String dirPath = Platform.environment['ANGEL_ENV'] == 'production' - ? './build/web' - : './web'; - sourceDirectory = new Directory(dirPath); - } - - RegExp requestingIndex = new RegExp(r'^((\/)|(\\))*$'); - - return (RequestContext req, ResponseContext res) async { - String requested = req.path.replaceAll(new RegExp(r'^\/'), ''); - File file = new File.fromUri( - sourceDirectory.absolute.uri.resolve(requested)); - if (await file.exists()) { - return await _sendFile(file, res); - } - - // Try to resolve index - String relative = req.path.replaceFirst(virtualRoot, "") - .replaceAll(new RegExp(r'^\/+'), ""); - if (requestingIndex.hasMatch(relative) || relative.isEmpty) { - for (String indexFileName in indexFileNames) { - file = - new File.fromUri(sourceDirectory.absolute.uri.resolve(indexFileName)); - if (await file.exists()) { - return await _sendFile(file, res); - } - } - } - - return true; - }; -} - +export 'src/serve_static.dart'; +export 'src/virtual_directory.dart'; diff --git a/lib/src/serve_static.dart b/lib/src/serve_static.dart new file mode 100644 index 00000000..53cd6698 --- /dev/null +++ b/lib/src/serve_static.dart @@ -0,0 +1,11 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; + +@deprecated +RequestMiddleware serveStatic( + {Directory sourceDirectory, + List indexFileNames: const ['index.html'], + String virtualRoot: '/'}) { + throw new Exception( + 'The `serveStatic` API is now deprecated. Please update your application to use the new `VirtualDirectory` API.'); +} diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart new file mode 100644 index 00000000..5037d5a9 --- /dev/null +++ b/lib/src/virtual_directory.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mime/mime.dart' show lookupMimeType; + +final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); +final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); + +String _pathify(String path) { + var p = path.replaceAll(_straySlashes, ''); + + Map 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; +} + +class VirtualDirectory extends Router { + Directory _source; + Directory get source => _source; + final List indexFileNames; + final String publicPath; + + VirtualDirectory( + {Directory source, + bool debug: false, + this.indexFileNames: const ['index.html'], + this.publicPath: '/'}) + : super(debug: debug) { + if (source != null) { + _source = source; + } else { + String dirPath = Platform.environment['ANGEL_ENV'] == 'production' + ? './build/web' + : './web'; + _source = new Directory(dirPath); + } + + final prefix = publicPath.replaceAll(_straySlashes, ''); + _printDebug('Source directory: ${source.absolute.path}'); + _printDebug('Public path prefix: "$prefix"'); + + get('*', (RequestContext req, ResponseContext res) async { + var path = req.path.replaceAll(_straySlashes, ''); + + if (prefix.isNotEmpty) { + path = path.replaceAll(new RegExp('^' + _pathify(prefix)), ''); + } + + final file = new File.fromUri(source.absolute.uri.resolve(path)); + _printDebug('Attempting to statically serve file: ${file.absolute.path}'); + + if (await file.exists()) { + return sendFile(file, res); + } else { + // Try to resolve index + if (path.isEmpty) { + for (String indexFileName in indexFileNames) { + final index = + new File.fromUri(source.absolute.uri.resolve(indexFileName)); + if (await index.exists()) { + return await sendFile(index, res); + } + } + } else { + _printDebug('File "$path" does not exist, and is not an index.'); + return true; + } + } + }); + } + + _printDebug(msg) { + if (debug) print(msg); + } + + Future sendFile(File file, ResponseContext res) async { + _printDebug('Streaming file ${file.absolute.path}...'); + res + ..willCloseItself = true + ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) + ..status(200); + await res.streamFile(file); + await res.underlyingResponse.close(); + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d0c0ad31..b593fd00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.0.1 +version: 1.1.0-dev dependencies: angel_framework: ">=1.0.0-dev < 2.0.0" mime: ">= 0.9.3 < 0.10.0" diff --git a/test/all_test.dart b/test/all_test.dart index 23d1e389..a4a7baaa 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -5,50 +5,60 @@ import 'package:http/http.dart' show Client; import 'package:test/test.dart'; main() { - group('angel_static', () { - Angel angel; - String url; - Client client = new Client(); + Angel app; + Directory testDir = new Directory('test'); + String url; + Client client = new Client(); - setUp(() async { - angel = new Angel(); - angel.registerMiddleware( - "static", serveStatic(sourceDirectory: new Directory("test"), - indexFileNames: ['index.php', 'index.txt'])); - angel.get('/virtual/*', "Fallback", - middleware: [serveStatic(sourceDirectory: new Directory("test"), - virtualRoot: '/virtual', - indexFileNames: ['index.txt']) - ]); - angel.get("*", "Fallback", middleware: ["static"]); + setUp(() async { + app = new Angel(); - await angel.startServer(InternetAddress.LOOPBACK_IP_V4, 0); - url = "http://${angel.httpServer.address.host}:${angel.httpServer.port}"; - }); + app.mount( + '/virtual', + new VirtualDirectory( + debug: true, + source: testDir, + publicPath: '/virtual', + indexFileNames: ['index.txt'])); - tearDown(() async { - await angel.httpServer.close(force: true); - }); + app.mount( + '/', + new VirtualDirectory( + debug: true, + source: testDir, + indexFileNames: ['index.php', 'index.txt'])); - 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[HttpHeaders.CONTENT_TYPE], equals("text/plain")); - }); + app.get('*', 'Fallback'); + app + ..normalize() + ..dumpTree(); - test('non-existent files are skipped', () async { - var response = await client.get("$url/nonexist.ent"); - expect(response.body, equals('"Fallback"')); - }); + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); - test('can match index files', () async { - var response = await client.get(url); - expect(response.body, equals("index!")); - }); + tearDown(() async { + await app.httpServer.close(force: true); + }); - test('virtualRoots can match index', () async { - var response = await client.get("$url/virtual"); - expect(response.body, equals("index!")); - }); + 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[HttpHeaders.CONTENT_TYPE], equals("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!")); }); } From ac2058be59cc4483c0db64eb7044ec5b1007ef03 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 12:23:17 -0500 Subject: [PATCH 15/67] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7b4f14a..9618bb21 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ main() async { final app = new Angel(); app.mount('/virtual', new VirtualDirectory( - source: new Directory('./public'), + source: new Directory('./foo/bar'), publicPath: '/virtual')); + app.mount('/', new VirtualDirectory(source: new Directory('./public'))); await app.startServer(); From 8fc27551fa12f51465a120ca30ddc82c3f823614 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 12:24:04 -0500 Subject: [PATCH 16/67] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9618bb21..9a442161 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ main() async { 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']`. - **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. From 196f79af9444c4872a05432490a10186f892cbb1 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 15:14:05 -0500 Subject: [PATCH 17/67] Breaking changes ;) --- README.md | 8 +-- lib/src/virtual_directory.dart | 90 ++++++++++++++++++++-------------- test/all_test.dart | 29 +++++------ 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 9a442161..9e186754 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,7 @@ import 'package:angel_static/angel_static.dart'; main() async { final app = new Angel(); - - app.mount('/virtual', new VirtualDirectory( - source: new Directory('./foo/bar'), - publicPath: '/virtual')); - - app.mount('/', new VirtualDirectory(source: new Directory('./public'))); - + await app.configure(new VirtualDirectory(source: new Directory('./public'))); await app.startServer(); } ``` diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 5037d5a9..a16ffce4 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_route/angel_route.dart'; import 'package:mime/mime.dart' show lookupMimeType; final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); @@ -22,7 +23,9 @@ String _pathify(String path) { return p; } -class VirtualDirectory extends Router { +class VirtualDirectory { + final bool debug; + String _prefix; Directory _source; Directory get source => _source; final List indexFileNames; @@ -30,10 +33,11 @@ class VirtualDirectory extends Router { VirtualDirectory( {Directory source, - bool debug: false, + this.debug: false, this.indexFileNames: const ['index.html'], - this.publicPath: '/'}) - : super(debug: debug) { + this.publicPath: '/'}) { + _prefix = publicPath.replaceAll(_straySlashes, ''); + if (source != null) { _source = source; } else { @@ -42,45 +46,14 @@ class VirtualDirectory extends Router { : './web'; _source = new Directory(dirPath); } - - final prefix = publicPath.replaceAll(_straySlashes, ''); - _printDebug('Source directory: ${source.absolute.path}'); - _printDebug('Public path prefix: "$prefix"'); - - get('*', (RequestContext req, ResponseContext res) async { - var path = req.path.replaceAll(_straySlashes, ''); - - if (prefix.isNotEmpty) { - path = path.replaceAll(new RegExp('^' + _pathify(prefix)), ''); - } - - final file = new File.fromUri(source.absolute.uri.resolve(path)); - _printDebug('Attempting to statically serve file: ${file.absolute.path}'); - - if (await file.exists()) { - return sendFile(file, res); - } else { - // Try to resolve index - if (path.isEmpty) { - for (String indexFileName in indexFileNames) { - final index = - new File.fromUri(source.absolute.uri.resolve(indexFileName)); - if (await index.exists()) { - return await sendFile(index, res); - } - } - } else { - _printDebug('File "$path" does not exist, and is not an index.'); - return true; - } - } - }); } _printDebug(msg) { if (debug) print(msg); } + call(AngelBase app) async => serve(app); + Future sendFile(File file, ResponseContext res) async { _printDebug('Streaming file ${file.absolute.path}...'); res @@ -88,7 +61,48 @@ class VirtualDirectory extends Router { ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) ..status(200); await res.streamFile(file); - await res.underlyingResponse.close(); + await res.io.close(); return false; } + + void serve(Router router) { + _printDebug('Source directory: ${source.absolute.path}'); + _printDebug('Public path prefix: "$_prefix"'); + + handler(RequestContext req, ResponseContext res) async { + var path = req.path.replaceAll(_straySlashes, ''); + + return serveFile(path, res); + } + + router.get('$publicPath/*', handler); + router.get(publicPath, (req, res) => serveFile('', res)); + } + + serveFile(String path, ResponseContext res) async { + if (_prefix.isNotEmpty) { + path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), ''); + } + + final file = new File.fromUri(source.absolute.uri.resolve(path)); + _printDebug('Attempting to statically serve file: ${file.absolute.path}'); + + if (await file.exists()) { + return sendFile(file, res); + } else { + // Try to resolve index + if (path.isEmpty) { + for (String indexFileName in indexFileNames) { + final index = + new File.fromUri(source.absolute.uri.resolve(indexFileName)); + if (await index.exists()) { + return await sendFile(index, res); + } + } + } else { + _printDebug('File "$path" does not exist, and is not an index.'); + return true; + } + } + } } diff --git a/test/all_test.dart b/test/all_test.dart index a4a7baaa..9b41167c 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -11,34 +11,31 @@ main() { Client client = new Client(); setUp(() async { - app = new Angel(); + app = new Angel(debug: true); - app.mount( - '/virtual', - new VirtualDirectory( - debug: true, - source: testDir, - publicPath: '/virtual', - indexFileNames: ['index.txt'])); + await app.configure(new VirtualDirectory( + debug: true, + source: testDir, + publicPath: '/virtual', + indexFileNames: ['index.txt'])); - app.mount( - '/', - new VirtualDirectory( - debug: true, - source: testDir, - indexFileNames: ['index.php', 'index.txt'])); + await app.configure(new VirtualDirectory( + debug: true, + source: testDir, + indexFileNames: ['index.php', 'index.txt'])); app.get('*', 'Fallback'); + app ..normalize() - ..dumpTree(); + ..dumpTree(showMatchers: true); await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; }); tearDown(() async { - await app.httpServer.close(force: true); + if (app.httpServer != null) await app.httpServer.close(force: true); }); test('can serve files, with correct Content-Type', () async { From cb3430e233f8b482af1d992a84998a0c758de367 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 27 Nov 2016 19:51:47 -0500 Subject: [PATCH 18/67] +1 --- README.md | 2 +- lib/src/virtual_directory.dart | 1 - pubspec.yaml | 2 +- test/all_test.dart | 4 +--- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9e186754..4ea17e81 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ In `pubspec.yaml`: ```yaml dependencies: angel_framework: ^1.0.0-dev - angel_static: ^1.1.0-dev + angel_static: ^1.1.0-dev+1 ``` # Usage diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index a16ffce4..ceb724fe 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -76,7 +76,6 @@ class VirtualDirectory { } router.get('$publicPath/*', handler); - router.get(publicPath, (req, res) => serveFile('', res)); } serveFile(String path, ResponseContext res) async { diff --git a/pubspec.yaml b/pubspec.yaml index b593fd00..d564240e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_static description: Static server middleware for Angel. homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.0-dev +version: 1.1.0-dev+1 dependencies: angel_framework: ">=1.0.0-dev < 2.0.0" mime: ">= 0.9.3 < 0.10.0" diff --git a/test/all_test.dart b/test/all_test.dart index 9b41167c..3043e126 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -26,9 +26,7 @@ main() { app.get('*', 'Fallback'); - app - ..normalize() - ..dumpTree(showMatchers: true); + app.dumpTree(showMatchers: true); await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; From 0ab637eb15086b7a4c12cd902e7c448c9124a85a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 21 Dec 2016 12:51:43 -0500 Subject: [PATCH 19/67] :) --- README.md | 4 ++-- lib/src/virtual_directory.dart | 7 +------ pubspec.yaml | 11 ++++++----- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4ea17e81..a6c58cac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -![version 1.1.0-dev](https://img.shields.io/badge/version-1.1.0--dev-red.svg) +![version 1.1.0-dev+2](https://img.shields.io/badge/version-1.1.0--dev+2-red.svg) ![build status](https://travis-ci.org/angel-dart/static.svg?branch=master) Static server middleware for Angel. @@ -11,7 +11,7 @@ In `pubspec.yaml`: ```yaml dependencies: angel_framework: ^1.0.0-dev - angel_static: ^1.1.0-dev+1 + angel_static: ^1.1.0-dev ``` # Usage diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index ceb724fe..461d5c99 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_route/angel_route.dart'; -import 'package:mime/mime.dart' show lookupMimeType; final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -56,12 +55,8 @@ class VirtualDirectory { Future sendFile(File file, ResponseContext res) async { _printDebug('Streaming file ${file.absolute.path}...'); - res - ..willCloseItself = true - ..header(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) - ..status(200); + res.statusCode = 200; await res.streamFile(file); - await res.io.close(); return false; } diff --git a/pubspec.yaml b/pubspec.yaml index d564240e..8db7412e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,12 @@ name: angel_static description: Static server middleware for Angel. +environment: + sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.0-dev+1 +version: 1.1.0-dev+2 dependencies: - angel_framework: ">=1.0.0-dev < 2.0.0" - mime: ">= 0.9.3 < 0.10.0" + angel_framework: ^1.0.0-dev dev_dependencies: - http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" + http: ^0.11.3 + test: ^0.12.13 From 7d424436c7125c598abff14487ce279cec599870 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 25 Jan 2017 17:40:41 -0500 Subject: [PATCH 20/67] 1.1.1 --- README.md | 4 +++- lib/src/virtual_directory.dart | 35 +++++++++++++++++++++------------- pubspec.yaml | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a6c58cac..0850305d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -![version 1.1.0-dev+2](https://img.shields.io/badge/version-1.1.0--dev+2-red.svg) +![version 1.1.1](https://img.shields.io/badge/version-1.1.1-red.svg) ![build status](https://travis-ci.org/angel-dart/static.svg?branch=master) Static server middleware for Angel. @@ -39,3 +39,5 @@ 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. - **debug**: Print verbose debug output. +- **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`, +then the callback's result will be sent to the user, instead of the file contents. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 461d5c99..0de0a209 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_route/angel_route.dart'; +typedef StaticFileCallback(File file, RequestContext req, ResponseContext res); + final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -27,6 +29,7 @@ class VirtualDirectory { String _prefix; Directory _source; Directory get source => _source; + final StaticFileCallback callback; final List indexFileNames; final String publicPath; @@ -34,7 +37,8 @@ class VirtualDirectory { {Directory source, this.debug: false, this.indexFileNames: const ['index.html'], - this.publicPath: '/'}) { + this.publicPath: '/', + this.callback}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { @@ -53,9 +57,17 @@ class VirtualDirectory { call(AngelBase app) async => serve(app); - Future sendFile(File file, ResponseContext res) async { - _printDebug('Streaming file ${file.absolute.path}...'); + Future sendFile( + File file, RequestContext req, ResponseContext res) async { + _printDebug('Sending file ${file.absolute.path}...'); res.statusCode = 200; + + if (callback != null) { + var r = callback(file, req, res); + r = r is Future ? await r : r; + if (r != null && r != true) return r; + } + await res.streamFile(file); return false; } @@ -63,17 +75,14 @@ class VirtualDirectory { void serve(Router router) { _printDebug('Source directory: ${source.absolute.path}'); _printDebug('Public path prefix: "$_prefix"'); - - handler(RequestContext req, ResponseContext res) async { + router.get('$publicPath/*', + (RequestContext req, ResponseContext res) async { var path = req.path.replaceAll(_straySlashes, ''); - - return serveFile(path, res); - } - - router.get('$publicPath/*', handler); + return serveFile(path, req, res); + }); } - serveFile(String path, ResponseContext res) async { + serveFile(String path, RequestContext req, ResponseContext res) async { if (_prefix.isNotEmpty) { path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), ''); } @@ -82,7 +91,7 @@ class VirtualDirectory { _printDebug('Attempting to statically serve file: ${file.absolute.path}'); if (await file.exists()) { - return sendFile(file, res); + return sendFile(file, req, res); } else { // Try to resolve index if (path.isEmpty) { @@ -90,7 +99,7 @@ class VirtualDirectory { final index = new File.fromUri(source.absolute.uri.resolve(indexFileName)); if (await index.exists()) { - return await sendFile(index, res); + return await sendFile(index, req, res); } } } else { diff --git a/pubspec.yaml b/pubspec.yaml index 8db7412e..f077cbde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.0-dev+2 +version: 1.1.1 dependencies: angel_framework: ^1.0.0-dev dev_dependencies: From ddae8d9cf136593a7914a2fd1537bac60d14658d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 27 Jan 2017 22:48:55 -0500 Subject: [PATCH 21/67] Fixed test --- test/all_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/all_test.dart b/test/all_test.dart index 3043e126..d30b7554 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -39,7 +39,7 @@ main() { 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[HttpHeaders.CONTENT_TYPE], equals("text/plain")); + expect(response.headers[HttpHeaders.CONTENT_TYPE], contains("text/plain")); }); test('non-existent files are skipped', () async { From 4d4422d31fdd0191ae4e4711ad7a04cda6f3420d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 28 Jan 2017 11:33:22 -0500 Subject: [PATCH 22/67] Hm --- lib/src/virtual_directory.dart | 3 +++ pubspec.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 0de0a209..7eb30f17 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_route/angel_route.dart'; +import 'package:mime/mime.dart'; typedef StaticFileCallback(File file, RequestContext req, ResponseContext res); @@ -60,6 +61,7 @@ class VirtualDirectory { Future sendFile( File file, RequestContext req, ResponseContext res) async { _printDebug('Sending file ${file.absolute.path}...'); + _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); res.statusCode = 200; if (callback != null) { @@ -68,6 +70,7 @@ class VirtualDirectory { if (r != null && r != true) return r; } + res.headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); await res.streamFile(file); return false; } diff --git a/pubspec.yaml b/pubspec.yaml index f077cbde..001fa82f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ author: thosakwe version: 1.1.1 dependencies: angel_framework: ^1.0.0-dev + mime: ^0.9.3 dev_dependencies: http: ^0.11.3 test: ^0.12.13 From 3ef26d9ada7a1d2b39e1228ed7cbca62fe9ad09c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 22 Feb 2017 18:43:27 -0500 Subject: [PATCH 23/67] 1.1.2 --- README.md | 8 +-- lib/src/virtual_directory.dart | 97 +++++++++++++++++++++------------- pubspec.yaml | 2 +- test/all_test.dart | 6 +++ test/nested/index.txt | 1 + 5 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 test/nested/index.txt diff --git a/README.md b/README.md index 0850305d..b04d11a7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # angel_static -![version 1.1.1](https://img.shields.io/badge/version-1.1.1-red.svg) -![build status](https://travis-ci.org/angel-dart/static.svg?branch=master) +[![version 1.1.2](https://img.shields.io/badge/pub-1.1.2-brightgreen.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 middleware for Angel. @@ -10,8 +10,7 @@ In `pubspec.yaml`: ```yaml dependencies: - angel_framework: ^1.0.0-dev - angel_static: ^1.1.0-dev + angel_static: ^1.1.0 ``` # Usage @@ -41,3 +40,4 @@ The `VirtualDirectory` API accepts a few named parameters: - **debug**: Print verbose debug output. - **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`, then the callback's result will be sent to the user, instead of the file contents. +- **streamToIO**: If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.. Default is `false`. \ No newline at end of file diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 7eb30f17..48e41fc0 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -25,7 +25,7 @@ String _pathify(String path) { return p; } -class VirtualDirectory { +class VirtualDirectory implements AngelPlugin { final bool debug; String _prefix; Directory _source; @@ -34,12 +34,16 @@ class VirtualDirectory { final List indexFileNames; final String publicPath; + /// If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`. + final bool streamToIO; + VirtualDirectory( {Directory source, this.debug: false, this.indexFileNames: const ['index.html'], this.publicPath: '/', - this.callback}) { + this.callback, + this.streamToIO: false}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { @@ -56,9 +60,46 @@ class VirtualDirectory { if (debug) print(msg); } - call(AngelBase app) async => serve(app); + call(Angel app) async => serve(app); - Future sendFile( + void serve(Router router) { + _printDebug('Source directory: ${source.absolute.path}'); + _printDebug('Public path prefix: "$_prefix"'); + router.get('$publicPath/*', + (RequestContext req, ResponseContext res) async { + var path = req.path.replaceAll(_straySlashes, ''); + return servePath(path, req, res); + }); + } + + servePath(String path, RequestContext req, ResponseContext res) async { + if (_prefix.isNotEmpty) { + path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), ''); + } + + if (path.isEmpty) path = '.'; + + var absolute = source.absolute.uri.resolve(path).toFilePath(); + var stat = await FileStat.stat(absolute); + return await serveStat(absolute, stat, req, res); + } + + Future serveStat(String absolute, FileStat stat, RequestContext req, + ResponseContext res) async { + if (stat.type == FileSystemEntityType.NOT_FOUND) + return true; + else if (stat.type == FileSystemEntityType.DIRECTORY) + return await serveDirectory(new Directory(absolute), req, res); + else if (stat.type == FileSystemEntityType.FILE) + return await serveFile(new File(absolute), req, res); + else if (stat.type == FileSystemEntityType.LINK) { + var link = new Link(absolute); + return await servePath(await link.resolveSymbolicLinks(), req, res); + } else + return true; + } + + Future serveFile( File file, RequestContext req, ResponseContext res) async { _printDebug('Sending file ${file.absolute.path}...'); _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); @@ -71,44 +112,24 @@ class VirtualDirectory { } res.headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); - await res.streamFile(file); + + if (streamToIO == true) + await res.streamFile(file); + else + await res.sendFile(file); return false; } - void serve(Router router) { - _printDebug('Source directory: ${source.absolute.path}'); - _printDebug('Public path prefix: "$_prefix"'); - router.get('$publicPath/*', - (RequestContext req, ResponseContext res) async { - var path = req.path.replaceAll(_straySlashes, ''); - return serveFile(path, req, res); - }); - } - - serveFile(String path, RequestContext req, ResponseContext res) async { - if (_prefix.isNotEmpty) { - path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), ''); - } - - final file = new File.fromUri(source.absolute.uri.resolve(path)); - _printDebug('Attempting to statically serve file: ${file.absolute.path}'); - - if (await file.exists()) { - return sendFile(file, req, res); - } else { - // Try to resolve index - if (path.isEmpty) { - for (String indexFileName in indexFileNames) { - final index = - new File.fromUri(source.absolute.uri.resolve(indexFileName)); - if (await index.exists()) { - return await sendFile(index, req, res); - } - } - } else { - _printDebug('File "$path" does not exist, and is not an index.'); - return true; + Future serveDirectory( + Directory directory, RequestContext req, ResponseContext res) async { + for (String indexFileName in indexFileNames) { + final index = + new File.fromUri(directory.absolute.uri.resolve(indexFileName)); + if (await index.exists()) { + return await serveFile(index, req, res); } } + + return true; } } diff --git a/pubspec.yaml b/pubspec.yaml index 001fa82f..2e7ad7c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.1 +version: 1.1.2 dependencies: angel_framework: ^1.0.0-dev mime: ^0.9.3 diff --git a/test/all_test.dart b/test/all_test.dart index d30b7554..80be5b20 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -42,6 +42,12 @@ main() { expect(response.headers[HttpHeaders.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[HttpHeaders.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"')); diff --git a/test/nested/index.txt b/test/nested/index.txt new file mode 100644 index 00000000..d402a929 --- /dev/null +++ b/test/nested/index.txt @@ -0,0 +1 @@ +Bird \ No newline at end of file From e6d9ffa79b98d33d2578cb1629e2b21722ae5ecd Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 26 Feb 2017 19:19:34 -0500 Subject: [PATCH 24/67] Added caching --- README.md | 8 +- lib/angel_static.dart | 1 + lib/src/cache.dart | 177 +++++++++++++++++++++++++++++++++ lib/src/virtual_directory.dart | 23 +++-- pubspec.yaml | 3 +- test/cache_sample.dart | 24 +++++ test/cache_test.dart | 71 +++++++++++++ 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 lib/src/cache.dart create mode 100644 test/cache_sample.dart create mode 100644 test/cache_test.dart diff --git a/README.md b/README.md index b04d11a7..56cb5385 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -[![version 1.1.2](https://img.shields.io/badge/pub-1.1.2-brightgreen.svg)](https://pub.dartlang.org/packages/angel_static) +[![version 1.1.3](https://img.shields.io/badge/pub-1.1.3-brightgreen.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 middleware for Angel. @@ -24,7 +24,13 @@ import 'package:angel_static/angel_static.dart'; main() async { final app = new Angel(); + + // Normal static server await app.configure(new VirtualDirectory(source: new Directory('./public'))); + + // Send Cache-Control, ETag, etc. as well + await app.configure(new CachingVirtualDirectory(source: new Directory('./public'))); + await app.startServer(); } ``` diff --git a/lib/angel_static.dart b/lib/angel_static.dart index 1f8830d4..79485e3d 100644 --- a/lib/angel_static.dart +++ b/lib/angel_static.dart @@ -1,4 +1,5 @@ library angel_static; +export 'src/cache.dart'; export 'src/serve_static.dart'; export 'src/virtual_directory.dart'; diff --git a/lib/src/cache.dart b/lib/src/cache.dart new file mode 100644 index 00000000..cb32e784 --- /dev/null +++ b/lib/src/cache.dart @@ -0,0 +1,177 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:crypto/crypto.dart'; +import 'package:intl/intl.dart'; +import 'package:mime/mime.dart'; +import 'virtual_directory.dart'; + +final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss'); + +/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`. +String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT'; + +/// Generates an ETag from the given buffer. +String generateEtag(List buf, {bool weak: true, Hash hash}) { + if (weak == false) { + Hash h = hash ?? md5; + return new String.fromCharCodes(h.convert(buf).bytes); + } else { + // length + first 50 bytes as base64url + return 'W/${buf.length}' + BASE64URL.encode(buf.take(50).toList()); + } +} + +/// Returns a string representation of the given [CacheAccessLevel]. +String accessLevelToString(CacheAccessLevel accessLevel) { + switch (accessLevel) { + case CacheAccessLevel.PRIVATE: + return 'private'; + case CacheAccessLevel.PUBLIC: + return 'public'; + default: + throw new ArgumentError('Unrecognized cache access level: $accessLevel'); + } +} + +/// A static server plug-in that also sets `Cache-Control` headers. +class CachingVirtualDirectory extends VirtualDirectory { + final Map _etags = {}; + + /// Either `PUBLIC` or `PRIVATE`. + final CacheAccessLevel accessLevel; + + /// Used to generate strong ETags, if [useWeakEtags] is false. + /// + /// Default: `md5`. + final Hash hash; + + /// If `true`, responses will always have `private, max-age=0` as their `Cache-Control` header. + 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; + + /// If `false` (default: `true`), ETags will be generated via MD5 hash. + final bool useWeakEtags; + + /// The `max-age` for `Cache-Control`. + final int maxAge; + + CachingVirtualDirectory( + {this.accessLevel: CacheAccessLevel.PUBLIC, + Directory source, + bool debug, + this.hash, + Iterable indexFileNames, + this.maxAge: 0, + this.noCache: false, + this.onlyInProduction: false, + this.useEtags: true, + this.useWeakEtags: true, + String publicPath, + StaticFileCallback callback, + bool streamToIO: false}) + : super( + source: source, + debug: debug == true, + indexFileNames: indexFileNames ?? ['index.html'], + publicPath: publicPath ?? '/', + callback: callback, + streamToIO: streamToIO == true); + + @override + Future serveFile( + File file, FileStat stat, RequestContext req, ResponseContext res) { + if (onlyInProduction == true && req.app.isProduction == true) { + return super.serveFile(file, stat, req, res); + } + + if (noCache == true) { + res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache'; + return super.serveFile(file, stat, req, res); + } else { + if (useEtags == true) { + var etags = req.headers[HttpHeaders.IF_NONE_MATCH]; + + if (etags?.isNotEmpty == true) { + bool hasBeenModified = false; + + for (var etag in etags) { + if (etag == '*') + hasBeenModified = true; + else { + hasBeenModified = _etags.containsKey(file.absolute.path) && + _etags[file.absolute.path] == etag; + } + } + + if (hasBeenModified) { + res.statusCode = HttpStatus.NOT_MODIFIED; + setCachedHeaders(file, stat, req, res); + return new Future.value(false); + } + } + } + + if (req.headers[HttpHeaders.IF_MODIFIED_SINCE] != null) { + try { + var ifModifiedSince = _fmt.parse(req.headers + .value(HttpHeaders.IF_MODIFIED_SINCE) + .replaceAll('GMT', '') + .trim()); + + if (ifModifiedSince.compareTo(stat.changed) > 0) { + res.statusCode = HttpStatus.NOT_MODIFIED; + setCachedHeaders(file, stat, req, res); + + if (_etags.containsKey(file.absolute.path)) + res.headers[HttpHeaders.ETAG] = _etags[file.absolute.path]; + + return new Future.value(false); + } + } catch (_) { + throw new AngelHttpException.badRequest( + message: 'Invalid date for If-Modified-Since header.'); + } + } + + return file.readAsBytes().then((buf) { + var etag = _etags[file.absolute.path] = + generateEtag(buf, weak: useWeakEtags != false); + res.headers + ..[HttpHeaders.ETAG] = etag + ..[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); + setCachedHeaders(file, stat, req, res); + + if (useWeakEtags == false) { + res + ..statusCode = 200 + ..willCloseItself = false + ..buffer.add(buf) + ..end(); + return new Future.value(false); + } + + return super.serveFile(file, stat, req, res); + }); + } + } + + void setCachedHeaders( + File file, FileStat stat, RequestContext req, ResponseContext res) { + var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC); + var expiry = new DateTime.now()..add(new Duration(seconds: maxAge ?? 0)); + + res.headers + ..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}' + ..[HttpHeaders.EXPIRES] = formatDateForHttp(expiry) + ..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(stat.changed); + } +} + +enum CacheAccessLevel { PUBLIC, PRIVATE } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 48e41fc0..22d3a0cd 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -25,13 +25,22 @@ String _pathify(String path) { return p; } +/// A static server plug-in. class VirtualDirectory implements AngelPlugin { final bool debug; String _prefix; Directory _source; + + /// The directory to serve files from. Directory get source => _source; + + /// An optional callback to run before serving files. final StaticFileCallback callback; - final List indexFileNames; + + /// Filenames to be resolved within directories as indices. + final Iterable indexFileNames; + + /// An optional public path to map requests to. final String publicPath; /// If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`. @@ -89,9 +98,9 @@ class VirtualDirectory implements AngelPlugin { if (stat.type == FileSystemEntityType.NOT_FOUND) return true; else if (stat.type == FileSystemEntityType.DIRECTORY) - return await serveDirectory(new Directory(absolute), req, res); + return await serveDirectory(new Directory(absolute), stat, req, res); else if (stat.type == FileSystemEntityType.FILE) - return await serveFile(new File(absolute), req, res); + return await serveFile(new File(absolute), stat, req, res); else if (stat.type == FileSystemEntityType.LINK) { var link = new Link(absolute); return await servePath(await link.resolveSymbolicLinks(), req, res); @@ -100,7 +109,7 @@ class VirtualDirectory implements AngelPlugin { } Future serveFile( - File file, RequestContext req, ResponseContext res) async { + File file, FileStat stat, RequestContext req, ResponseContext res) async { _printDebug('Sending file ${file.absolute.path}...'); _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); res.statusCode = 200; @@ -120,13 +129,13 @@ class VirtualDirectory implements AngelPlugin { return false; } - Future serveDirectory( - Directory directory, RequestContext req, ResponseContext res) async { + Future serveDirectory(Directory directory, FileStat stat, + RequestContext req, ResponseContext res) async { for (String indexFileName in indexFileNames) { final index = new File.fromUri(directory.absolute.uri.resolve(indexFileName)); if (await index.exists()) { - return await serveFile(index, req, res); + return await serveFile(index, stat, req, res); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2e7ad7c6..59b2d0d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,9 +4,10 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.2 +version: 1.1.3 dependencies: angel_framework: ^1.0.0-dev + intl: ">=0.0.0 <1.0.0" mime: ^0.9.3 dev_dependencies: http: ^0.11.3 diff --git a/test/cache_sample.dart b/test/cache_sample.dart new file mode 100644 index 00000000..03321670 --- /dev/null +++ b/test/cache_sample.dart @@ -0,0 +1,24 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; + +main() async { + Angel app; + Directory testDir = new Directory('test'); + app = new Angel(debug: true); + + await app.configure(new CachingVirtualDirectory( + source: testDir, + maxAge: 350, + onlyInProduction: false, + // useWeakEtags: false, + //publicPath: '/virtual', + indexFileNames: ['index.txt'])); + + app.get('*', 'Fallback'); + + app.dumpTree(showMatchers: true); + + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + print('Open at http://${app.httpServer.address.host}:${app.httpServer.port}'); +} diff --git a/test/cache_test.dart b/test/cache_test.dart new file mode 100644 index 00000000..b7ed5432 --- /dev/null +++ b/test/cache_test.dart @@ -0,0 +1,71 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:http/http.dart' show Client; +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart'; + +main() { + Angel app; + Directory testDir = new Directory('test'); + String url; + Client client = new Client(); + + setUp(() async { + app = new Angel(debug: true); + + await app.configure(new CachingVirtualDirectory( + source: testDir, maxAge: 350, onlyInProduction: false, + //publicPath: '/virtual', + indexFileNames: ['index.txt'])); + + app.get('*', 'Fallback'); + + app.dumpTree(showMatchers: true); + + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; + }); + + tearDown(() async { + if (app.httpServer != null) await app.httpServer.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( + [ + HttpHeaders.ETAG, + HttpHeaders.CACHE_CONTROL, + HttpHeaders.EXPIRES, + HttpHeaders.LAST_MODIFIED + ], + everyElement(predicate( + response.headers.containsKey, 'contained in response headers'))); + }); + + test('if-modified-since', () async { + var response = await client.get("$url", headers: { + HttpHeaders.IF_MODIFIED_SINCE: + formatDateForHttp(new DateTime.now()..add(new Duration(days: 365))) + }); + + print('Response status: ${response.statusCode}'); + + expect(response.statusCode, equals(304)); + expect( + [ + HttpHeaders.CACHE_CONTROL, + HttpHeaders.EXPIRES, + HttpHeaders.LAST_MODIFIED + ], + everyElement(predicate( + response.headers.containsKey, 'contained in response headers'))); + }); +} From 7230a932c0757414605ef2266099a8546fc63634 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 1 Mar 2017 23:01:32 -0500 Subject: [PATCH 25/67] 1.1.4 --- README.md | 2 +- lib/src/cache.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 56cb5385..da3830ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -[![version 1.1.3](https://img.shields.io/badge/pub-1.1.3-brightgreen.svg)](https://pub.dartlang.org/packages/angel_static) +[![version 1.1.4](https://img.shields.io/badge/pub-1.1.4-brightgreen.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 middleware for Angel. diff --git a/lib/src/cache.dart b/lib/src/cache.dart index cb32e784..e28ed664 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -142,7 +142,7 @@ class CachingVirtualDirectory extends VirtualDirectory { return file.readAsBytes().then((buf) { var etag = _etags[file.absolute.path] = - generateEtag(buf, weak: useWeakEtags != false); + generateEtag(buf, weak: useWeakEtags != false, hash: hash); res.headers ..[HttpHeaders.ETAG] = etag ..[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); diff --git a/pubspec.yaml b/pubspec.yaml index 59b2d0d0..6f19d2c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.3 +version: 1.1.4 dependencies: angel_framework: ^1.0.0-dev intl: ">=0.0.0 <1.0.0" From 9c2292edbe6ae0e9258236937eb35b11b0b993b3 Mon Sep 17 00:00:00 2001 From: "zengyun261@qq.com" Date: Mon, 27 Mar 2017 20:59:02 +0800 Subject: [PATCH 26/67] fix IF_MODIFIED_SINCE --- lib/src/cache.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/cache.dart b/lib/src/cache.dart index e28ed664..db310efe 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -120,12 +120,9 @@ class CachingVirtualDirectory extends VirtualDirectory { if (req.headers[HttpHeaders.IF_MODIFIED_SINCE] != null) { try { - var ifModifiedSince = _fmt.parse(req.headers - .value(HttpHeaders.IF_MODIFIED_SINCE) - .replaceAll('GMT', '') - .trim()); + var ifModifiedSince = req.headers.ifModifiedSince; - if (ifModifiedSince.compareTo(stat.changed) > 0) { + if (ifModifiedSince.compareTo(stat.modified) >= 0) { res.statusCode = HttpStatus.NOT_MODIFIED; setCachedHeaders(file, stat, req, res); @@ -170,7 +167,7 @@ class CachingVirtualDirectory extends VirtualDirectory { res.headers ..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}' ..[HttpHeaders.EXPIRES] = formatDateForHttp(expiry) - ..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(stat.changed); + ..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(stat.modified); } } From f2822fba00e1d855df96b5bac4128b1d379a17e9 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Fri, 31 Mar 2017 21:08:01 -0400 Subject: [PATCH 27/67] Update README.md --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da3830ad..958d9892 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ main() async { } ``` +# Push State Example +```dart +var vDir = new VirtualDirectory(...); +var indexFile = new File.fromUri(vDir.source.uri.resolve('index.html')); + +app.after.add((req, ResponseContext res) { + // Fallback to index.html on 404 + return res.sendFile(indexFile); +}); +``` + # 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 @@ -46,4 +57,4 @@ The `VirtualDirectory` API accepts a few named parameters: - **debug**: Print verbose debug output. - **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`, then the callback's result will be sent to the user, instead of the file contents. -- **streamToIO**: If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.. Default is `false`. \ No newline at end of file +- **streamToIO**: If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.. Default is `false`. From 734d5f8547091236ab1ed5416db6becdea187787 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 26 Apr 2017 18:39:47 -0400 Subject: [PATCH 28/67] GZIP on streamToIO --- README.md | 2 +- lib/src/virtual_directory.dart | 12 +++++++++--- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 958d9892..02c1c600 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # angel_static -[![version 1.1.4](https://img.shields.io/badge/pub-1.1.4-brightgreen.svg)](https://pub.dartlang.org/packages/angel_static) +[![version 1.1.4+1](https://img.shields.io/badge/pub-1.1.4+1-brightgreen.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 middleware for Angel. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 22d3a0cd..f1e717b4 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -122,9 +122,15 @@ class VirtualDirectory implements AngelPlugin { res.headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); - if (streamToIO == true) - await res.streamFile(file); - else + if (streamToIO == true) { + res + ..io.headers.set(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) + ..io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip') + ..end() + ..willCloseItself = true; + + await file.openRead().transform(GZIP.encoder).pipe(res.io); + } else await res.sendFile(file); return false; } diff --git a/pubspec.yaml b/pubspec.yaml index 6f19d2c0..6805d32d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.4 +version: 1.1.4+1 dependencies: angel_framework: ^1.0.0-dev intl: ">=0.0.0 <1.0.0" From fa3474404c1594256afea235292b9e1942e44276 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 14 Jun 2017 05:37:39 -0400 Subject: [PATCH 29/67] Fix expiry --- README.md | 4 ++-- lib/src/cache.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02c1c600..8272c5af 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# angel_static +# static -[![version 1.1.4+1](https://img.shields.io/badge/pub-1.1.4+1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_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 middleware for Angel. diff --git a/lib/src/cache.dart b/lib/src/cache.dart index db310efe..f49f0069 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -162,7 +162,7 @@ class CachingVirtualDirectory extends VirtualDirectory { void setCachedHeaders( File file, FileStat stat, RequestContext req, ResponseContext res) { var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC); - var expiry = new DateTime.now()..add(new Duration(seconds: maxAge ?? 0)); + var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0)); res.headers ..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}' diff --git a/pubspec.yaml b/pubspec.yaml index 6805d32d..8f93d485 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/angel_static author: thosakwe -version: 1.1.4+1 +version: 1.1.4+2 dependencies: angel_framework: ^1.0.0-dev intl: ">=0.0.0 <1.0.0" From 8bd228c033c32ad947200da03b6fa64121b93619 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 22:05:06 -0400 Subject: [PATCH 30/67] 1.2.0 --- .analysis-options | 2 + README.md | 75 ++++++++- lib/angel_static.dart | 2 + lib/src/cache.dart | 75 ++++++++- lib/src/file_info.dart | 91 +++++++++++ lib/src/file_transformer.dart | 15 ++ lib/src/virtual_directory.dart | 271 ++++++++++++++++++++++++++++++--- pubspec.yaml | 13 +- test/all_test.dart | 4 +- test/transformer_test.dart | 41 +++++ 10 files changed, 556 insertions(+), 33 deletions(-) create mode 100644 .analysis-options create mode 100644 lib/src/file_info.dart create mode 100644 lib/src/file_transformer.dart create mode 100644 test/transformer_test.dart diff --git a/.analysis-options b/.analysis-options new file mode 100644 index 00000000..518eb901 --- /dev/null +++ b/.analysis-options @@ -0,0 +1,2 @@ +analyzer: + strong-mode: true \ No newline at end of file diff --git a/README.md b/README.md index 8272c5af..0d3b019c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ In `pubspec.yaml`: ```yaml dependencies: - angel_static: ^1.1.0 + angel_static: ^1.2.0 ``` # Usage @@ -58,3 +58,76 @@ The `VirtualDirectory` API accepts a few named parameters: - **callback**: Runs before sending a file to a client. Use this to set headers, etc. If it returns anything other than `null` or `true`, then the callback's result will be sent to the user, instead of the file contents. - **streamToIO**: If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`.. Default is `false`. + +# Transformers +`angel_static` now supports *transformers*. Similarly to `pub serve`, or `package:build`, these +let you dynamically compile assets before sending them to users. For example, in development, you might +consider using transformers to compile CSS files, or to even replace `pub serve`. +Transformers are supported by `VirtualDirectory` and `CachingVirtualDirectory`. + +To create a transformer: +```dart +class MinifierTransformer { + /// Use this to declare outputs, and indicate if your transformer + /// will compile a file. + @override + FileInfo declareOutput(FileInfo file) { + // For example, we might only want to minify HTML files. + if (!file.extensions.endsWith('.min.html')) + return null; + else return file.changeExtension('.min.html'); + } + + /// Actually compile the asset here. + @override + FutureOr transform(FileInfo file) async { + return file + .changeExtension('.min.html') + .changeContent( + file.content + .transform(UTF8.decoder) + .transform(const LineSplitter() + .transform(UTF8.encoder)) + ); + } +} +``` + +To use it: +```dart +configureServer(Angel app) async { + var vDir = new CachingVirtualDirectory( + transformers: [new MinifierTransformer()] + ); + await app.configure(vDir); + + // It is suggested that you await `transformersLoaded`. + // Otherwise, you may receive 404's on paths that should send a compiled asset. + await vDir.transformersLoaded; +} +``` + +## Pre-building +You can pre-build all your assets with one command: + +```dart +configureServer(Angel app) async { + var vDir = new VirtualDirectory(transformers: [...]); + await app.configure(vDir); + + // Build if in production + if (app.isProduction) { + await vDir.buildToDisk(); + } +} +``` + +## In Production +By default, transformers are disabled in production mode. +To force-enable them: + +```dart +configureServer(Angel app) async { + var vDir = new VirtualDirectory(useTransformersInProduction: true, transformers: [...]); +} +``` \ No newline at end of file diff --git a/lib/angel_static.dart b/lib/angel_static.dart index 79485e3d..624906cd 100644 --- a/lib/angel_static.dart +++ b/lib/angel_static.dart @@ -1,5 +1,7 @@ library angel_static; export 'src/cache.dart'; +export 'src/file_info.dart'; +export 'src/file_transformer.dart'; export 'src/serve_static.dart'; export 'src/virtual_directory.dart'; diff --git a/lib/src/cache.dart b/lib/src/cache.dart index f49f0069..f75f2635 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -5,6 +5,8 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; import 'package:mime/mime.dart'; +import 'file_info.dart'; +import 'file_transformer.dart'; import 'virtual_directory.dart'; final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss'); @@ -75,14 +77,16 @@ class CachingVirtualDirectory extends VirtualDirectory { this.useWeakEtags: true, String publicPath, StaticFileCallback callback, - bool streamToIO: false}) + bool streamToIO: false, + Iterable transformers: const []}) : super( source: source, debug: debug == true, indexFileNames: indexFileNames ?? ['index.html'], publicPath: publicPath ?? '/', callback: callback, - streamToIO: streamToIO == true); + streamToIO: streamToIO == true, + transformers: transformers ?? []); @override Future serveFile( @@ -112,7 +116,7 @@ class CachingVirtualDirectory extends VirtualDirectory { if (hasBeenModified) { res.statusCode = HttpStatus.NOT_MODIFIED; - setCachedHeaders(file, stat, req, res); + setCachedHeaders(stat.modified, req, res); return new Future.value(false); } } @@ -124,7 +128,7 @@ class CachingVirtualDirectory extends VirtualDirectory { if (ifModifiedSince.compareTo(stat.modified) >= 0) { res.statusCode = HttpStatus.NOT_MODIFIED; - setCachedHeaders(file, stat, req, res); + setCachedHeaders(stat.modified, req, res); if (_etags.containsKey(file.absolute.path)) res.headers[HttpHeaders.ETAG] = _etags[file.absolute.path]; @@ -143,7 +147,7 @@ class CachingVirtualDirectory extends VirtualDirectory { res.headers ..[HttpHeaders.ETAG] = etag ..[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path); - setCachedHeaders(file, stat, req, res); + setCachedHeaders(stat.modified, req, res); if (useWeakEtags == false) { res @@ -160,14 +164,71 @@ class CachingVirtualDirectory extends VirtualDirectory { } void setCachedHeaders( - File file, FileStat stat, RequestContext req, ResponseContext res) { + DateTime modified, RequestContext req, ResponseContext res) { var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC); var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0)); res.headers ..[HttpHeaders.CACHE_CONTROL] = '$privacy, max-age=${maxAge ?? 0}' ..[HttpHeaders.EXPIRES] = formatDateForHttp(expiry) - ..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(stat.modified); + ..[HttpHeaders.LAST_MODIFIED] = formatDateForHttp(modified); + } + + @override + Future serveAsset( + FileInfo fileInfo, RequestContext req, ResponseContext res) { + if (onlyInProduction == true && req.app.isProduction == true) { + return super.serveAsset(fileInfo, req, res); + } + + if (noCache == true) { + res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache'; + return super.serveAsset(fileInfo, req, res); + } else { + if (useEtags == true) { + var etags = req.headers[HttpHeaders.IF_NONE_MATCH]; + + if (etags?.isNotEmpty == true) { + bool hasBeenModified = false; + + for (var etag in etags) { + if (etag == '*') + hasBeenModified = true; + else { + hasBeenModified = _etags.containsKey(fileInfo.filename) && + _etags[fileInfo.filename] == etag; + } + } + + if (hasBeenModified) { + res.statusCode = HttpStatus.NOT_MODIFIED; + setCachedHeaders(fileInfo.lastModified, req, res); + return new Future.value(false); + } + } + } + } + + if (req.headers[HttpHeaders.IF_MODIFIED_SINCE] != null) { + try { + var ifModifiedSince = req.headers.ifModifiedSince; + + if (ifModifiedSince.compareTo(fileInfo.lastModified) >= 0) { + res.statusCode = HttpStatus.NOT_MODIFIED; + setCachedHeaders(fileInfo.lastModified, req, res); + + if (_etags.containsKey(fileInfo.filename)) + res.headers[HttpHeaders.ETAG] = _etags[fileInfo.filename]; + + return new Future.value(false); + } + } catch (_) { + throw new AngelHttpException.badRequest( + message: 'Invalid date for If-Modified-Since header.'); + } + } + + return super.serveAsset(fileInfo, req, res); } } diff --git a/lib/src/file_info.dart b/lib/src/file_info.dart new file mode 100644 index 00000000..f0394b64 --- /dev/null +++ b/lib/src/file_info.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:mime/mime.dart'; +import 'package:path/path.dart' as p; + +/// Represents information about a file, regardless of whether it exists in the filesystem +/// or in memory. +abstract class FileInfo { + /// Returns the content of the file. + Stream> get content; + + /// This file's extension. + String get extension; + + /// The name of the file. + String get filename; + + /// The time when this file was last modified. + DateTime get lastModified; + + /// The file's MIME type. + String get mimeType; + + /// Creates a [FileInfo] instance representing a physical file. + factory FileInfo.fromFile(File file) => new _FileInfoImpl( + () => file.openRead(), + file.absolute.path, + lookupMimeType(file.path), + file.statSync().modified); + + /// Creates a [FileInfo] describing a file that might not even exists to begin with. + factory FileInfo.hypothetical(String hypotheticalFileName) => + new _FileInfoImpl(null, hypotheticalFileName, + lookupMimeType(hypotheticalFileName), null); + + /// Returns an identical instance, but with a different filename. + FileInfo changeFilename(String newFilename); + + /// Returns an identical instance, but with a different extension. + FileInfo changeExtension(String newExtension); + + /// Returns an identical instance, but with a different content. + FileInfo changeContent(Stream> newContent); + + /// Returns an identical instance, but with differnet content, set to the given String. + FileInfo changeText(String newText, {Encoding encoding: UTF8}); + + /// Returns an identical instance, but with a different MIME type. + FileInfo changeMimeType(String newMimeType); +} + +class _FileInfoImpl implements FileInfo { + @override + Stream> get content => getContent(); + + @override + final String filename, mimeType; + + @override + final DateTime lastModified; + + final Function getContent; + + _FileInfoImpl(Stream> this.getContent(), this.filename, + this.mimeType, this.lastModified); + + @override + String get extension => p.extension(filename); + + @override + FileInfo changeFilename(String newFilename) => + new _FileInfoImpl(getContent, newFilename, mimeType, lastModified); + + @override + FileInfo changeExtension(String newExtension) => + changeFilename(p.withoutExtension(filename) + newExtension); + + @override + FileInfo changeContent(Stream> newContent) => + new _FileInfoImpl(() => newContent, filename, mimeType, lastModified); + + @override + FileInfo changeText(String newText, {Encoding encoding: UTF8}) => + changeContent(new Stream>.fromIterable( + [(encoding ?? UTF8).encode(newText)])); + + @override + FileInfo changeMimeType(String newMimeType) => + new _FileInfoImpl(getContent, filename, newMimeType, lastModified); +} diff --git a/lib/src/file_transformer.dart b/lib/src/file_transformer.dart new file mode 100644 index 00000000..21be2f85 --- /dev/null +++ b/lib/src/file_transformer.dart @@ -0,0 +1,15 @@ +import 'dart:async'; +import 'file_info.dart'; + +/// A class capable of transforming inputs into new outputs, on-the-fly. +/// +/// Ex. A transformer that compiles Stylus files. +abstract class FileTransformer { + /// Changes the name of a [file] into what it will be once it is transformed. + /// + /// If this transformer will not be consume the file, then return `null`. + FileInfo declareOutput(FileInfo file); + + /// Transforms an input [file] into a new representation. + FutureOr transform(FileInfo file); +} \ No newline at end of file diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index f1e717b4..9a7f1717 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -2,7 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_route/angel_route.dart'; +import 'package:cli_util/cli_logging.dart' as cli; import 'package:mime/mime.dart'; +import 'package:pool/pool.dart'; +import 'package:watcher/watcher.dart'; +import 'file_info.dart'; +import 'file_transformer.dart'; typedef StaticFileCallback(File file, RequestContext req, ResponseContext res); @@ -28,8 +33,16 @@ String _pathify(String path) { /// A static server plug-in. class VirtualDirectory implements AngelPlugin { final bool debug; + Angel _app; String _prefix; Directory _source; + final Completer> _transformerLoad = + new Completer>(); + final Map _transformerMap = {}; + Pool _transformerMapMutex; + final List _transformers = []; + List _transformersCache; + StreamSubscription _watch; /// The directory to serve files from. Directory get source => _source; @@ -46,14 +59,35 @@ class VirtualDirectory implements AngelPlugin { /// If set to `true`, files will be streamed to `res.io`, instead of added to `res.buffer`. final bool streamToIO; + /// A collection of [FileTransformer] instances that will be used to dynamically compile assets, if any. **READ-ONLY**. + List get transformers => + _transformersCache ?? + (_transformersCache = + new List.unmodifiable(_transformers)); + + /// If `true` (default: `false`), then transformers will not be disabled in production. + final bool useTransformersInProduction; + + /// Completes when all [transformers] are loaded. + Future> get transformersLoaded { + if ((!_app.isProduction || useTransformersInProduction == true) && + !_transformerLoad.isCompleted) + return _transformerLoad.future; + else + return new Future.value(_transformerMap); + } + VirtualDirectory( {Directory source, this.debug: false, this.indexFileNames: const ['index.html'], this.publicPath: '/', this.callback, - this.streamToIO: false}) { + this.streamToIO: false, + this.useTransformersInProduction: false, + Iterable transformers: const []}) { _prefix = publicPath.replaceAll(_straySlashes, ''); + this._transformers.addAll(transformers ?? []); if (source != null) { _source = source; @@ -65,20 +99,79 @@ class VirtualDirectory implements AngelPlugin { } } - _printDebug(msg) { - if (debug) print(msg); + call(Angel app) async { + serve(_app = app); + app.justBeforeStop.add((_) => close()); } - call(Angel app) async => serve(app); - void serve(Router router) { - _printDebug('Source directory: ${source.absolute.path}'); - _printDebug('Public path prefix: "$_prefix"'); + // _printDebug('Source directory: ${source.absolute.path}'); + // _printDebug('Public path prefix: "$_prefix"'); router.get('$publicPath/*', (RequestContext req, ResponseContext res) async { var path = req.path.replaceAll(_straySlashes, ''); return servePath(path, req, res); }); + + if ((!_app.isProduction || useTransformersInProduction == true) && + _transformers.isNotEmpty) { + // Create mutex, and watch for file changes + _transformerMapMutex = new Pool(1); + _transformerMapMutex.request().then((resx) { + _buildTransformerMap().then((_) => resx.release()); + }); + } + } + + close() async { + if (!_transformerLoad.isCompleted) { + _transformerLoad.completeError(new StateError( + 'VirtualDirectory was closed before all transformers loaded.')); + } + + _transformerMapMutex?.close(); + _watch?.cancel(); + } + + Future _buildTransformerMap() async { + print('VirtualDirectory is loading transformers...'); + + await for (var entity in source.list(recursive: true)) { + if (entity is File) { + _applyTransformers(entity.absolute.uri.toFilePath()); + } + } + + print('VirtualDirectory finished loading transformers.'); + _transformerLoad.complete(_transformerMap); + + _watch = + new DirectoryWatcher(source.absolute.path).events.listen((e) async { + _transformerMapMutex.withResource(() { + _applyTransformers(e.path); + }); + }); + } + + void _applyTransformers(String originalAbsolutePath) { + FileInfo file = new FileInfo.fromFile(new File(originalAbsolutePath)); + FileInfo outFile = file; + var wasClaimed = false; + + do { + wasClaimed = false; + for (var transformer in _transformers) { + var claimed = transformer.declareOutput(outFile); + if (claimed != null) { + outFile = claimed; + wasClaimed = true; + } + } + } while (wasClaimed); + + var finalName = outFile.filename; + if (finalName?.isNotEmpty == true && outFile != file) + _transformerMap[finalName] = originalAbsolutePath; } servePath(String path, RequestContext req, ResponseContext res) async { @@ -95,23 +188,44 @@ class VirtualDirectory implements AngelPlugin { Future serveStat(String absolute, FileStat stat, RequestContext req, ResponseContext res) async { - if (stat.type == FileSystemEntityType.NOT_FOUND) - return true; - else if (stat.type == FileSystemEntityType.DIRECTORY) + if (stat.type == FileSystemEntityType.DIRECTORY) return await serveDirectory(new Directory(absolute), stat, req, res); else if (stat.type == FileSystemEntityType.FILE) return await serveFile(new File(absolute), stat, req, res); else if (stat.type == FileSystemEntityType.LINK) { var link = new Link(absolute); return await servePath(await link.resolveSymbolicLinks(), req, res); + } else if (_transformerMapMutex != null) { + var resx = await _transformerMapMutex.request(); + if (!_transformerMap.containsKey(absolute)) return true; + var sourceFile = new File(_transformerMap[absolute]); + resx.release(); + if (!await sourceFile.exists()) + return true; + else { + return await serveAsset(new FileInfo.fromFile(sourceFile), req, res); + } } else return true; } - Future serveFile( + Future serveDirectory(Directory directory, FileStat stat, + RequestContext req, ResponseContext res) async { + for (String indexFileName in indexFileNames) { + final index = + new File.fromUri(directory.absolute.uri.resolve(indexFileName)); + if (await index.exists()) { + return await serveFile(index, stat, req, res); + } + } + + return true; + } + + Future serveFileOld( File file, FileStat stat, RequestContext req, ResponseContext res) async { - _printDebug('Sending file ${file.absolute.path}...'); - _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); + // _printDebug('Sending file ${file.absolute.path}...'); + // _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); res.statusCode = 200; if (callback != null) { @@ -135,16 +249,131 @@ class VirtualDirectory implements AngelPlugin { return false; } - Future serveDirectory(Directory directory, FileStat stat, - RequestContext req, ResponseContext res) async { - for (String indexFileName in indexFileNames) { - final index = - new File.fromUri(directory.absolute.uri.resolve(indexFileName)); - if (await index.exists()) { - return await serveFile(index, stat, req, res); + void _ensureContentTypeAllowed(String mimeType, RequestContext req) { + var value = req.headers.value(HttpHeaders.ACCEPT); + bool acceptable = value == null || + value.isEmpty || + value.contains(mimeType) || + value.contains('*/*'); + if (!acceptable) + throw new AngelHttpException( + new UnsupportedError( + 'Client requested $value, but server wanted to send $mimeType.'), + statusCode: HttpStatus.NOT_ACCEPTABLE, + message: '406 Not Acceptable'); + } + + Future serveFile( + File file, FileStat stat, RequestContext req, ResponseContext res) async { + // _printDebug('Sending file ${file.absolute.path}...'); + // _printDebug('MIME type for ${file.path}: ${lookupMimeType(file.path)}'); + res.statusCode = 200; + + if (callback != null) { + var r = callback(file, req, res); + r = r is Future ? await r : r; + if (r != null && r != true) return r; + } + + var type = lookupMimeType(file.path); + _ensureContentTypeAllowed(type, req); + res.headers[HttpHeaders.CONTENT_TYPE] = type; + + if (streamToIO == true) { + res + ..io.headers.set(HttpHeaders.CONTENT_TYPE, lookupMimeType(file.path)) + ..io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip') + ..end() + ..willCloseItself = true; + + await file.openRead().transform(GZIP.encoder).pipe(res.io); + } else + await res.sendFile(file); + return false; + } + + Future serveAsset( + FileInfo fileInfo, RequestContext req, ResponseContext res) async { + var file = await compileAsset(fileInfo); + if (file == null) return true; + _ensureContentTypeAllowed(file.mimeType, req); + res.headers[HttpHeaders.CONTENT_TYPE] = file.mimeType; + res.statusCode = 200; + + if (streamToIO == true) { + res + ..statusCode = 200 + ..io.headers.set(HttpHeaders.CONTENT_TYPE, file.mimeType) + ..io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip') + ..end() + ..willCloseItself = true; + await file.content.transform(GZIP.encoder).pipe(res.io); + } else { + await file.content.forEach(res.buffer.add); + } + + return false; + } + + /// Applies all [_transformers] to an input [file], if any. + Future compileAsset(FileInfo file) async { + var iterations = 0; + FileInfo result = file; + bool wasTransformed = false; + + do { + wasTransformed = false; + String originalName = file.filename; + for (var transformer in _transformers) { + if (++iterations >= 100) { + print( + 'VirtualDirectory has tried 100 times to compile ${file.filename}. Perhaps one of your transformers is not changing the output file\'s extension.'); + throw new AngelHttpException(new StackOverflowError(), + statusCode: 500); + } else if (iterations < 100) iterations++; + var claimed = transformer.declareOutput(result); + if (claimed != null) { + result = await transformer.transform(result); + wasTransformed = true; + } + } + + // Don't re-compile infinitely... + if (result.filename == originalName) wasTransformed = false; + } while (wasTransformed); + + return result == file ? null : result; + } + + /// Builds assets to disk using [transformers]. + Future buildToDisk() async { + var l = new cli.Logger.standard(); + print('Building assets in "${source.absolute.path}"...'); + + await for (var entity in source.list(recursive: true)) { + if (entity is File) { + var p = l.progress('Building "${entity.absolute.path}"'); + + try { + var asset = new FileInfo.fromFile(entity); + var compiled = await compileAsset(asset); + if (compiled == null) + p.finish(message: '"${entity.absolute.path}" did not require compilation; skipping it.'); + else { + p.finish( + message: + 'Built "${entity.absolute.path}" to "${compiled.filename}".', + showTiming: true); + } + } on AngelHttpException { + // Ignore 500 + } catch (e, st) { + p.finish(message: 'Failed to build "${entity.absolute.path}".'); + stderr..writeln(e)..writeln(st); + } } } - return true; + print('Build of assets in "${source.absolute.path}" complete.'); } } diff --git a/pubspec.yaml b/pubspec.yaml index 8f93d485..03bdcd62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,13 +2,20 @@ name: angel_static description: Static server middleware for Angel. environment: sdk: ">=1.19.0" -homepage: https://github.com/angel-dart/angel_static -author: thosakwe -version: 1.1.4+2 +homepage: https://github.com/angel-dart/static +author: Tobe O +version: 1.2.0 dependencies: angel_framework: ^1.0.0-dev + cli_util: ^0.1.1 + crypto: ^2.0.0 intl: ">=0.0.0 <1.0.0" mime: ^0.9.3 + path: ^1.4.2 + pool: ^1.0.0 + watcher: ^0.9.7 dev_dependencies: + angel_diagnostics: ^1.0.0 + angel_test: ^1.0.0 http: ^0.11.3 test: ^0.12.13 diff --git a/test/all_test.dart b/test/all_test.dart index 80be5b20..a5fa5516 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:angel_diagnostics/angel_diagnostics.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_static/angel_static.dart'; import 'package:http/http.dart' show Client; @@ -24,10 +25,11 @@ main() { source: testDir, indexFileNames: ['index.php', 'index.txt'])); - app.get('*', 'Fallback'); + app.after.add('Fallback'); app.dumpTree(showMatchers: true); + await app.configure(logRequests()); await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); url = "http://${app.httpServer.address.host}:${app.httpServer.port}"; }); diff --git a/test/transformer_test.dart b/test/transformer_test.dart new file mode 100644 index 00000000..69b43548 --- /dev/null +++ b/test/transformer_test.dart @@ -0,0 +1,41 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:angel_test/angel_test.dart'; +import 'package:test/test.dart'; + +main() { + TestClient client; + + setUp(() async { + var app = new Angel(); + var vDir = new CachingVirtualDirectory( + source: new Directory('test'), + transformers: [new ExtensionTransformer()]); + await app.configure(vDir); + await vDir.transformersLoaded.then((map) { + print('Loaded transformer map: $map'); + }); + client = await connectTo(app); + }); + + tearDown(() => client.close()); + + test('foo', () async { + var response = await client.get('/index.ext'); + print('Response: ${response.body}'); + expect(response, hasBody('.txt')); + }); +} + +class ExtensionTransformer implements FileTransformer { + @override + FileInfo declareOutput(FileInfo file) { + return file.extension == '.ext' ? null : file.changeExtension('.ext'); + } + + @override + FutureOr transform(FileInfo file) => + file.changeText(file.extension).changeExtension('.ext'); +} From ed389479f53da1816bbb31437774ac8c3d13242b Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 22:11:27 -0400 Subject: [PATCH 31/67] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d3b019c..4edda4a9 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Transformers are supported by `VirtualDirectory` and `CachingVirtualDirectory`. To create a transformer: ```dart -class MinifierTransformer { +class MinifierTransformer extends FileTransformer { /// Use this to declare outputs, and indicate if your transformer /// will compile a file. @override From 8f2ef4f740ced6b824f66ac22cb2a8771d85015a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 22:20:58 -0400 Subject: [PATCH 32/67] Pragma --- lib/src/cache.dart | 20 ++++++++++++++++++-- lib/src/virtual_directory.dart | 14 +++++++++++++- pubspec.yaml | 2 +- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/src/cache.dart b/lib/src/cache.dart index f75f2635..8e093733 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -95,7 +95,15 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } - if (noCache == true) { + bool shouldNotCache = noCache == true; + + if (!shouldNotCache) { + shouldNotCache = + req.headers.value(HttpHeaders.CACHE_CONTROL) == 'no-cache' || + req.headers.value(HttpHeaders.PRAGMA) == 'no-cache'; + } + + if (shouldNotCache) { res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache'; return super.serveFile(file, stat, req, res); } else { @@ -181,7 +189,15 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveAsset(fileInfo, req, res); } - if (noCache == true) { + bool shouldNotCache = noCache == true; + + if (!shouldNotCache) { + shouldNotCache = + req.headers.value(HttpHeaders.CACHE_CONTROL) == 'no-cache' || + req.headers.value(HttpHeaders.PRAGMA) == 'no-cache'; + } + + if (shouldNotCache) { res.headers[HttpHeaders.CACHE_CONTROL] = 'private, max-age=0, no-cache'; return super.serveAsset(fileInfo, req, res); } else { diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 9a7f1717..be22b073 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -217,6 +217,16 @@ class VirtualDirectory implements AngelPlugin { if (await index.exists()) { return await serveFile(index, stat, req, res); } + + // Try to compile an asset + if (_transformerMap.isNotEmpty && + _transformerMap.containsKey(index.absolute.path)) { + return await serveAsset( + new FileInfo.fromFile( + new File(_transformerMap[index.absolute.path])), + req, + res); + } } return true; @@ -358,7 +368,9 @@ class VirtualDirectory implements AngelPlugin { var asset = new FileInfo.fromFile(entity); var compiled = await compileAsset(asset); if (compiled == null) - p.finish(message: '"${entity.absolute.path}" did not require compilation; skipping it.'); + p.finish( + message: + '"${entity.absolute.path}" did not require compilation; skipping it.'); else { p.finish( message: diff --git a/pubspec.yaml b/pubspec.yaml index 03bdcd62..b951db6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.0 +version: 1.2.1 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 From 8062399b3545ad6fb2fa783c1b53779b987f092a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 22:51:47 -0400 Subject: [PATCH 33/67] Fix build to sink --- lib/src/virtual_directory.dart | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index be22b073..71ef4ab3 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -372,6 +372,10 @@ class VirtualDirectory implements AngelPlugin { message: '"${entity.absolute.path}" did not require compilation; skipping it.'); else { + var outFile = new File(compiled.filename); + if (!await outFile.exists()) await outFile.create(recursive: true); + var sink = outFile.openWrite(); + await compiled.content.pipe(sink); p.finish( message: 'Built "${entity.absolute.path}" to "${compiled.filename}".', diff --git a/pubspec.yaml b/pubspec.yaml index b951db6c..d7d300ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.1 +version: 1.2.2 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 From 6df23c7fee40e27a55dbc9aae22142ed4c950396 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 23:05:07 -0400 Subject: [PATCH 34/67] +1 --- lib/src/virtual_directory.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 71ef4ab3..7d4a95c0 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -263,8 +263,8 @@ class VirtualDirectory implements AngelPlugin { var value = req.headers.value(HttpHeaders.ACCEPT); bool acceptable = value == null || value.isEmpty || - value.contains(mimeType) || - value.contains('*/*'); + value?.contains(mimeType) == true || + value?.contains('*/*') == true; if (!acceptable) throw new AngelHttpException( new UnsupportedError( diff --git a/pubspec.yaml b/pubspec.yaml index d7d300ff..8428edde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.2 +version: 1.2.2+1 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 From 21f031c2070448bf28e2d3dd06ab3ac868c29315 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 23:13:01 -0400 Subject: [PATCH 35/67] octet stream --- lib/src/file_info.dart | 2 +- lib/src/virtual_directory.dart | 4 ++-- pubspec.yaml | 2 +- test/all_test.dart | 8 ++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/file_info.dart b/lib/src/file_info.dart index f0394b64..2e1a6c16 100644 --- a/lib/src/file_info.dart +++ b/lib/src/file_info.dart @@ -26,7 +26,7 @@ abstract class FileInfo { factory FileInfo.fromFile(File file) => new _FileInfoImpl( () => file.openRead(), file.absolute.path, - lookupMimeType(file.path), + lookupMimeType(file.path) ?? 'application/octet-stream', file.statSync().modified); /// Creates a [FileInfo] describing a file that might not even exists to begin with. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 7d4a95c0..5c098eca 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -262,8 +262,8 @@ class VirtualDirectory implements AngelPlugin { void _ensureContentTypeAllowed(String mimeType, RequestContext req) { var value = req.headers.value(HttpHeaders.ACCEPT); bool acceptable = value == null || - value.isEmpty || - value?.contains(mimeType) == true || + value?.isNotEmpty != true || + (mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) || value?.contains('*/*') == true; if (!acceptable) throw new AngelHttpException( diff --git a/pubspec.yaml b/pubspec.yaml index 8428edde..013b9cf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.2+1 +version: 1.2.2+3 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 diff --git a/test/all_test.dart b/test/all_test.dart index a5fa5516..892007de 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -64,4 +64,12 @@ main() { var response = await client.get("$url/virtual"); expect(response.body, equals("index!")); }); + + test('chrome accept', () async { + var response = await client.get("$url/virtual", headers: { + HttpHeaders.ACCEPT: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' + }); + expect(response.body, equals("index!")); + }); } From ea3d209a936c8e86d80669c6edf2b1b87e724332 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 15 Jun 2017 23:30:54 -0400 Subject: [PATCH 36/67] Run twice --- lib/src/cache.dart | 7 +++--- lib/src/file_info.dart | 7 ++++-- pubspec.yaml | 3 ++- test/cache_test.dart | 2 +- test/foo.mustache | 1 + test/transformer_test.dart | 47 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 test/foo.mustache diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 8e093733..599ebf0f 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -130,7 +130,7 @@ class CachingVirtualDirectory extends VirtualDirectory { } } - if (req.headers[HttpHeaders.IF_MODIFIED_SINCE] != null) { + if (req.headers.ifModifiedSince != null) { try { var ifModifiedSince = req.headers.ifModifiedSince; @@ -225,11 +225,12 @@ class CachingVirtualDirectory extends VirtualDirectory { } } - if (req.headers[HttpHeaders.IF_MODIFIED_SINCE] != null) { + if (req.headers.ifModifiedSince != null) { try { var ifModifiedSince = req.headers.ifModifiedSince; - if (ifModifiedSince.compareTo(fileInfo.lastModified) >= 0) { + if (fileInfo.lastModified != null && + ifModifiedSince.compareTo(fileInfo.lastModified) >= 0) { res.statusCode = HttpStatus.NOT_MODIFIED; setCachedHeaders(fileInfo.lastModified, req, res); diff --git a/lib/src/file_info.dart b/lib/src/file_info.dart index 2e1a6c16..095bd198 100644 --- a/lib/src/file_info.dart +++ b/lib/src/file_info.dart @@ -69,8 +69,11 @@ class _FileInfoImpl implements FileInfo { String get extension => p.extension(filename); @override - FileInfo changeFilename(String newFilename) => - new _FileInfoImpl(getContent, newFilename, mimeType, lastModified); + FileInfo changeFilename(String newFilename) => new _FileInfoImpl( + getContent, + newFilename, + lookupMimeType(newFilename) ?? mimeType ?? 'application/octet-stream', + lastModified); @override FileInfo changeExtension(String newExtension) => diff --git a/pubspec.yaml b/pubspec.yaml index 013b9cf7..4e4bebf2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.2+3 +version: 1.2.2+4 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 @@ -18,4 +18,5 @@ dev_dependencies: angel_diagnostics: ^1.0.0 angel_test: ^1.0.0 http: ^0.11.3 + mustache4dart: ^1.1.0 test: ^0.12.13 diff --git a/test/cache_test.dart b/test/cache_test.dart index b7ed5432..83225fa0 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -53,7 +53,7 @@ main() { test('if-modified-since', () async { var response = await client.get("$url", headers: { HttpHeaders.IF_MODIFIED_SINCE: - formatDateForHttp(new DateTime.now()..add(new Duration(days: 365))) + formatDateForHttp(new DateTime.now().add(new Duration(days: 365))) }); print('Response status: ${response.statusCode}'); diff --git a/test/foo.mustache b/test/foo.mustache new file mode 100644 index 00000000..f0521e3f --- /dev/null +++ b/test/foo.mustache @@ -0,0 +1 @@ +

{{foo}}

\ No newline at end of file diff --git a/test/transformer_test.dart b/test/transformer_test.dart index 69b43548..495fdf64 100644 --- a/test/transformer_test.dart +++ b/test/transformer_test.dart @@ -1,12 +1,14 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_static/angel_static.dart'; import 'package:angel_test/angel_test.dart'; +import 'package:mustache4dart/mustache4dart.dart' as ms; import 'package:test/test.dart'; main() { - TestClient client; + TestClient client, client2; setUp(() async { var app = new Angel(); @@ -18,15 +20,39 @@ main() { print('Loaded transformer map: $map'); }); client = await connectTo(app); + + var app2 = new Angel(); + var vDir2 = new CachingVirtualDirectory( + source: new Directory('test'), + transformers: [ + new MustacheTransformer({'foo': 'bar'}) + ]); + await app2.configure(vDir2); + await vDir2.transformersLoaded.then((map) { + print('Loaded transformer map2: $map'); + }); + client2 = await connectTo(app2); }); - tearDown(() => client.close()); + tearDown(() => client.close().then((_) => client2.close())); test('foo', () async { var response = await client.get('/index.ext'); print('Response: ${response.body}'); expect(response, hasBody('.txt')); }); + + test('request twice in a row', () async { + var response = await client2.get('/foo.html'); + print('Response: ${response.body}'); + print('Response headers: ${response.headers}'); + expect(response, hasBody('

bar

')); + + var response2 = await client2.get('/foo.html'); + expect(response2, hasHeader(HttpHeaders.CONTENT_TYPE, ContentType.HTML.mimeType)); + print('Response2: ${response2.body}'); + expect(response2, hasBody('

bar

')); + }); } class ExtensionTransformer implements FileTransformer { @@ -39,3 +65,20 @@ class ExtensionTransformer implements FileTransformer { FutureOr transform(FileInfo file) => file.changeText(file.extension).changeExtension('.ext'); } + +class MustacheTransformer implements FileTransformer { + final Map locals; + + MustacheTransformer(this.locals); + + @override + FileInfo declareOutput(FileInfo file) => + file.extension == '.mustache' ? file.changeExtension('.html') : null; + + @override + FutureOr transform(FileInfo file) async { + var template = await file.content.transform(UTF8.decoder).join(); + var compiled = ms.render(template, locals ?? {}); + return file.changeExtension('.html').changeText(compiled); + } +} From a55f97c87157a84ac3b65747cf74701408b77ca1 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Tue, 20 Jun 2017 15:57:03 -0400 Subject: [PATCH 37/67] 1.2.3 --- CHANGELOG.md | 3 + lib/src/virtual_directory.dart | 11 ++-- pubspec.yaml | 2 +- test/issue41_test.dart | 61 +++++++++++++++++++ test/node_modules/swagger-ui-dist/index.html | 10 +++ .../swagger-ui-dist/swagger-ui.css | 3 + test/node_modules/swagger-ui-dist/test.js | 1 + 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 test/issue41_test.dart create mode 100644 test/node_modules/swagger-ui-dist/index.html create mode 100644 test/node_modules/swagger-ui-dist/swagger-ui.css create mode 100644 test/node_modules/swagger-ui-dist/test.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dc6ee309 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.2.3 +Fixed #40 and #41, which dealt with paths being improperly served when using a +`publicPath`. \ No newline at end of file diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 5c098eca..94c08a92 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -107,8 +107,8 @@ class VirtualDirectory implements AngelPlugin { void serve(Router router) { // _printDebug('Source directory: ${source.absolute.path}'); // _printDebug('Public path prefix: "$_prefix"'); - router.get('$publicPath/*', - (RequestContext req, ResponseContext res) async { + //router.get('$publicPath/*', + router.get('$_prefix/*', (RequestContext req, ResponseContext res) async { var path = req.path.replaceAll(_straySlashes, ''); return servePath(path, req, res); }); @@ -124,7 +124,7 @@ class VirtualDirectory implements AngelPlugin { } close() async { - if (!_transformerLoad.isCompleted) { + if (!_transformerLoad.isCompleted && _transformers.isNotEmpty) { _transformerLoad.completeError(new StateError( 'VirtualDirectory was closed before all transformers loaded.')); } @@ -176,10 +176,13 @@ class VirtualDirectory implements AngelPlugin { servePath(String path, RequestContext req, ResponseContext res) async { if (_prefix.isNotEmpty) { - path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), ''); + // Only replace the *first* incidence + // Resolve: https://github.com/angel-dart/angel/issues/41 + path = path.replaceFirst(new RegExp('^' + _pathify(_prefix)), ''); } if (path.isEmpty) path = '.'; + path = path.replaceAll(_straySlashes, ''); var absolute = source.absolute.uri.resolve(path).toFilePath(); var stat = await FileStat.stat(absolute); diff --git a/pubspec.yaml b/pubspec.yaml index 4e4bebf2..e6e1fed1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.2.2+4 +version: 1.2.3 dependencies: angel_framework: ^1.0.0-dev cli_util: ^0.1.1 diff --git a/test/issue41_test.dart b/test/issue41_test.dart new file mode 100644 index 00000000..212918a3 --- /dev/null +++ b/test/issue41_test.dart @@ -0,0 +1,61 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_diagnostics/angel_diagnostics.dart'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:angel_test/angel_test.dart'; +import 'package:test/test.dart'; + +final Directory swaggerUiDistDir = + new Directory('test/node_modules/swagger-ui-dist'); + +main() async { + TestClient client; + String swaggerUiCssContents, swaggerTestJsContents; + + setUp(() async { + // Load file contents + swaggerUiCssContents = + await new File.fromUri(swaggerUiDistDir.uri.resolve('swagger-ui.css')) + .readAsString(); + swaggerTestJsContents = + await new File.fromUri(swaggerUiDistDir.uri.resolve('test.js')) + .readAsString(); + + // Initialize app + var app = new Angel(); + await Future.forEach([ + new VirtualDirectory(source: swaggerUiDistDir, publicPath: 'swagger/'), + logRequests() + ], app.configure); + + 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); + }); +} diff --git a/test/node_modules/swagger-ui-dist/index.html b/test/node_modules/swagger-ui-dist/index.html new file mode 100644 index 00000000..6607e2bc --- /dev/null +++ b/test/node_modules/swagger-ui-dist/index.html @@ -0,0 +1,10 @@ + + + + Swagger... + + +

Hello!

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

bar

')); - - var response2 = await client2.get('/foo.html'); - expect(response2, hasHeader(HttpHeaders.CONTENT_TYPE, ContentType.HTML.mimeType)); - print('Response2: ${response2.body}'); - expect(response2, hasBody('

bar

')); - }); -} - -class ExtensionTransformer implements FileTransformer { - @override - FileInfo declareOutput(FileInfo file) { - return file.extension == '.ext' ? null : file.changeExtension('.ext'); - } - - @override - FutureOr transform(FileInfo file) => - file.changeText(file.extension).changeExtension('.ext'); -} - -class MustacheTransformer implements FileTransformer { - final Map locals; - - MustacheTransformer(this.locals); - - @override - FileInfo declareOutput(FileInfo file) => - file.extension == '.mustache' ? file.changeExtension('.html') : null; - - @override - FutureOr transform(FileInfo file) async { - var template = await file.content.transform(UTF8.decoder).join(); - var compiled = ms.render(template, locals ?? {}); - return file.changeExtension('.html').changeText(compiled); - } -} From 3f2c29571b00dd749fa8379d92601a1cbb85474f Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 24 Sep 2017 01:02:10 -0400 Subject: [PATCH 42/67] 1.3.0-alpha --- README.md | 10 +++++--- lib/src/cache.dart | 26 ++----------------- pubspec.yaml | 2 +- test/all_test.dart | 24 ------------------ test/cache_test.dart | 10 +++++++- test/push_state_test.dart | 53 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 54 deletions(-) create mode 100644 test/push_state_test.dart diff --git a/README.md b/README.md index a551d3e5..343a327b 100644 --- a/README.md +++ b/README.md @@ -9,25 +9,27 @@ In `pubspec.yaml`: ```yaml dependencies: - angel_static: ^1.3.0 + angel_static: ^1.3.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 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; main() async { var app = new Angel(); + var fs = const LocalFileSystem(); // Normal static server - var vDir = new VirtualDirectory(source: new Directory('./public')); + var vDir = new VirtualDirectory(app, fs, source: new Directory('./public')); // Send Cache-Control, ETag, etc. as well - var vDir = new CachingVirtualDirectory(source: new Directory('./public')); + var vDir = new CachingVirtualDirectory(app, fs, source: new Directory('./public')); // Mount the VirtualDirectory's request handler app.use(vDir.handleRequest); diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 8a06cda7..329883e7 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -128,38 +128,16 @@ class CachingVirtualDirectory extends VirtualDirectory { } } - var queue = new StreamQueue(file.openRead()); - - return new Future(() async { - var buf = new Uint8List(50), hanging = []; - int added = 0; - - while (added < 50) { - var deficit = 50 - added; - var next = await queue.next; - - for (int i = 0; i < deficit; i++) { - buf[added + i] = next[i]; - } - - if (next.length > deficit) { - hanging.addAll(next.skip(deficit)); - } - } - + return file.readAsBytes().then((buf) { var etag = _etags[file.absolute.path] = weakEtag(buf); - res.statusCode = 200; res.headers ..['ETag'] = etag ..['content-type'] = lookupMimeType(file.path) ?? 'application/octet-stream'; setCachedHeaders(stat.modified, req, res); - res.add(buf); - res.add(hanging); - - return queue.rest.pipe(res).then((_) => false); + return false; }); } } diff --git a/pubspec.yaml b/pubspec.yaml index 6852094f..983facd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: mime: ^0.9.3 path: ^1.4.2 dev_dependencies: - angel_test: ^1.0.0 + angel_test: ^1.1.0-alpha http: ^0.11.3 mustache4dart: ^1.1.0 test: ^0.12.13 diff --git a/test/all_test.dart b/test/all_test.dart index ad7f4fa7..c01c3def 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -75,28 +75,4 @@ main() { }); expect(response.body, equals("index!")); }); - - test('can gzip: just gzip', () async { - var response = await client - .get("$url/sample.txt", headers: {'accept-encoding': 'gzip'}); - expect(response.body, equals("Hello world")); - expect(response.headers['content-type'], contains("text/plain")); - expect(response.headers['content-encoding'], 'gzip'); - }); - - test('can gzip: wildcard', () async { - var response = await client - .get("$url/sample.txt", headers: {'accept-encoding': 'foo, *'}); - expect(response.body, equals("Hello world")); - expect(response.headers['content-type'], contains("text/plain")); - expect(response.headers['content-encoding'], 'gzip'); - }); - - test('can gzip: gzip and friends', () async { - var response = await client.get("$url/sample.txt", - headers: {'accept-encoding': 'gzip, deflate, br'}); - expect(response.body, equals("Hello world")); - expect(response.headers['content-type'], contains("text/plain")); - expect(response.headers['content-encoding'], 'gzip'); - }); } diff --git a/test/cache_test.dart b/test/cache_test.dart index 49425bb8..d2c4de9d 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -3,6 +3,7 @@ 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'; @@ -26,6 +27,13 @@ main() { app.dumpTree(showMatchers: true); + app.logger = new 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 app.startServer(); url = "http://${server.address.host}:${server.port}"; }); @@ -43,7 +51,7 @@ main() { expect(response.statusCode, equals(200)); expect( - ['ETag', 'cache-control', 'expires', 'last-modified'], + ['etag', 'cache-control', 'expires', 'last-modified'], everyElement(predicate( response.headers.containsKey, 'contained in response headers'))); }); diff --git a/test/push_state_test.dart b/test/push_state_test.dart new file mode 100644 index 00000000..8d141071 --- /dev/null +++ b/test/push_state_test.dart @@ -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 = new MemoryFileSystem(); + + var webDir = fileSystem.directory('web'); + await webDir.create(recursive: true); + + var indexFile = webDir.childFile('index.html'); + await indexFile.writeAsString('index'); + + app = new Angel(); + + var vDir = new VirtualDirectory( + app, + fileSystem, + source: webDir, + ); + + app + ..use(vDir.handleRequest) + ..use(vDir.pushState('index.html')) + ..use('Fallback'); + + app.logger = new 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'); + }); +} From 16c42d14f9a7c94b5279adb49b196c53fbbc4e28 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 18 Nov 2017 00:43:20 -0500 Subject: [PATCH 43/67] Fixed #27 --- CHANGELOG.md | 3 +++ lib/src/cache.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a510dd2..1b8b343f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 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. diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 329883e7..64458078 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -15,7 +15,7 @@ String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT'; /// Generates a weak ETag from the given buffer. String weakEtag(List buf) { - return 'W/${buf.length}' + BASE64URL.encode(buf); + return 'W/${buf.length}' + BASE64URL.encode(buf.take(50).toList()); } /// Returns a string representation of the given [CacheAccessLevel]. diff --git a/pubspec.yaml b/pubspec.yaml index 983facd8..dea1f22c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.0-alpha +version: 1.3.0-alpha+1 dependencies: angel_framework: ^1.1.0-alpha file: ^2.0.0 From 0c18eefccaece26391822e732183317c5c0133cb Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 18 Nov 2017 01:12:59 -0500 Subject: [PATCH 44/67] 1.3.1 --- CHANGELOG.md | 4 ++ example/main.dart | 18 ++++++++ lib/src/cache.dart | 4 +- lib/src/virtual_directory.dart | 80 +++++++++++++++++++++++++++++++--- pubspec.yaml | 2 +- 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 example/main.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8b343f..92074220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.3.1 +* 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). diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..fd606cf7 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,18 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + +main() async { + var app = new Angel(); + var fs = const LocalFileSystem(); + var vDir = new VirtualDirectory( + app, + fs, + allowDirectoryListing: true, + source: fs.directory(fs.currentDirectory), + ); + app.use(vDir.handleRequest); + + var server = await app.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 64458078..5982e08a 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -60,13 +60,15 @@ class CachingVirtualDirectory extends VirtualDirectory { this.noCache: false, this.onlyInProduction: false, this.useEtags: true, + bool allowDirectoryListing, String publicPath, callback(File file, RequestContext req, ResponseContext res)}) : super(app, fileSystem, source: source, indexFileNames: indexFileNames ?? ['index.html'], publicPath: publicPath ?? '/', - callback: callback); + callback: callback, + allowDirectoryListing: allowDirectoryListing); @override Future serveFile( diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index ae52c190..939d0a49 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:angel_framework/angel_framework.dart'; import 'package:file/file.dart'; import 'package:mime/mime.dart'; +import 'package:path/path.dart' as p; final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -42,11 +43,15 @@ class VirtualDirectory { /// 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; + VirtualDirectory(this.app, this.fileSystem, {Directory source, this.indexFileNames: const ['index.html'], this.publicPath: '/', - this.callback}) { + this.callback, + this.allowDirectoryListing: false}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { _source = source; @@ -68,13 +73,22 @@ class VirtualDirectory { } /// A handler that serves the file at the given path, unless the user has requested that path. - RequestMiddleware pushState(String 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. + RequestMiddleware 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 new Future.value(true); + + if (accepts?.isNotEmpty == true) { + if (!accepts.any(req.accepts)) return new Future.value(true); + } + return servePath(vPath, req, res); }; } @@ -93,15 +107,15 @@ class VirtualDirectory { var absolute = source.absolute.uri.resolve(path).toFilePath(); var stat = await fileSystem.stat(absolute); - return await serveStat(absolute, stat, req, res); + return await serveStat(absolute, path, stat, req, res); } /// Writes the file at the path given by the [stat] to a response. - Future serveStat(String absolute, FileStat stat, RequestContext req, + Future serveStat(String absolute, String relative, FileStat stat, RequestContext req, ResponseContext res) async { if (stat.type == FileSystemEntityType.DIRECTORY) return await serveDirectory( - fileSystem.directory(absolute), stat, req, res); + 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) { @@ -112,7 +126,7 @@ class VirtualDirectory { } /// Serves the index file of a [directory], if it exists. - Future serveDirectory(Directory directory, FileStat stat, + Future serveDirectory(Directory directory, String relative, FileStat stat, RequestContext req, ResponseContext res) async { for (String indexFileName in indexFileNames) { final index = @@ -122,6 +136,60 @@ class VirtualDirectory { } } + if (allowDirectoryListing == true) { + res.headers['content-type'] = 'text/html'; + res + ..write('') + ..write('') + ..write( + '') + ..write('') + ..write(''); + + res.write('
  • ..
  • '); + + List entities = await directory + .list(followLinks: false) + .toList() + .then((l) => new 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 += '/'; + + res.write('
  • $type $stub
  • '); + } + + res..write(''); + return false; + } + return true; } diff --git a/pubspec.yaml b/pubspec.yaml index dea1f22c..e808124b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.0-alpha+1 +version: 1.3.1 dependencies: angel_framework: ^1.1.0-alpha file: ^2.0.0 From b1de9715d48e4d6bd634641fe1843c43b7851b08 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 18 Nov 2017 01:13:29 -0500 Subject: [PATCH 45/67] +2 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e808124b..0b4f06ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.1 +version: 1.3.0-alpha+2 dependencies: angel_framework: ^1.1.0-alpha file: ^2.0.0 From 6945c538c3f5703462409c1f216824446fa62a56 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 28 Nov 2017 16:09:16 -0500 Subject: [PATCH 46/67] Strict pushstate --- CHANGELOG.md | 5 ++++- lib/src/virtual_directory.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92074220..0871ace9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# 1.3.1 +# 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. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 939d0a49..dc2a5f2e 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -86,7 +86,7 @@ class VirtualDirectory { if (path == vPath) return new Future.value(true); if (accepts?.isNotEmpty == true) { - if (!accepts.any(req.accepts)) return new Future.value(true); + if (!accepts.any((x) => req.accepts(x, strict: true))) return new Future.value(true); } return servePath(vPath, req, res); diff --git a/pubspec.yaml b/pubspec.yaml index 0b4f06ed..10bf1f72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.19.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.0-alpha+2 +version: 1.3.0 dependencies: angel_framework: ^1.1.0-alpha file: ^2.0.0 From cceabd3c7d250764ce39f8a2171cdf3a764e234a Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 13:38:22 -0400 Subject: [PATCH 47/67] Dart 2 fixes + bump version + allow buffering --- CHANGELOG.md | 4 ++++ analysis_options.yaml | 3 ++- example/main.dart | 3 ++- lib/src/cache.dart | 10 +++++----- lib/src/virtual_directory.dart | 35 ++++++++++++++++++++++------------ pubspec.yaml | 5 +++-- test/all_test.dart | 7 +++++-- test/cache_sample.dart | 4 +++- test/cache_test.dart | 6 ++++-- 9 files changed, 51 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0871ace9..80f84867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 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. diff --git a/analysis_options.yaml b/analysis_options.yaml index 518eb901..eae1e42a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,2 +1,3 @@ analyzer: - strong-mode: true \ No newline at end of file + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index fd606cf7..593f2551 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,6 +4,7 @@ import 'package:file/local.dart'; main() async { var app = new Angel(); + var http = new AngelHttp(app); var fs = const LocalFileSystem(); var vDir = new VirtualDirectory( app, @@ -13,6 +14,6 @@ main() async { ); app.use(vDir.handleRequest); - var server = await app.startServer('127.0.0.1', 3000); + var server = await http.startServer('127.0.0.1', 3000); print('Listening at http://${server.address.address}:${server.port}'); } diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 5982e08a..4dc7020a 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -1,8 +1,6 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; import 'package:angel_framework/angel_framework.dart'; -import 'package:async/async.dart'; +import 'package:dart2_constant/convert.dart'; import 'package:file/file.dart'; import 'package:intl/intl.dart'; import 'package:mime/mime.dart'; @@ -15,7 +13,7 @@ String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT'; /// Generates a weak ETag from the given buffer. String weakEtag(List buf) { - return 'W/${buf.length}' + BASE64URL.encode(buf.take(50).toList()); + return 'W/${buf.length}' + base64Url.encode(buf.take(50).toList()); } /// Returns a string representation of the given [CacheAccessLevel]. @@ -61,6 +59,7 @@ class CachingVirtualDirectory extends VirtualDirectory { this.onlyInProduction: false, this.useEtags: true, bool allowDirectoryListing, + bool useStream, String publicPath, callback(File file, RequestContext req, ResponseContext res)}) : super(app, fileSystem, @@ -68,7 +67,8 @@ class CachingVirtualDirectory extends VirtualDirectory { indexFileNames: indexFileNames ?? ['index.html'], publicPath: publicPath ?? '/', callback: callback, - allowDirectoryListing: allowDirectoryListing); + allowDirectoryListing: allowDirectoryListing, + useStream: useStream); @override Future serveFile( diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index dc2a5f2e..8c795753 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -46,12 +46,18 @@ class VirtualDirectory { /// 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 useStream; + VirtualDirectory(this.app, this.fileSystem, {Directory source, this.indexFileNames: const ['index.html'], this.publicPath: '/', this.callback, - this.allowDirectoryListing: false}) { + this.allowDirectoryListing: false, + this.useStream: true}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { _source = source; @@ -86,7 +92,8 @@ class VirtualDirectory { if (path == vPath) return new Future.value(true); if (accepts?.isNotEmpty == true) { - if (!accepts.any((x) => req.accepts(x, strict: true))) return new Future.value(true); + if (!accepts.any((x) => req.accepts(x, strict: true))) + return new Future.value(true); } return servePath(vPath, req, res); @@ -111,8 +118,8 @@ class VirtualDirectory { } /// Writes the file at the path given by the [stat] to a response. - Future serveStat(String absolute, String relative, FileStat stat, RequestContext req, - ResponseContext res) async { + Future 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); @@ -126,8 +133,8 @@ class VirtualDirectory { } /// Serves the index file of a [directory], if it exists. - Future serveDirectory(Directory directory, String relative, FileStat stat, - RequestContext req, ResponseContext res) async { + Future 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)); @@ -177,11 +184,9 @@ class VirtualDirectory { type = '[Directory]'; else if (entity is Link) type = '[Link]'; - if (relative.isNotEmpty) - href = '/' + relative + '/' + stub; + if (relative.isNotEmpty) href = '/' + relative + '/' + stub; - if (entity is Directory) - href += '/'; + if (entity is Directory) href += '/'; res.write('
  • $type $stub
  • '); } @@ -215,14 +220,20 @@ class VirtualDirectory { if (callback != null) { var r = callback(file, req, res); r = r is Future ? await r : r; - if (r != null && r != true) return r; + return r == true; + //if (r != null && r != true) return r; } var type = lookupMimeType(file.path) ?? 'application/octet-stream'; _ensureContentTypeAllowed(type, req); res.headers['content-type'] = type; - await file.openRead().pipe(res); + if (useStream != true) { + res.buffer.add(await file.readAsBytes()); + } else { + await file.openRead().pipe(res); + } + return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index 10bf1f72..8265af4a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,13 @@ name: angel_static description: Static server middleware for Angel. environment: - sdk: ">=1.19.0" + sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.0 +version: 1.3.0+1 dependencies: angel_framework: ^1.1.0-alpha + dart2_constant: ^1.0.0 file: ^2.0.0 intl: ">=0.0.0 <1.0.0" mime: ^0.9.3 diff --git a/test/all_test.dart b/test/all_test.dart index c01c3def..5178ffc3 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -8,12 +8,14 @@ import 'package:test/test.dart'; main() { Angel app; + AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); String url; Client client = new Client(); setUp(() async { app = new Angel(); + http = new AngelHttp(app); app.logger = new Logger('angel')..onRecord.listen(print); app.use( @@ -26,6 +28,7 @@ main() { app.use( new VirtualDirectory(app, const LocalFileSystem(), source: testDir, + useStream: false, indexFileNames: ['index.php', 'index.txt']).handleRequest, ); @@ -33,12 +36,12 @@ main() { app.dumpTree(showMatchers: true); - var server = await app.startServer(); + var server = await http.startServer(); url = "http://${server.address.host}:${server.port}"; }); tearDown(() async { - if (app.httpServer != null) await app.httpServer.close(force: true); + if (http.httpServer != null) await http.httpServer.close(force: true); }); test('can serve files, with correct Content-Type', () async { diff --git a/test/cache_sample.dart b/test/cache_sample.dart index 6a6e0dc7..86f0e09a 100644 --- a/test/cache_sample.dart +++ b/test/cache_sample.dart @@ -5,8 +5,10 @@ import 'package:file/local.dart'; main() async { Angel app; + AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); app = new Angel(); + http = new AngelHttp(app); app.use( new CachingVirtualDirectory(app, const LocalFileSystem(), @@ -20,6 +22,6 @@ main() async { app.dumpTree(showMatchers: true); - var server = await app.startServer(); + var server = await http.startServer(); print('Open at http://${server.address.host}:${server.port}'); } diff --git a/test/cache_test.dart b/test/cache_test.dart index d2c4de9d..285e01a2 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -9,12 +9,14 @@ import 'package:test/test.dart'; main() { Angel app; + AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); String url; Client client = new Client(); setUp(() async { app = new Angel(); + http = new AngelHttp(app); app.use( new CachingVirtualDirectory(app, const LocalFileSystem(), @@ -34,12 +36,12 @@ main() { if (rec.stackTrace != null) print(rec.stackTrace); }); - var server = await app.startServer(); + var server = await http.startServer(); url = "http://${server.address.host}:${server.port}"; }); tearDown(() async { - if (app.httpServer != null) await app.httpServer.close(force: true); + if (http.httpServer != null) await http.httpServer.close(force: true); }); test('sets etag, cache-control, expires, last-modified', () async { From 5288b5065fade97d77437ea69d54e5c6382e17a2 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 13:38:58 -0400 Subject: [PATCH 48/67] Build both dev and stable Dart --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3939d628..a9e2c109 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,4 @@ language: dart +dart: + - dev + - stable \ No newline at end of file From 2e8573cd9658e02721323595f33591186eb747a3 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 28 Aug 2018 10:58:28 -0400 Subject: [PATCH 49/67] Bump to 2.0.0-alpha --- .gitignore | 1 + CHANGELOG.md | 4 ++++ README.md | 2 +- example/main.dart | 2 +- lib/src/cache.dart | 4 ++-- lib/src/virtual_directory.dart | 23 ++++++++++++----------- pubspec.yaml | 13 ++++++------- test/all_test.dart | 8 ++++---- test/cache_sample.dart | 4 ++-- test/cache_test.dart | 4 ++-- test/issue41_test.dart | 2 +- test/push_state_test.dart | 6 +++--- 12 files changed, 39 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 917abe7c..ced607a7 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ packages # Include when developing application packages. pubspec.lock +.dart_tool \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f84867..389d8478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.0.0 +* Upgrade dependencies to Angel 2 + file@5. +* Replace `useStream` with `useBuffer`. + # 1.3.0+1 * Dart 2 fixes. * Enable optionally writing responses to the buffer instead of streaming. diff --git a/README.md b/README.md index 343a327b..760b9773 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ In `pubspec.yaml`: ```yaml dependencies: - angel_static: ^1.3.0-alpha + angel_static: ^2.0.0-alpha ``` # Usage diff --git a/example/main.dart b/example/main.dart index 593f2551..4bbee909 100644 --- a/example/main.dart +++ b/example/main.dart @@ -12,7 +12,7 @@ main() async { allowDirectoryListing: true, source: fs.directory(fs.currentDirectory), ); - app.use(vDir.handleRequest); + app.fallback(vDir.handleRequest); var server = await http.startServer('127.0.0.1', 3000); print('Listening at http://${server.address.address}:${server.port}'); diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 4dc7020a..acb52b06 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -59,7 +59,7 @@ class CachingVirtualDirectory extends VirtualDirectory { this.onlyInProduction: false, this.useEtags: true, bool allowDirectoryListing, - bool useStream, + bool useBuffer, String publicPath, callback(File file, RequestContext req, ResponseContext res)}) : super(app, fileSystem, @@ -68,7 +68,7 @@ class CachingVirtualDirectory extends VirtualDirectory { publicPath: publicPath ?? '/', callback: callback, allowDirectoryListing: allowDirectoryListing, - useStream: useStream); + useBuffer: useBuffer); @override Future serveFile( diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 8c795753..1ba07ce9 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:angel_framework/angel_framework.dart'; import 'package:file/file.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; @@ -49,7 +50,7 @@ class VirtualDirectory { /// 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 useStream; + final bool useBuffer; VirtualDirectory(this.app, this.fileSystem, {Directory source, @@ -57,7 +58,7 @@ class VirtualDirectory { this.publicPath: '/', this.callback, this.allowDirectoryListing: false, - this.useStream: true}) { + this.useBuffer: true}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { _source = source; @@ -83,7 +84,7 @@ class VirtualDirectory { /// 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. - RequestMiddleware pushState(String path, {Iterable accepts}) { + RequestHandler pushState(String path, {Iterable accepts}) { var vPath = path.replaceAll(_straySlashes, ''); if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath'; @@ -120,12 +121,12 @@ class VirtualDirectory { /// Writes the file at the path given by the [stat] to a response. Future serveStat(String absolute, String relative, FileStat stat, RequestContext req, ResponseContext res) async { - if (stat.type == FileSystemEntityType.DIRECTORY) + if (stat.type == FileSystemEntityType.directory) return await serveDirectory( fileSystem.directory(absolute), relative, stat, req, res); - else if (stat.type == FileSystemEntityType.FILE) + else if (stat.type == FileSystemEntityType.file) return await serveFile(fileSystem.file(absolute), stat, req, res); - else if (stat.type == FileSystemEntityType.LINK) { + else if (stat.type == FileSystemEntityType.link) { var link = fileSystem.link(absolute); return await servePath(await link.resolveSymbolicLinks(), req, res); } else @@ -144,7 +145,7 @@ class VirtualDirectory { } if (allowDirectoryListing == true) { - res.headers['content-type'] = 'text/html'; + res.contentType = new MediaType('text', 'html'); res ..write('') ..write('') @@ -226,12 +227,12 @@ class VirtualDirectory { var type = lookupMimeType(file.path) ?? 'application/octet-stream'; _ensureContentTypeAllowed(type, req); - res.headers['content-type'] = type; + res.contentType = new MediaType.parse(type); - if (useStream != true) { - res.buffer.add(await file.readAsBytes()); + if (useBuffer == true) { + await res.sendFile(file); } else { - await file.openRead().pipe(res); + await res.streamFile(file); } return false; diff --git a/pubspec.yaml b/pubspec.yaml index 8265af4a..c30ec1c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,16 +4,15 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 1.3.0+1 +version: 2.0.0-alpha dependencies: - angel_framework: ^1.1.0-alpha - dart2_constant: ^1.0.0 - file: ^2.0.0 + angel_framework: ^2.0.0-alpha + file: ^5.0.0 intl: ">=0.0.0 <1.0.0" mime: ^0.9.3 path: ^1.4.2 dev_dependencies: - angel_test: ^1.1.0-alpha + angel_test: ^2.0.0-alpha http: ^0.11.3 - mustache4dart: ^1.1.0 - test: ^0.12.13 + mustache4dart: ^3.0.0-dev.0.0 + test: ^1.0.0 diff --git a/test/all_test.dart b/test/all_test.dart index 5178ffc3..971ab023 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -18,21 +18,21 @@ main() { http = new AngelHttp(app); app.logger = new Logger('angel')..onRecord.listen(print); - app.use( + app.fallback( new VirtualDirectory(app, const LocalFileSystem(), source: testDir, publicPath: '/virtual', indexFileNames: ['index.txt']).handleRequest, ); - app.use( + app.fallback( new VirtualDirectory(app, const LocalFileSystem(), source: testDir, - useStream: false, + useBuffer: true, indexFileNames: ['index.php', 'index.txt']).handleRequest, ); - app.use('Fallback'); + app.fallback((req, res) => 'Fallback'); app.dumpTree(showMatchers: true); diff --git a/test/cache_sample.dart b/test/cache_sample.dart index 86f0e09a..444054fe 100644 --- a/test/cache_sample.dart +++ b/test/cache_sample.dart @@ -10,7 +10,7 @@ main() async { app = new Angel(); http = new AngelHttp(app); - app.use( + app.fallback( new CachingVirtualDirectory(app, const LocalFileSystem(), source: testDir, maxAge: 350, @@ -18,7 +18,7 @@ main() async { indexFileNames: ['index.txt']).handleRequest, ); - app.get('*', 'Fallback'); + app.get('*', (req, res) => 'Fallback'); app.dumpTree(showMatchers: true); diff --git a/test/cache_test.dart b/test/cache_test.dart index 285e01a2..8fea55cc 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -18,14 +18,14 @@ main() { app = new Angel(); http = new AngelHttp(app); - app.use( + app.fallback( new CachingVirtualDirectory(app, const LocalFileSystem(), source: testDir, maxAge: 350, onlyInProduction: false, //publicPath: '/virtual', indexFileNames: ['index.txt']).handleRequest, ); - app.get('*', 'Fallback'); + app.get('*', (req, res) => 'Fallback'); app.dumpTree(showMatchers: true); diff --git a/test/issue41_test.dart b/test/issue41_test.dart index 5db45080..e4bd0938 100644 --- a/test/issue41_test.dart +++ b/test/issue41_test.dart @@ -26,7 +26,7 @@ main() async { var app = new Angel(); app.logger = new Logger('angel')..onRecord.listen(print); - app.use( + app.fallback( new VirtualDirectory(app, const LocalFileSystem(), source: swaggerUiDistDir, publicPath: 'swagger/') .handleRequest, diff --git a/test/push_state_test.dart b/test/push_state_test.dart index 8d141071..8aede873 100644 --- a/test/push_state_test.dart +++ b/test/push_state_test.dart @@ -28,9 +28,9 @@ main() { ); app - ..use(vDir.handleRequest) - ..use(vDir.pushState('index.html')) - ..use('Fallback'); + ..fallback(vDir.handleRequest) + ..fallback(vDir.pushState('index.html')) + ..fallback((req, res) => 'Fallback'); app.logger = new Logger('push_state') ..onRecord.listen( From e403f5da2d97d274f7247855ab2b456951281348 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 20 Oct 2018 20:24:44 -0400 Subject: [PATCH 50/67] 2.0.0 --- CHANGELOG.md | 1 + lib/src/cache.dart | 14 +++++--------- lib/src/virtual_directory.dart | 3 ++- pubspec.yaml | 5 ++--- test/cache_test.dart | 3 ++- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389d8478..66600ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 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. diff --git a/lib/src/cache.dart b/lib/src/cache.dart index acb52b06..ae84cc93 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -1,16 +1,12 @@ import 'dart:async'; +import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; import 'package:dart2_constant/convert.dart'; import 'package:file/file.dart'; -import 'package:intl/intl.dart'; +//import 'package:intl/intl.dart'; import 'package:mime/mime.dart'; import 'virtual_directory.dart'; -final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss'); - -/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`. -String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT'; - /// Generates a weak ETag from the given buffer. String weakEtag(List buf) { return 'W/${buf.length}' + base64Url.encode(buf.take(50).toList()); @@ -59,7 +55,7 @@ class CachingVirtualDirectory extends VirtualDirectory { this.onlyInProduction: false, this.useEtags: true, bool allowDirectoryListing, - bool useBuffer, + bool useBuffer: false, String publicPath, callback(File file, RequestContext req, ResponseContext res)}) : super(app, fileSystem, @@ -150,11 +146,11 @@ class CachingVirtualDirectory extends VirtualDirectory { res.headers ..['cache-control'] = '$privacy, max-age=${maxAge ?? 0}' - ..['last-modified'] = formatDateForHttp(modified); + ..['last-modified'] = HttpDate.format(modified); if (maxAge != null) { var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0)); - res.headers['expires'] = formatDateForHttp(expiry); + res.headers['expires'] = HttpDate.format(expiry); } } } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 1ba07ce9..fc90fe31 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -58,7 +58,7 @@ class VirtualDirectory { this.publicPath: '/', this.callback, this.allowDirectoryListing: false, - this.useBuffer: true}) { + this.useBuffer: false}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { _source = source; @@ -230,6 +230,7 @@ class VirtualDirectory { res.contentType = new MediaType.parse(type); if (useBuffer == true) { + res.useBuffer(); await res.sendFile(file); } else { await res.streamFile(file); diff --git a/pubspec.yaml b/pubspec.yaml index c30ec1c3..b8e5a40f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,14 +1,13 @@ name: angel_static -description: Static server middleware for Angel. +description: Static server middleware for Angel. Use this to serve files to users. environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.0.0-alpha +version: 2.0.0 dependencies: angel_framework: ^2.0.0-alpha file: ^5.0.0 - intl: ">=0.0.0 <1.0.0" mime: ^0.9.3 path: ^1.4.2 dev_dependencies: diff --git a/test/cache_test.dart b/test/cache_test.dart index 8fea55cc..c767cb58 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -1,3 +1,4 @@ +import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_static/angel_static.dart'; import 'package:file/file.dart'; @@ -61,7 +62,7 @@ main() { test('if-modified-since', () async { var response = await client.get("$url", headers: { 'if-modified-since': - formatDateForHttp(new DateTime.now().add(new Duration(days: 365))) + HttpDate.format(new DateTime.now().add(new Duration(days: 365))) }); print('Response status: ${response.statusCode}'); From 4f642cec9066daf7d5429cb558a866f317dac213 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 20 Oct 2018 20:30:04 -0400 Subject: [PATCH 51/67] Pub nits --- lib/src/cache.dart | 2 +- pubspec.yaml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/cache.dart b/lib/src/cache.dart index ae84cc93..c28f5b47 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; -import 'package:dart2_constant/convert.dart'; import 'package:file/file.dart'; //import 'package:intl/intl.dart'; import 'package:mime/mime.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index b8e5a40f..cdbe8a8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,10 +8,13 @@ version: 2.0.0 dependencies: angel_framework: ^2.0.0-alpha file: ^5.0.0 + http_parser: ^3.0.0 mime: ^0.9.3 path: ^1.4.2 dev_dependencies: angel_test: ^2.0.0-alpha http: ^0.11.3 + logging: ^0.11.0 + matcher: ^0.12.0 mustache4dart: ^3.0.0-dev.0.0 test: ^1.0.0 From 9f63ef57d9229de8eb073870890c0ac6c96284a3 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 13 Nov 2018 16:25:17 -0500 Subject: [PATCH 52/67] 2.0.1 --- CHANGELOG.md | 5 +++++ README.md | 9 +++++---- example/main.dart | 28 ++++++++++++++++++++++------ lib/src/cache.dart | 20 +++++++++----------- lib/src/virtual_directory.dart | 25 ++++++++++++++----------- pubspec.yaml | 6 ++---- test/HELLO.md | 2 ++ test/all_test.dart | 3 ++- test/cache_sample.dart | 1 + test/cache_test.dart | 3 ++- 10 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 test/HELLO.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 66600ff0..f3dfc388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 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`. diff --git a/README.md b/README.md index 760b9773..be8aba6b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ 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'; @@ -32,10 +33,10 @@ main() async { var vDir = new CachingVirtualDirectory(app, fs, source: new Directory('./public')); // Mount the VirtualDirectory's request handler - app.use(vDir.handleRequest); + app.fallback(vDir.handleRequest); // Start your server!!! - await app.startServer(); + await new AngelHttp(app).startServer(); } ``` @@ -49,10 +50,10 @@ the user is requesting that file. This can be very useful for SPA's. var vDir = new CachingVirtualDirectory(...); // Mount it -app.use(vDir.handleRequest); +app.fallback(vDir.handleRequest); // Fallback to index.html on 404 -app.use(vDir.pushState('index.html')); +app.fallback(vDir.pushState('index.html')); ``` # Options diff --git a/example/main.dart b/example/main.dart index 4bbee909..ca11c5b8 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,17 +1,33 @@ 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() async { var app = new Angel(); var http = new AngelHttp(app); var fs = const LocalFileSystem(); - var vDir = new VirtualDirectory( - app, - fs, - allowDirectoryListing: true, - source: fs.directory(fs.currentDirectory), - ); + var vDir = new CachingVirtualDirectory(app, fs, + allowDirectoryListing: true, + source: fs.currentDirectory, + 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 = new 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); var server = await http.startServer('127.0.0.1', 3000); diff --git a/lib/src/cache.dart b/lib/src/cache.dart index c28f5b47..01cb4548 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; import 'package:file/file.dart'; -//import 'package:intl/intl.dart'; -import 'package:mime/mime.dart'; import 'virtual_directory.dart'; /// Generates a weak ETag from the given buffer. @@ -85,21 +83,21 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } else { if (useEtags == true) { - var etags = req.headers['if-none-match']; + var etagsToMatchAgainst = req.headers['if-none-match']; - if (etags?.isNotEmpty == true) { + if (etagsToMatchAgainst?.isNotEmpty == true) { bool hasBeenModified = false; - for (var etag in etags) { + for (var etag in etagsToMatchAgainst) { if (etag == '*') hasBeenModified = true; else { - hasBeenModified = _etags.containsKey(file.absolute.path) && - _etags[file.absolute.path] == etag; + hasBeenModified = !_etags.containsKey(file.absolute.path) || + _etags[file.absolute.path] != etag; } } - if (hasBeenModified) { + if (!hasBeenModified) { res.statusCode = 304; setCachedHeaders(stat.modified, req, res); return new Future.value(false); @@ -128,11 +126,11 @@ class CachingVirtualDirectory extends VirtualDirectory { return file.readAsBytes().then((buf) { var etag = _etags[file.absolute.path] = weakEtag(buf); - res.statusCode = 200; + //res.statusCode = 200; res.headers ..['ETag'] = etag - ..['content-type'] = - lookupMimeType(file.path) ?? 'application/octet-stream'; + ..['content-type'] = res.app.mimeTypeResolver.lookup(file.path) ?? + 'application/octet-stream'; setCachedHeaders(stat.modified, req, res); res.add(buf); return false; diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index fc90fe31..4060f6cc 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:angel_framework/angel_framework.dart'; import 'package:file/file.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); @@ -70,7 +69,8 @@ class VirtualDirectory { /// Responds to incoming HTTP requests. Future handleRequest(RequestContext req, ResponseContext res) { - if (req.method != 'GET') return new Future.value(true); + if (req.method != 'GET' && req.method != 'HEAD') + return new Future.value(true); var path = req.path.replaceAll(_straySlashes, ''); if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix)) @@ -114,6 +114,11 @@ class VirtualDirectory { 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); } @@ -140,11 +145,13 @@ class VirtualDirectory { final index = fileSystem.file(directory.absolute.uri.resolve(indexFileName)); if (await index.exists()) { + if (req.method == 'HEAD') return false; return await serveFile(index, stat, req, res); } } if (allowDirectoryListing == true) { + if (req.method == 'HEAD') return false; res.contentType = new MediaType('text', 'html'); res ..write('') @@ -216,7 +223,7 @@ class VirtualDirectory { /// Writes the contents of a file to a response. Future serveFile( File file, FileStat stat, RequestContext req, ResponseContext res) async { - res.statusCode = 200; + if (req.method == 'HEAD') return false; if (callback != null) { var r = callback(file, req, res); @@ -225,17 +232,13 @@ class VirtualDirectory { //if (r != null && r != true) return r; } - var type = lookupMimeType(file.path) ?? 'application/octet-stream'; + var type = + app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream'; _ensureContentTypeAllowed(type, req); res.contentType = new MediaType.parse(type); - if (useBuffer == true) { - res.useBuffer(); - await res.sendFile(file); - } else { - await res.streamFile(file); - } - + if (useBuffer == true) res.useBuffer(); + await res.streamFile(file); return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index cdbe8a8e..37fc94a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,17 +4,15 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.0.0 +version: 2.0.1 dependencies: angel_framework: ^2.0.0-alpha file: ^5.0.0 http_parser: ^3.0.0 - mime: ^0.9.3 path: ^1.4.2 dev_dependencies: angel_test: ^2.0.0-alpha - http: ^0.11.3 + http: logging: ^0.11.0 matcher: ^0.12.0 - mustache4dart: ^3.0.0-dev.0.0 test: ^1.0.0 diff --git a/test/HELLO.md b/test/HELLO.md new file mode 100644 index 00000000..de89e4df --- /dev/null +++ b/test/HELLO.md @@ -0,0 +1,2 @@ +# hello +world! \ No newline at end of file diff --git a/test/all_test.dart b/test/all_test.dart index 971ab023..b7526785 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -1,4 +1,5 @@ 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'; @@ -41,7 +42,7 @@ main() { }); tearDown(() async { - if (http.httpServer != null) await http.httpServer.close(force: true); + if (http.server != null) await http.server.close(force: true); }); test('can serve files, with correct Content-Type', () async { diff --git a/test/cache_sample.dart b/test/cache_sample.dart index 444054fe..aaab4012 100644 --- a/test/cache_sample.dart +++ b/test/cache_sample.dart @@ -1,4 +1,5 @@ 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'; diff --git a/test/cache_test.dart b/test/cache_test.dart index c767cb58..83ebce65 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -1,5 +1,6 @@ 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'; @@ -42,7 +43,7 @@ main() { }); tearDown(() async { - if (http.httpServer != null) await http.httpServer.close(force: true); + if (http.server != null) await http.server.close(force: true); }); test('sets etag, cache-control, expires, last-modified', () async { From bf8071d34cd133faf72b61fa9c15d9ef0e91f3ff Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 13 Nov 2018 18:37:31 -0500 Subject: [PATCH 53/67] Fix typo --- CHANGELOG.md | 3 +++ lib/src/virtual_directory.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3dfc388..f7b2963a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 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. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 4060f6cc..0a5a6d13 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -159,7 +159,7 @@ class VirtualDirectory { ..write( '') ..write('') - ..write(''); + ..write(''); res.write('
  • ..
  • '); diff --git a/pubspec.yaml b/pubspec.yaml index 37fc94a2..1b6da222 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.0.1 +version: 2.0.2 dependencies: angel_framework: ^2.0.0-alpha file: ^5.0.0 From 585d497b1596727191ba4ea0791190f3cb0a5aa4 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 14 Nov 2018 00:43:47 -0500 Subject: [PATCH 54/67] 2.1.0 --- CHANGELOG.md | 4 ++ README.md | 4 +- lib/src/cache.dart | 100 ++++++++++++++++++++++---------- lib/src/virtual_directory.dart | 101 ++++++++++++++++++++++++++++++--- pubspec.yaml | 7 ++- 5 files changed, 177 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b2963a..0f8a1e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 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. diff --git a/README.md b/README.md index be8aba6b..288e05d9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ [![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 middleware for Angel. +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`: diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 01cb4548..3cede6cf 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -1,13 +1,14 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; import 'package:file/file.dart'; import 'virtual_directory.dart'; -/// Generates a weak ETag from the given buffer. -String weakEtag(List buf) { - return 'W/${buf.length}' + base64Url.encode(buf.take(50).toList()); +/// Generates an MD5 ETag from the given buffer. +String md5Etag(List buf) { + return hex.encode(md5.convert(buf).bytes); } /// Returns a string representation of the given [CacheAccessLevel]. @@ -67,6 +68,8 @@ class CachingVirtualDirectory extends VirtualDirectory { @override Future serveFile( File file, FileStat stat, RequestContext req, ResponseContext res) { + res.headers['accept-ranges'] = 'bytes'; + if (onlyInProduction == true && req.app.isProduction != true) { return super.serveFile(file, stat, req, res); } @@ -82,8 +85,55 @@ class CachingVirtualDirectory extends VirtualDirectory { 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 new Future.value(false); + } else if (ifRange) { + // Return 200, just send the whole thing. + return res.streamFile(file).then((_) => false); + } + } catch (_) { + throw new 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; @@ -97,38 +147,30 @@ class CachingVirtualDirectory extends VirtualDirectory { } } - if (!hasBeenModified) { - res.statusCode = 304; - setCachedHeaders(stat.modified, req, res); - return new Future.value(false); + if (!ifRange) { + if (!hasBeenModified) { + res.statusCode = 304; + setCachedHeaders(stat.modified, req, res); + return new Future.value(false); + } + } else { + if (!hasBeenModified) { + // Continue serving like a regular range... + return super.serveFile(file, stat, req, res); + } else { + // Otherwise, send the whole thing. + return res.streamFile(file).then((_) => false); + } } } } - if (req.headers.ifModifiedSince != null) { - try { - var ifModifiedSince = req.headers.ifModifiedSince; - - if (ifModifiedSince.compareTo(stat.modified) >= 0) { - res.statusCode = 304; - setCachedHeaders(stat.modified, req, res); - - if (_etags.containsKey(file.absolute.path)) - res.headers['ETag'] = _etags[file.absolute.path]; - - return new Future.value(false); - } - } catch (_) { - throw new AngelHttpException.badRequest( - message: 'Invalid date for If-Modified-Since header.'); - } - } - return file.readAsBytes().then((buf) { - var etag = _etags[file.absolute.path] = weakEtag(buf); + if (useEtags) { + res.headers['ETag'] = _etags[file.absolute.path] = md5Etag(buf); + } //res.statusCode = 200; res.headers - ..['ETag'] = etag ..['content-type'] = res.app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream'; setCachedHeaders(stat.modified, req, res); diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 0a5a6d13..db74049e 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -3,6 +3,7 @@ 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 = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); @@ -223,22 +224,108 @@ class VirtualDirectory { /// Writes the contents of a file to a response. Future serveFile( File file, FileStat stat, RequestContext req, ResponseContext res) async { - if (req.method == 'HEAD') return false; + if (req.method == 'HEAD') { + res.headers['accept-ranges'] = 'bytes'; + return false; + } if (callback != null) { - var r = callback(file, req, res); - r = r is Future ? await r : r; - return r == true; - //if (r != null && r != true) return r; + 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 = new MediaType.parse(type); - if (useBuffer == true) res.useBuffer(); - await res.streamFile(file); + + if (req.headers.value('range')?.startsWith('bytes ') != true) { + await res.streamFile(file); + } else { + var header = new RangeHeader.parse(req.headers.value('range')); + var items = RangeHeader.foldItems(header.items); + var totalFileSize = await file.length(); + header = new 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 new AngelHttpException( + new 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 new AngelHttpException( + new Exception("Given range $item is out of bounds."), + statusCode: 416, + message: "Given range $item is out of bounds."); + } + } + + if (header.items.isEmpty) { + throw new AngelHttpException(null, + statusCode: 416, message: '`Range` header may not be empty.'); + } else if (header.items.length == 1) { + var item = header.items[0]; + Stream> 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 = new 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.pipe(res); + return false; + } else { + var transformer = new 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 = new MediaType( + 'multipart', 'byteranges', {'boundary': transformer.boundary}); + await file.openRead().transform(transformer).pipe(res); + return false; + } + } + return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index 1b6da222..a71aa605 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,15 +1,18 @@ name: angel_static -description: Static server middleware for Angel. Use this to serve files to users. +description: Static server middleware for Angel. Also capable of serving Range responses. environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.0.2 +version: 2.1.0 dependencies: angel_framework: ^2.0.0-alpha + 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: From 2d44ba1693fa6ec33345d6397fe82346c8186abd Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 14 Nov 2018 00:53:07 -0500 Subject: [PATCH 55/67] Ignore media --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ced607a7..5292e53c 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,6 @@ packages # Include when developing application packages. pubspec.lock -.dart_tool \ No newline at end of file +.dart_tool +*.mp3 +*.mp4 \ No newline at end of file From 9a038ded48975b8dc6a7324532eb67a6f043935d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 14 Nov 2018 00:58:35 -0500 Subject: [PATCH 56/67] Bump to 2.1.1 --- CHANGELOG.md | 3 +++ lib/src/virtual_directory.dart | 3 ++- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8a1e35..701b3155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 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. diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index db74049e..a4fa4b93 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -72,7 +72,7 @@ class VirtualDirectory { Future handleRequest(RequestContext req, ResponseContext res) { if (req.method != 'GET' && req.method != 'HEAD') return new Future.value(true); - var path = req.path.replaceAll(_straySlashes, ''); + var path = req.uri.path.replaceAll(_straySlashes, ''); if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix)) return new Future.value(true); @@ -196,6 +196,7 @@ class VirtualDirectory { if (relative.isNotEmpty) href = '/' + relative + '/' + stub; if (entity is Directory) href += '/'; + href = Uri.encodeFull(href); res.write('
  • $type $stub
  • '); } diff --git a/pubspec.yaml b/pubspec.yaml index a71aa605..d51c1082 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.0 +version: 2.1.1 dependencies: angel_framework: ^2.0.0-alpha convert: ^2.0.0 From 6a3309c9872c8a378bfee7d4a332590b49f44622 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 27 Jan 2019 17:14:54 -0500 Subject: [PATCH 57/67] 2.1.2 --- CHANGELOG.md | 3 +++ example/main.dart | 14 +++++++++----- lib/src/cache.dart | 30 ++++++------------------------ lib/src/virtual_directory.dart | 7 +------ pubspec.yaml | 2 +- 5 files changed, 20 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701b3155..329b1625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 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. diff --git a/example/main.dart b/example/main.dart index ca11c5b8..b923181c 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,14 +4,17 @@ import 'package:angel_static/angel_static.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; -main() async { +main(List args) async { var app = new Angel(); var http = new AngelHttp(app); var fs = const LocalFileSystem(); - var vDir = new CachingVirtualDirectory(app, fs, - allowDirectoryListing: true, - source: fs.currentDirectory, - maxAge: const Duration(days: 24).inSeconds); + var vDir = new 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') @@ -31,5 +34,6 @@ main() async { app.fallback(vDir.handleRequest); 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}'); } diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 3cede6cf..2669d321 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -1,16 +1,9 @@ import 'dart:async'; import 'dart:io' show HttpDate; import 'package:angel_framework/angel_framework.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; import 'package:file/file.dart'; import 'virtual_directory.dart'; -/// Generates an MD5 ETag from the given buffer. -String md5Etag(List buf) { - return hex.encode(md5.convert(buf).bytes); -} - /// Returns a string representation of the given [CacheAccessLevel]. String accessLevelToString(CacheAccessLevel accessLevel) { switch (accessLevel) { @@ -114,8 +107,7 @@ class CachingVirtualDirectory extends VirtualDirectory { return new Future.value(false); } else if (ifRange) { - // Return 200, just send the whole thing. - return res.streamFile(file).then((_) => false); + return super.serveFile(file, stat, req, res); } } catch (_) { throw new AngelHttpException.badRequest( @@ -154,28 +146,18 @@ class CachingVirtualDirectory extends VirtualDirectory { return new Future.value(false); } } else { - if (!hasBeenModified) { - // Continue serving like a regular range... - return super.serveFile(file, stat, req, res); - } else { - // Otherwise, send the whole thing. - return res.streamFile(file).then((_) => false); - } + return super.serveFile(file, stat, req, res); } } } - return file.readAsBytes().then((buf) { + return file.lastModified().then((stamp) { if (useEtags) { - res.headers['ETag'] = _etags[file.absolute.path] = md5Etag(buf); + res.headers['ETag'] = _etags[file.absolute.path] = stamp.millisecondsSinceEpoch.toString(); } - //res.statusCode = 200; - res.headers - ..['content-type'] = res.app.mimeTypeResolver.lookup(file.path) ?? - 'application/octet-stream'; + setCachedHeaders(stat.modified, req, res); - res.add(buf); - return false; + return res.streamFile(file).then((_) => false); }); } } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index a4fa4b93..bc24dc2a 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -146,13 +146,11 @@ class VirtualDirectory { final index = fileSystem.file(directory.absolute.uri.resolve(indexFileName)); if (await index.exists()) { - if (req.method == 'HEAD') return false; return await serveFile(index, stat, req, res); } } if (allowDirectoryListing == true) { - if (req.method == 'HEAD') return false; res.contentType = new MediaType('text', 'html'); res ..write('') @@ -225,10 +223,7 @@ class VirtualDirectory { /// Writes the contents of a file to a response. Future serveFile( File file, FileStat stat, RequestContext req, ResponseContext res) async { - if (req.method == 'HEAD') { - res.headers['accept-ranges'] = 'bytes'; - return false; - } + res.headers['accept-ranges'] = 'bytes'; if (callback != null) { return await req.app.executeHandler( diff --git a/pubspec.yaml b/pubspec.yaml index d51c1082..d81c9611 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.1 +version: 2.1.2 dependencies: angel_framework: ^2.0.0-alpha convert: ^2.0.0 From c7edb440852e938c9d2709c45d940eb1c10518fc Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 27 Jan 2019 17:15:02 -0500 Subject: [PATCH 58/67] fmt --- lib/src/cache.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 2669d321..8e9eb9ba 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -153,9 +153,10 @@ class CachingVirtualDirectory extends VirtualDirectory { return file.lastModified().then((stamp) { if (useEtags) { - res.headers['ETag'] = _etags[file.absolute.path] = stamp.millisecondsSinceEpoch.toString(); + res.headers['ETag'] = _etags[file.absolute.path] = + stamp.millisecondsSinceEpoch.toString(); } - + setCachedHeaders(stat.modified, req, res); return res.streamFile(file).then((_) => false); }); From 42779a85e444e3820cb76b9dcd97fe6e4381f3e6 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 27 Jan 2019 19:30:40 -0500 Subject: [PATCH 59/67] 2.1.2+1 --- CHANGELOG.md | 3 +++ lib/src/cache.dart | 2 +- lib/src/virtual_directory.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329b1625..5598e4df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.1.2+1 +* Fix a typo that prevented `Range` requests from working. + # 2.1.2 * Patch support for range+streaming in Caching server. diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 8e9eb9ba..b3edbb37 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -158,7 +158,7 @@ class CachingVirtualDirectory extends VirtualDirectory { } setCachedHeaders(stat.modified, req, res); - return res.streamFile(file).then((_) => false); + return super.serveFile(file, stat, req, res); }); } } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index bc24dc2a..1b85d584 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -240,7 +240,7 @@ class VirtualDirectory { res.contentType = new MediaType.parse(type); if (useBuffer == true) res.useBuffer(); - if (req.headers.value('range')?.startsWith('bytes ') != true) { + if (req.headers.value('range')?.startsWith('bytes=') != true) { await res.streamFile(file); } else { var header = new RangeHeader.parse(req.headers.value('range')); diff --git a/pubspec.yaml b/pubspec.yaml index d81c9611..8948b2a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=1.8.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.2 +version: 2.1.2+1 dependencies: angel_framework: ^2.0.0-alpha convert: ^2.0.0 From fd53da891f18d547a459a906f475cb4bbdc3a0cd Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 2 May 2019 19:29:09 -0400 Subject: [PATCH 60/67] 2.1.3 --- CHANGELOG.md | 5 ++++ README.md | 10 +++---- analysis_options.yaml | 7 ++++- example/main.dart | 9 +++--- lib/src/cache.dart | 24 +++++++-------- lib/src/virtual_directory.dart | 54 +++++++++++++++++----------------- pubspec.yaml | 5 ++-- test/all_test.dart | 12 ++++---- test/cache_sample.dart | 6 ++-- test/cache_test.dart | 12 ++++---- test/issue41_test.dart | 6 ++-- test/push_state_test.dart | 8 ++--- 12 files changed, 85 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5598e4df..503ecb49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 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. diff --git a/README.md b/README.md index 288e05d9..f20e9a1e 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,20 @@ import 'package:angel_static/angel_static.dart'; import 'package:file/local.dart'; main() async { - var app = new Angel(); + var app = Angel(); var fs = const LocalFileSystem(); // Normal static server - var vDir = new VirtualDirectory(app, fs, source: new Directory('./public')); + var vDir = VirtualDirectory(app, fs, source: Directory('./public')); // Send Cache-Control, ETag, etc. as well - var vDir = new CachingVirtualDirectory(app, fs, source: new Directory('./public')); + var vDir = CachingVirtualDirectory(app, fs, source: Directory('./public')); // Mount the VirtualDirectory's request handler app.fallback(vDir.handleRequest); // Start your server!!! - await new AngelHttp(app).startServer(); + await AngelHttp(app).startServer(); } ``` @@ -49,7 +49,7 @@ the user is requesting that file. This can be very useful for SPA's. ```dart // Create VirtualDirectory as well -var vDir = new CachingVirtualDirectory(...); +var vDir = CachingVirtualDirectory(...); // Mount it app.fallback(vDir.handleRequest); diff --git a/analysis_options.yaml b/analysis_options.yaml index eae1e42a..085be64d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,8 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: - implicit-casts: false \ No newline at end of file + implicit-casts: false +linter: + rules: + - unnecessary_const + - unnecessary_new \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index b923181c..61791fda 100644 --- a/example/main.dart +++ b/example/main.dart @@ -5,10 +5,10 @@ import 'package:file/local.dart'; import 'package:logging/logging.dart'; main(List args) async { - var app = new Angel(); - var http = new AngelHttp(app); + var app = Angel(); + var http = AngelHttp(app); var fs = const LocalFileSystem(); - var vDir = new CachingVirtualDirectory( + var vDir = CachingVirtualDirectory( app, fs, allowDirectoryListing: true, @@ -24,7 +24,7 @@ main(List args) async { ..addExtension('md', 'text/plain') ..addExtension('yaml', 'text/plain'); - app.logger = new Logger('example') + app.logger = Logger('example') ..onRecord.listen((rec) { print(rec); if (rec.error != null) print(rec.error); @@ -32,6 +32,7 @@ main(List args) async { }); 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}'); diff --git a/lib/src/cache.dart b/lib/src/cache.dart index b3edbb37..cad01d39 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -12,7 +12,7 @@ String accessLevelToString(CacheAccessLevel accessLevel) { case CacheAccessLevel.PUBLIC: return 'public'; default: - throw new ArgumentError('Unrecognized cache access level: $accessLevel'); + throw ArgumentError('Unrecognized cache access level: $accessLevel'); } } @@ -38,16 +38,16 @@ class CachingVirtualDirectory extends VirtualDirectory { final int maxAge; CachingVirtualDirectory(Angel app, FileSystem fileSystem, - {this.accessLevel: CacheAccessLevel.PUBLIC, + {this.accessLevel = CacheAccessLevel.PUBLIC, Directory source, bool debug, Iterable indexFileNames, - this.maxAge: 0, - this.noCache: false, - this.onlyInProduction: false, - this.useEtags: true, + this.maxAge = 0, + this.noCache = false, + this.onlyInProduction = false, + this.useEtags = true, bool allowDirectoryListing, - bool useBuffer: false, + bool useBuffer = false, String publicPath, callback(File file, RequestContext req, ResponseContext res)}) : super(app, fileSystem, @@ -63,7 +63,7 @@ class CachingVirtualDirectory extends VirtualDirectory { File file, FileStat stat, RequestContext req, ResponseContext res) { res.headers['accept-ranges'] = 'bytes'; - if (onlyInProduction == true && req.app.isProduction != true) { + if (onlyInProduction == true && req.app.environment.isProduction != true) { return super.serveFile(file, stat, req, res); } @@ -105,12 +105,12 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } - return new Future.value(false); + return Future.value(false); } else if (ifRange) { return super.serveFile(file, stat, req, res); } } catch (_) { - throw new AngelHttpException.badRequest( + throw AngelHttpException.badRequest( message: 'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.'); } @@ -143,7 +143,7 @@ class CachingVirtualDirectory extends VirtualDirectory { if (!hasBeenModified) { res.statusCode = 304; setCachedHeaders(stat.modified, req, res); - return new Future.value(false); + return Future.value(false); } } else { return super.serveFile(file, stat, req, res); @@ -172,7 +172,7 @@ class CachingVirtualDirectory extends VirtualDirectory { ..['last-modified'] = HttpDate.format(modified); if (maxAge != null) { - var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0)); + var expiry = DateTime.now().add(Duration(seconds: maxAge ?? 0)); res.headers['expires'] = HttpDate.format(expiry); } } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 1b85d584..4eb0cd9e 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -5,8 +5,8 @@ import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart' as p; import 'package:range_header/range_header.dart'; -final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); -final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); +final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); +final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); String _pathify(String path) { var p = path.replaceAll(_straySlashes, ''); @@ -54,16 +54,16 @@ class VirtualDirectory { VirtualDirectory(this.app, this.fileSystem, {Directory source, - this.indexFileNames: const ['index.html'], - this.publicPath: '/', + this.indexFileNames = const ['index.html'], + this.publicPath = '/', this.callback, - this.allowDirectoryListing: false, - this.useBuffer: false}) { + this.allowDirectoryListing = false, + this.useBuffer = false}) { _prefix = publicPath.replaceAll(_straySlashes, ''); if (source != null) { _source = source; } else { - String dirPath = app.isProduction ? './build/web' : './web'; + String dirPath = app.environment.isProduction ? './build/web' : './web'; _source = fileSystem.directory(dirPath); } } @@ -71,11 +71,11 @@ class VirtualDirectory { /// Responds to incoming HTTP requests. Future handleRequest(RequestContext req, ResponseContext res) { if (req.method != 'GET' && req.method != 'HEAD') - return new Future.value(true); + return Future.value(true); var path = req.uri.path.replaceAll(_straySlashes, ''); if (_prefix?.isNotEmpty == true && !path.startsWith(_prefix)) - return new Future.value(true); + return Future.value(true); return servePath(path, req, res); } @@ -91,11 +91,11 @@ class VirtualDirectory { return (RequestContext req, ResponseContext res) { var path = req.path.replaceAll(_straySlashes, ''); - if (path == vPath) return new Future.value(true); + if (path == vPath) return Future.value(true); if (accepts?.isNotEmpty == true) { if (!accepts.any((x) => req.accepts(x, strict: true))) - return new Future.value(true); + return Future.value(true); } return servePath(vPath, req, res); @@ -108,7 +108,7 @@ class VirtualDirectory { if (_prefix.isNotEmpty) { // Only replace the *first* incidence // Resolve: https://github.com/angel-dart/angel/issues/41 - path = path.replaceFirst(new RegExp('^' + _pathify(_prefix)), ''); + path = path.replaceFirst(RegExp('^' + _pathify(_prefix)), ''); } if (path.isEmpty) path = '.'; @@ -151,7 +151,7 @@ class VirtualDirectory { } if (allowDirectoryListing == true) { - res.contentType = new MediaType('text', 'html'); + res.contentType = MediaType('text', 'html'); res ..write('') ..write('') @@ -165,7 +165,7 @@ class VirtualDirectory { List entities = await directory .list(followLinks: false) .toList() - .then((l) => new List.from(l)); + .then((l) => List.from(l)); entities.sort((a, b) { if (a is Directory) { if (b is Directory) return a.path.compareTo(b.path); @@ -213,8 +213,8 @@ class VirtualDirectory { (mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) || value?.contains('*/*') == true; if (!acceptable) - throw new AngelHttpException( - new UnsupportedError( + throw AngelHttpException( + UnsupportedError( 'Client requested $value, but server wanted to send $mimeType.'), statusCode: 406, message: '406 Not Acceptable'); @@ -237,16 +237,16 @@ class VirtualDirectory { res.headers['accept-ranges'] = 'bytes'; _ensureContentTypeAllowed(type, req); res.headers['accept-ranges'] = 'bytes'; - res.contentType = new MediaType.parse(type); + 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 = new RangeHeader.parse(req.headers.value('range')); + var header = RangeHeader.parse(req.headers.value('range')); var items = RangeHeader.foldItems(header.items); var totalFileSize = await file.length(); - header = new RangeHeader(items); + header = RangeHeader(items); for (var item in header.items) { bool invalid = false; @@ -257,23 +257,23 @@ class VirtualDirectory { invalid = item.end == -1; if (invalid) { - throw new AngelHttpException( - new Exception("Semantically invalid, or unbounded range."), + 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 new AngelHttpException( - new Exception("Given range $item is out of bounds."), + 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 new AngelHttpException(null, + throw AngelHttpException(null, statusCode: 416, message: '`Range` header may not be empty.'); } else if (header.items.length == 1) { var item = header.items[0]; @@ -298,7 +298,7 @@ class VirtualDirectory { } } - res.contentType = new MediaType.parse( + res.contentType = MediaType.parse( app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream'); res.statusCode = 206; @@ -307,7 +307,7 @@ class VirtualDirectory { await stream.pipe(res); return false; } else { - var transformer = new RangeHeaderTransformer( + var transformer = RangeHeaderTransformer( header, app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream', @@ -315,7 +315,7 @@ class VirtualDirectory { res.statusCode = 206; res.headers['content-length'] = transformer.computeContentLength(totalFileSize).toString(); - res.contentType = new MediaType( + res.contentType = MediaType( 'multipart', 'byteranges', {'boundary': transformer.boundary}); await file.openRead().transform(transformer).pipe(res); return false; diff --git a/pubspec.yaml b/pubspec.yaml index 8948b2a5..5bfde275 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: angel_static description: Static server middleware for Angel. Also capable of serving Range responses. environment: - sdk: ">=1.8.0 <3.0.0" + sdk: ">=2.0.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O version: 2.1.2+1 dependencies: - angel_framework: ^2.0.0-alpha + angel_framework: ^2.0.0-rc.0 convert: ^2.0.0 crypto: ^2.0.0 file: ^5.0.0 @@ -18,4 +18,5 @@ dev_dependencies: http: logging: ^0.11.0 matcher: ^0.12.0 + pedantic: ^1.0.0 test: ^1.0.0 diff --git a/test/all_test.dart b/test/all_test.dart index b7526785..0e337b9c 100644 --- a/test/all_test.dart +++ b/test/all_test.dart @@ -12,22 +12,22 @@ main() { AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); String url; - Client client = new Client(); + Client client = Client(); setUp(() async { - app = new Angel(); - http = new AngelHttp(app); - app.logger = new Logger('angel')..onRecord.listen(print); + app = Angel(); + http = AngelHttp(app); + app.logger = Logger('angel')..onRecord.listen(print); app.fallback( - new VirtualDirectory(app, const LocalFileSystem(), + VirtualDirectory(app, const LocalFileSystem(), source: testDir, publicPath: '/virtual', indexFileNames: ['index.txt']).handleRequest, ); app.fallback( - new VirtualDirectory(app, const LocalFileSystem(), + VirtualDirectory(app, const LocalFileSystem(), source: testDir, useBuffer: true, indexFileNames: ['index.php', 'index.txt']).handleRequest, diff --git a/test/cache_sample.dart b/test/cache_sample.dart index aaab4012..8f9c28bd 100644 --- a/test/cache_sample.dart +++ b/test/cache_sample.dart @@ -8,11 +8,11 @@ main() async { Angel app; AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); - app = new Angel(); - http = new AngelHttp(app); + app = Angel(); + http = AngelHttp(app); app.fallback( - new CachingVirtualDirectory(app, const LocalFileSystem(), + CachingVirtualDirectory(app, const LocalFileSystem(), source: testDir, maxAge: 350, onlyInProduction: false, diff --git a/test/cache_test.dart b/test/cache_test.dart index 83ebce65..c0aba9cb 100644 --- a/test/cache_test.dart +++ b/test/cache_test.dart @@ -14,14 +14,14 @@ main() { AngelHttp http; Directory testDir = const LocalFileSystem().directory('test'); String url; - Client client = new Client(); + Client client = Client(); setUp(() async { - app = new Angel(); - http = new AngelHttp(app); + app = Angel(); + http = AngelHttp(app); app.fallback( - new CachingVirtualDirectory(app, const LocalFileSystem(), + CachingVirtualDirectory(app, const LocalFileSystem(), source: testDir, maxAge: 350, onlyInProduction: false, //publicPath: '/virtual', indexFileNames: ['index.txt']).handleRequest, @@ -31,7 +31,7 @@ main() { app.dumpTree(showMatchers: true); - app.logger = new Logger('angel_static') + app.logger = Logger('angel_static') ..onRecord.listen((rec) { print(rec); if (rec.error != null) print(rec.error); @@ -63,7 +63,7 @@ main() { test('if-modified-since', () async { var response = await client.get("$url", headers: { 'if-modified-since': - HttpDate.format(new DateTime.now().add(new Duration(days: 365))) + HttpDate.format(DateTime.now().add(Duration(days: 365))) }); print('Response status: ${response.statusCode}'); diff --git a/test/issue41_test.dart b/test/issue41_test.dart index e4bd0938..b02cb245 100644 --- a/test/issue41_test.dart +++ b/test/issue41_test.dart @@ -23,11 +23,11 @@ main() async { .readAsString(); // Initialize app - var app = new Angel(); - app.logger = new Logger('angel')..onRecord.listen(print); + var app = Angel(); + app.logger = Logger('angel')..onRecord.listen(print); app.fallback( - new VirtualDirectory(app, const LocalFileSystem(), + VirtualDirectory(app, const LocalFileSystem(), source: swaggerUiDistDir, publicPath: 'swagger/') .handleRequest, ); diff --git a/test/push_state_test.dart b/test/push_state_test.dart index 8aede873..1312407f 100644 --- a/test/push_state_test.dart +++ b/test/push_state_test.dart @@ -11,7 +11,7 @@ main() { TestClient client; setUp(() async { - fileSystem = new MemoryFileSystem(); + fileSystem = MemoryFileSystem(); var webDir = fileSystem.directory('web'); await webDir.create(recursive: true); @@ -19,9 +19,9 @@ main() { var indexFile = webDir.childFile('index.html'); await indexFile.writeAsString('index'); - app = new Angel(); + app = Angel(); - var vDir = new VirtualDirectory( + var vDir = VirtualDirectory( app, fileSystem, source: webDir, @@ -32,7 +32,7 @@ main() { ..fallback(vDir.pushState('index.html')) ..fallback((req, res) => 'Fallback'); - app.logger = new Logger('push_state') + app.logger = Logger('push_state') ..onRecord.listen( (rec) { print(rec); From 6dd7ffea03abb76535da1f46a66453dcbbb27963 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 2 May 2019 19:29:43 -0400 Subject: [PATCH 61/67] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5bfde275..67357f25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=2.0.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.2+1 +version: 2.1.3 dependencies: angel_framework: ^2.0.0-rc.0 convert: ^2.0.0 From 99cef39faab39de97cbae1aa46a65c99f9f27b30 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 6 Jun 2019 10:33:40 -0400 Subject: [PATCH 62/67] Fix control flow formatting --- lib/src/cache.dart | 7 ++++--- lib/src/virtual_directory.dart | 37 ++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/src/cache.dart b/lib/src/cache.dart index cad01d39..0f859a39 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -96,8 +96,9 @@ class CachingVirtualDirectory extends VirtualDirectory { res.statusCode = 304; setCachedHeaders(stat.modified, req, res); - if (useEtags && _etags.containsKey(file.absolute.path)) + if (useEtags && _etags.containsKey(file.absolute.path)) { res.headers['ETag'] = _etags[file.absolute.path]; + } if (ifRange) { // Send the 206 like normal @@ -131,9 +132,9 @@ class CachingVirtualDirectory extends VirtualDirectory { bool hasBeenModified = false; for (var etag in etagsToMatchAgainst) { - if (etag == '*') + if (etag == '*') { hasBeenModified = true; - else { + } else { hasBeenModified = !_etags.containsKey(file.absolute.path) || _etags[file.absolute.path] != etag; } diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index 4eb0cd9e..ff2ed4e5 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -70,12 +70,14 @@ class VirtualDirectory { /// Responds to incoming HTTP requests. Future handleRequest(RequestContext req, ResponseContext res) { - if (req.method != 'GET' && req.method != 'HEAD') + if (req.method != 'GET' && req.method != 'HEAD') { return Future.value(true); + } 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); + } return servePath(path, req, res); } @@ -94,8 +96,9 @@ class VirtualDirectory { if (path == vPath) return Future.value(true); if (accepts?.isNotEmpty == true) { - if (!accepts.any((x) => req.accepts(x, strict: true))) + if (!accepts.any((x) => req.accepts(x, strict: true))) { return Future.value(true); + } } return servePath(vPath, req, res); @@ -117,8 +120,9 @@ class VirtualDirectory { var absolute = source.absolute.uri.resolve(path).toFilePath(); var parent = source.absolute.uri.toFilePath(); - if (!p.isWithin(parent, absolute) && !p.equals(parent, absolute)) + 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); @@ -127,16 +131,17 @@ class VirtualDirectory { /// Writes the file at the path given by the [stat] to a response. Future serveStat(String absolute, String relative, FileStat stat, RequestContext req, ResponseContext res) async { - if (stat.type == FileSystemEntityType.directory) + if (stat.type == FileSystemEntityType.directory) { return await serveDirectory( fileSystem.directory(absolute), relative, stat, req, res); - else if (stat.type == FileSystemEntityType.file) + } else if (stat.type == FileSystemEntityType.file) { return await serveFile(fileSystem.file(absolute), stat, req, res); - else if (stat.type == FileSystemEntityType.link) { + } else if (stat.type == FileSystemEntityType.link) { var link = fileSystem.link(absolute); return await servePath(await link.resolveSymbolicLinks(), req, res); - } else + } else { return true; + } } /// Serves the index file of a [directory], if it exists. @@ -171,9 +176,9 @@ class VirtualDirectory { if (b is Directory) return a.path.compareTo(b.path); return -1; } else if (a is File) { - if (b is Directory) + if (b is Directory) { return 1; - else if (b is File) return a.path.compareTo(b.path); + } else if (b is File) return a.path.compareTo(b.path); return -1; } else if (b is Link) return a.path.compareTo(b.path); @@ -185,11 +190,11 @@ class VirtualDirectory { var href = stub; String type; - if (entity is File) + if (entity is File) { type = '[File]'; - else if (entity is Directory) + } else if (entity is Directory) { type = '[Directory]'; - else if (entity is Link) type = '[Link]'; + } else if (entity is Link) type = '[Link]'; if (relative.isNotEmpty) href = '/' + relative + '/' + stub; @@ -212,12 +217,13 @@ class VirtualDirectory { value?.isNotEmpty != true || (mimeType?.isNotEmpty == true && value?.contains(mimeType) == true) || value?.contains('*/*') == true; - if (!acceptable) + 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. @@ -253,8 +259,9 @@ class VirtualDirectory { if (item.start != -1) { invalid = item.end != -1 && item.end < item.start; - } else + } else { invalid = item.end == -1; + } if (invalid) { throw AngelHttpException( From df90ce1bbc3602fa513fd80c3f2aa86769d2882b Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 6 Jun 2019 12:19:09 -0400 Subject: [PATCH 63/67] 2.1.3+1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 503ecb49..a6da3302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.1.3+1 +* Apply control flow lints. + # 2.1.3 * Apply lints. * Pin to Dart `>=2.0.0 <3.0.0`. From cc23c2a97e60e25869130b15f147b93092a9ed15 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 6 Jun 2019 12:19:16 -0400 Subject: [PATCH 64/67] Bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 67357f25..f18bdd8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=2.0.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.3 +version: 2.1.3+1 dependencies: angel_framework: ^2.0.0-rc.0 convert: ^2.0.0 From e3c291d913571b4e97feee98a4f0592877610806 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Tue, 25 Jun 2019 12:59:00 -0700 Subject: [PATCH 65/67] Prepare for upcoming change to File.openRead() An upcoming change to the Dart SDK will change the signature of `File.openRead()` from returning `Stream>` to returning `Stream`. This forwards-compatible change prepares for that SDK breaking change by casting the Stream to `List` before transforming it. https://github.com/dart-lang/sdk/issues/36900 --- lib/src/virtual_directory.dart | 11 ++++++++--- pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/src/virtual_directory.dart b/lib/src/virtual_directory.dart index ff2ed4e5..c36a5659 100644 --- a/lib/src/virtual_directory.dart +++ b/lib/src/virtual_directory.dart @@ -1,4 +1,5 @@ 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'; @@ -284,7 +285,7 @@ class VirtualDirectory { statusCode: 416, message: '`Range` header may not be empty.'); } else if (header.items.length == 1) { var item = header.items[0]; - Stream> stream; + Stream stream; int len = 0, total = totalFileSize; if (item.start == -1) { @@ -311,7 +312,7 @@ class VirtualDirectory { res.statusCode = 206; res.headers['content-length'] = len.toString(); res.headers['content-range'] = 'bytes ' + item.toContentRange(total); - await stream.pipe(res); + await stream.cast>().pipe(res); return false; } else { var transformer = RangeHeaderTransformer( @@ -324,7 +325,11 @@ class VirtualDirectory { transformer.computeContentLength(totalFileSize).toString(); res.contentType = MediaType( 'multipart', 'byteranges', {'boundary': transformer.boundary}); - await file.openRead().transform(transformer).pipe(res); + await file + .openRead() + .cast>() + .transform(transformer) + .pipe(res); return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index f18bdd8d..33d4a02e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=2.0.0 <3.0.0" homepage: https://github.com/angel-dart/static author: Tobe O -version: 2.1.3+1 +version: 2.1.3+2 dependencies: angel_framework: ^2.0.0-rc.0 convert: ^2.0.0 From 02534d17e86d5c17e3180631a9250dcf4731f56c Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 20:24:09 -0400 Subject: [PATCH 66/67] changelog --- .CHANGELOG.md.swp | Bin 0 -> 12288 bytes CHANGELOG.md | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .CHANGELOG.md.swp diff --git a/.CHANGELOG.md.swp b/.CHANGELOG.md.swp new file mode 100644 index 0000000000000000000000000000000000000000..41f697df237925d16fb731d69fe33ce22062b5ee GIT binary patch literal 12288 zcmeHNJ!~9B6y8Ka;V*zfNs0+ZB8h!__SsgV5F#$I9UH}o@lPT^5j(d#cQ^6g&N4IW zvnik=H6W-^=t7f*5~x8`&{6>v;in)Sf&z)}&F=Xu8%02>NORJsy&KQGdGCAQdvn%J zYy9%UB0CvQ1b96W1lPVFx%$hk1N;AaF9>W`8NS{Z^rySMR#i)w+9Hq7Dz#o~N*$&u z=INd#Zw?IBS5%>6QEz4)k=BjnR>t*n^UaIrXD?hjANG3CqJ9#je zY)p)knUSZDuqO|#-HS!P)-&K4@Cun;2H1?cm_NJo&nE*XTUSy8So6;hYWE1*8a@{L2&2*oX79~ga7}3 ze>@1j1ik=12R;IB03QI?fi^G(i~>i1+m8jokHB}p$G}bC252e*IbagF^ALOmP64+c#CzZ-@FDOD@G|fca2WUj{A>b!KmiPJoZW{x z_bz(|JOiEq&%i%15amTHmGKg{-6#y^B- z5sfk16|AL_Eo_jcOl2_F60n$A)_FtbIqb@|o2A9;g)o+h%^Y4!i=-sT0ow~9qPr**i2r?R?=t+2Jyr7CvAy4L3ARK4Cos6{J`RjP03l=rwSmd@go5d;t9}(+w!!l*rHfKDc znF&1Gwls>XiOa&MbORy0I?`xdJ$iSSs{4eMPmRVgqG3skTC0!Sn= zZX4QZg5Q+)NTshKe!W72M*iDA*Gr1+a%wr}s>;R6$MJ#qI8c zE~Q0BW~c!wIGGs>4oKa~ApfATi}6$}m$HvoPbT=-=UUJ%%c7^K=-M(B%xa#Qw$eSJ zO)00zaJ#sI6Eq-r+JU%S@0su@(i`mKudMJ2aiTwl3$pnxEtYkh})9K*}T&n zcAjm{M67DLgOrdc^H7>A*bTyEaaY9aM1#mofwpJ6939S`5s7l`PMDbIrN$!Tu0lt{ zj1Oh(q6Kjg^@cep8Ud6}ImwhQL1r5~l?j_)UA({`_MxF2(kWLt8LWZ9#hGb0RajKO z*v=er))#!;l?M5-ki}^MvP_ZZ3JD|ol-y{Uj(}l*BKs31Rcp%&wW@~7m{C}J)Eu(a zE-B2l9Pl42O^ueVEnj#R4)L_lx121bf2`BsL%=2yuJB=oQMZ|Oy$M6<1mf7vN-M55tJXV?2DrIdP v?7JqFj^TLA(E?q9iX3M&9VIbZWd|}-I6YDk;%iB^jAnZDSVhd_Xe0O=6QoSS literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md index a6da3302..be8e4015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.1.3+2 +* Prepare for upcoming change to File.openRead() + # 2.1.3+1 * Apply control flow lints. @@ -68,4 +71,4 @@ Fixes https://github.com/angel-dart/angel/issues/44. # 1.2.3 Fixed #40 and #41, which dealt with paths being improperly served when using a -`publicPath`. \ No newline at end of file +`publicPath`. From 6ec67dde7bd318c5e82fce66e3b22dfa3a378c24 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 8 Jul 2019 20:24:25 -0400 Subject: [PATCH 67/67] delete swp --- .CHANGELOG.md.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .CHANGELOG.md.swp diff --git a/.CHANGELOG.md.swp b/.CHANGELOG.md.swp deleted file mode 100644 index 41f697df237925d16fb731d69fe33ce22062b5ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeHNJ!~9B6y8Ka;V*zfNs0+ZB8h!__SsgV5F#$I9UH}o@lPT^5j(d#cQ^6g&N4IW zvnik=H6W-^=t7f*5~x8`&{6>v;in)Sf&z)}&F=Xu8%02>NORJsy&KQGdGCAQdvn%J zYy9%UB0CvQ1b96W1lPVFx%$hk1N;AaF9>W`8NS{Z^rySMR#i)w+9Hq7Dz#o~N*$&u z=INd#Zw?IBS5%>6QEz4)k=BjnR>t*n^UaIrXD?hjANG3CqJ9#je zY)p)knUSZDuqO|#-HS!P)-&K4@Cun;2H1?cm_NJo&nE*XTUSy8So6;hYWE1*8a@{L2&2*oX79~ga7}3 ze>@1j1ik=12R;IB03QI?fi^G(i~>i1+m8jokHB}p$G}bC252e*IbagF^ALOmP64+c#CzZ-@FDOD@G|fca2WUj{A>b!KmiPJoZW{x z_bz(|JOiEq&%i%15amTHmGKg{-6#y^B- z5sfk16|AL_Eo_jcOl2_F60n$A)_FtbIqb@|o2A9;g)o+h%^Y4!i=-sT0ow~9qPr**i2r?R?=t+2Jyr7CvAy4L3ARK4Cos6{J`RjP03l=rwSmd@go5d;t9}(+w!!l*rHfKDc znF&1Gwls>XiOa&MbORy0I?`xdJ$iSSs{4eMPmRVgqG3skTC0!Sn= zZX4QZg5Q+)NTshKe!W72M*iDA*Gr1+a%wr}s>;R6$MJ#qI8c zE~Q0BW~c!wIGGs>4oKa~ApfATi}6$}m$HvoPbT=-=UUJ%%c7^K=-M(B%xa#Qw$eSJ zO)00zaJ#sI6Eq-r+JU%S@0su@(i`mKudMJ2aiTwl3$pnxEtYkh})9K*}T&n zcAjm{M67DLgOrdc^H7>A*bTyEaaY9aM1#mofwpJ6939S`5s7l`PMDbIrN$!Tu0lt{ zj1Oh(q6Kjg^@cep8Ud6}ImwhQL1r5~l?j_)UA({`_MxF2(kWLt8LWZ9#hGb0RajKO z*v=er))#!;l?M5-ki}^MvP_ZZ3JD|ol-y{Uj(}l*BKs31Rcp%&wW@~7m{C}J)Eu(a zE-B2l9Pl42O^ueVEnj#R4)L_lx121bf2`BsL%=2yuJB=oQMZ|Oy$M6<1mf7vN-M55tJXV?2DrIdP v?7JqFj^TLA(E?q9iX3M&9VIbZWd|}-I6YDk;%iB^jAnZDSVhd_Xe0O=6QoSS