Skip to content

Commit

Permalink
Merge pull request #43 from PyreStudios/brad/42
Browse files Browse the repository at this point in the history
Start on static file access
  • Loading branch information
bradcypert committed Mar 15, 2023
2 parents 1f1aba9 + 453e2be commit c3d5133
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 77 deletions.
19 changes: 19 additions & 0 deletions doc/site/docs/router/static_assets.md
@@ -0,0 +1,19 @@
---
sidebar_position: 5
---

# Static Assets

Steward also offers static asset handlers for serving files that do not change based on server requests. Commonly, image files, html pages, and more are served as static assets. If you are using view templating, those templates are _not_ static assets and should not be served this way.

```dart
final router = Router();
router.staticFiles('/static');
```

Additionally, you can use middleware with static file handling.

```dart
final router = Router();
router.staticFiles('/private-assets', middleware: [RequireAuthMiddleware]);
```
13 changes: 13 additions & 0 deletions example/assets/index.html
@@ -0,0 +1,13 @@
<!doctype html>

<html lang="en">
<head>
<meta charset="utf-8">

<title>A random asset</title>
</head>

<body>
<h1>Here's an asset</h1>
</body>
</html>
4 changes: 4 additions & 0 deletions example/config.yml
@@ -0,0 +1,4 @@
---
app:
name: My Steward App
port: 4040
2 changes: 2 additions & 0 deletions example/main.dart
Expand Up @@ -12,6 +12,8 @@ class SimpleController extends Controller {

Future main() async {
var router = Router();

router.staticFiles('/assets');
router.get('/hello', (Request request) async {
return Response.Ok('Hello World!');
});
Expand Down
24 changes: 19 additions & 5 deletions lib/router/router.dart
Expand Up @@ -4,12 +4,14 @@ import 'dart:mirrors';
import 'package:steward/controllers/controller.dart';
import 'package:steward/controllers/route_utils.dart';
import 'package:path_to_regexp/path_to_regexp.dart';
import 'package:steward/router/static_binding.dart';
import 'package:steward/steward.dart';

export 'package:steward/router/response.dart';
export 'package:steward/router/request.dart';

enum HttpVerb { Connect, Delete, Get, Head, Options, Patch, Post, Put, Trace }

typedef RequestCallback = Future<Response> Function(Request request);

abstract class Processable {
Expand All @@ -21,6 +23,8 @@ abstract class RouteBinding implements Processable {
late String path;
List<MiddlewareFunc> middleware = [];

bool get isPrefixBinding;

RouteBinding(
{required this.verb, required this.path, this.middleware = const []});
}
Expand All @@ -39,6 +43,9 @@ class _FunctionBinding extends RouteBinding {
Future<Response> process(Request request) {
return callback(request);
}

@override
bool get isPrefixBinding => false;
}

class Router {
Expand Down Expand Up @@ -94,6 +101,9 @@ class Router {
{List<MiddlewareFunc> middleware = const []}) =>
_addBinding(path, HttpVerb.Put, handler, middleware: middleware);

void staticFiles(String path, {List<MiddlewareFunc> middleware = const []}) =>
bindings.add(StaticBinding(path: path, middleware: middleware));

void trace(String path, RequestCallback handler,
{List<MiddlewareFunc> middleware = const []}) =>
_addBinding(path, HttpVerb.Trace, handler, middleware: middleware);
Expand Down Expand Up @@ -131,18 +141,22 @@ class Router {
for (var i = 0; i < bindings.length; i++) {
var params = <String>[];

// Get the root pattern from the pathToRegex call
var rootPattern =
pathToRegExp(bindings[i].path, parameters: params).pattern;
// Get the root pattern from the pathToRegex call
var rootPattern = pathToRegExp(bindings[i].path,
parameters: params, prefix: bindings[i].isPrefixBinding)
.pattern;

// TODO: Circumvent this for prefix bindings I guess?

// Build a new regex by removing the $, adding in the optional trailing slash
// and then adding the end terminator back on ($).
var cleanedPattern = rootPattern.substring(0, rootPattern.length - 1);
var cleanedPattern = rootPattern.substring(0, rootPattern.lastIndexOf('\$'));
// account for the path already ending in slash
if (cleanedPattern.endsWith('/')) {
cleanedPattern =
cleanedPattern.substring(0, cleanedPattern.length - 1);
}
var regex = RegExp('$cleanedPattern\\/?\$', caseSensitive: false);
var regex = RegExp('$cleanedPattern\\/?${bindings[i].isPrefixBinding ? '\$)' :'\$'}', caseSensitive: false);
hasMatch = regex.hasMatch(request.uri.path);

if (hasMatch) {
Expand Down
54 changes: 54 additions & 0 deletions lib/router/static_binding.dart
@@ -0,0 +1,54 @@
import 'dart:io';

import 'package:steward/router/router.dart';

class StaticBinding extends RouteBinding {
StaticBinding({required super.path, super.middleware})
: super(verb: HttpVerb.Get);

static final imageExtensions = ['png', 'jpeg', 'jpg', 'webp', 'gif'];
static final textExtensions = ['css', 'csv', 'html', 'xml'];

@override
Future<Response> process(Request request) async {
var assetPath = '$path${request.request.uri.path.split(path).last}';
var assetType = 'text/html';

try {
final extension = assetPath.split('.').last;
if (imageExtensions.contains(extension)) {
assetType = 'image/$extension';
}
if (textExtensions.contains(extension)) {
assetType = 'text/$extension';
} else {
// TODO: this is probably to vague
assetType = 'application/$extension';
}
} on StateError {
// if there is no extension, assume text/html.
assetType = 'text/html';
}

if (assetType == 'text/html' && !assetPath.contains('.')) {
assetPath = '$assetPath.html';
}

try {
final asset = File('./$assetPath');
final contents = await asset.readAsString();
final response = Response(200, body: contents);
final splitAssetType = assetType.split('/');
final primaryType = splitAssetType.first;
final subType = splitAssetType.last;
response.headers.contentType = ContentType(primaryType, subType);

return response;
} catch (e) {
return Response.NotFound();
}
}

@override
bool get isPrefixBinding => true;
}

0 comments on commit c3d5133

Please sign in to comment.