Skip to content
This repository has been archived by the owner on Mar 18, 2021. It is now read-only.

How to serve "static" files? #157

Closed
woopla opened this issue Dec 11, 2016 · 12 comments
Closed

How to serve "static" files? #157

woopla opened this issue Dec 11, 2016 · 12 comments

Comments

@woopla
Copy link

woopla commented Dec 11, 2016

Hi,

I'm trying to write a full stack Dart app, using Aqueduct on the back-end side and Angular Dart on the front-end.

Problem is, how do I serve the files intended for the client using Aqueduct? I.e. everything that's typically in the build/web dir (and indirectly build/lib) when running pub build.

Thanks,

Clément

@joeconwaystk
Copy link
Member

var html = new File("web/index.html").readAsStringSync();
return new Response.ok(html)..contentType = ContentType.HTML;

@zoechi
Copy link

zoechi commented Dec 11, 2016

I'm sure you also can forward requests to certain routes to https://pub.dartlang.org/packages/shelf_static

@woopla
Copy link
Author

woopla commented Dec 12, 2016

Not so fast @joeconwaystk 😸 Based on the two lines from your reply, I made something more generic:

import 'package:path/path.dart' as path;
import 'dart:io' as io;
import 'package:mime/mime.dart' as mime;

class StaticFilesController extends HTTPController {
  final String basePath;
  final String defaultFile;

  StaticFilesController({this.basePath: "build/web", this.defaultFile: "index.html"});

  // Initially based on https://github.com/stablekernel/aqueduct/issues/157
  @httpGet getFile() async {
    String realPath = request.path.remainingPath;
    if (realPath.length == 0) {
      realPath = defaultFile;
    }
    String filePath = path.join(basePath, realPath);
    io.File file = new io.File(filePath);
    var fileContents = file.readAsStringSync();

    String contentType = mime.lookupMimeType(filePath);

    return new Response.ok(fileContents)
      ..contentType = io.ContentType.parse(contentType);
  }
}

I'm sending a complete AngularDart app, so just index.html won't cut it. Because Response encodes the body according to the Response Type, for each new file type that I have, I have to include an encoder. For now, I only needed to add application/javascript.

The problem is one of scalability: I don't have images, fonts, etc. in this app for now, but I don't want to have to keep adding encoders as I go. Would it be possible to add a raw option to Response, so that no encoding attempt is made on the body? This would also avoid a v.toString() on file data already stored in String variables.

Maybe it also makes sense to include this controller in the framework? This is a rather generic thing, not application-specific.

What's your take on this?

Thanks,

Clément

@joeconwaystk
Copy link
Member

@woopla That's a good looking piece of code.

I'll reopen this because the real issue sounds like the out of the box behavior for Encoders is cumbersome. I think there can be some more intelligent behavior there. I'm just not sure what it is yet. The default behavior for the underlying HttpResponse object may work, and the encoder should just drop the toString and it's up to the developer to make sure the type they're passing works.

w.r.t to adding the static file controller, there's three things to consider. First, there are more ways to store an HTML asset than on the filesystem (e.g. a Redis instance). So, the controller would have to be decoupled from the storage of the assets. Then, it'd likely someone would want to post-process the HTML asset (e.g., some sort of templating). So there would have to be some opportunity to do that. Finally, there would also need to be some sort of in-memory caching that the storage mechanism uses to avoid reading from disk each time; but that may be solved by the decoupling in the first point.

@joeconwaystk joeconwaystk reopened this Dec 12, 2016
@woopla
Copy link
Author

woopla commented Dec 12, 2016

@joeconwaystk there are some smarts in Shelf to handle chunked encoding (see https://github.com/dart-lang/shelf/blob/master/lib/shelf_io.dart#L137), but apart from that, it seems to rely on HttpResponse to do the right thing. It uses dart:io's File.openRead() to return a Stream and doesn't care if it's a string or bytes. I'm gonna try using readAsBytesSync() and dropping the toString() inside the Aqueduct code.

For the static file controller, I had not thought about this decoupling aspect. Let me try and see what I can do... Probably not before the end of the week, though.

@zidenis
Copy link

zidenis commented Dec 12, 2016

Since using Aqueduct + AngularDart should be a common case, a canonical HTTPController that serves static files would be a good addition to the template aqueduct create -n or documentation.

@joeconwaystk
Copy link
Member

If it helps, the next template version will have the following to support authorization code flow login page: https://github.com/stablekernel/aqueduct/blob/jc/oauth/example/templates/default/lib/utility/html_template.dart

@joeconwaystk
Copy link
Member

@woopla will check those out

@woopla
Copy link
Author

woopla commented Dec 19, 2016

So I tried the following:

  1. Modified one line in StaticFilesController.getFile() above to var fileContents = file.readAsBytesSync();
  2. Added new encoders just after adding the route for static files in the RequestSink.setupRouter() override:
    // Catch-all for static files
    router
      .route("/*")
      .generate(() => new StaticFilesController());
    Response.addEncoder(io.ContentType.parse("application/javascript"), (j) => j);
    Response.addEncoder(io.ContentType.parse("text/*"), (j) => j);

And it doesn't work :( I'm getting arrays of bytes shown as strings in the browser. This kinda works:

    // Catch-all for static files
    router
      .route("/*")
      .generate(() => new StaticFilesController());
    Response.addEncoder(io.ContentType.parse("application/javascript"), (j) => UTF8.decode(j));
    Response.addEncoder(io.ContentType.parse("text/*"), (j) => UTF8.decode(j));
    Response.addEncoder(io.ContentType.parse("image/*"), (List<int> j) => new List<int>.from(j));

Except for the last part - binary streams are still transformed to strings further down the line (my favicon.ico is not sent as binary data). I also tried the simpler (j) => j for the images, but it also doesn't work.

I have a (simple project)[https://github.com/woopla/angueduct] that I use to learn Dart & Angular, it contains the code mentioned here.

Any ideas? I mean, without having to modify the Aqueduct code itself to remove the encoders part.

Thanks,

Clément

@joeconwaystk
Copy link
Member

Looks like the body is written using the standard library HttpResponse.write, which automatically does a toString and does some interpretation of the content-type and charset. HttpResponse.add is the generic version that assumes the bytes have been encoded prior to. I think there does need to be a change in the Aqueduct code, but I'd need to spend some time researching it/take some suggestions on that.

@joeconwaystk
Copy link
Member

Okay, this is now in 2.2. See the following documentation:

https://aqueduct.io/docs/http/request_and_response/#response-objects-and-http-body-encoding
https://aqueduct.io/docs/http/serving_files/

@chrisnorman7
Copy link

Hi,
I've trying to create an Aqueduct project myself, and I want to serve a directory of sound files. This directory will change over time obviously, and I don't want to route all subdirectories by hand. Is there some quick way to just route it? I'm thinking:

router.route('/sounds/*').link(() => FileController('sounds', recursive: true));

Cheers,

Chris

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants