Rocket: Static file serving

Created on 22 Mar 2017  路  24Comments  路  Source: SergioBenitez/Rocket

First off: let me just say that Rocket is very nice and has been a pleasure to use, kudos!

I looked around the docs and couldn't find any support for serving a set of static files from a directory. This is something that I've seen supported in every web framework I've ever used. As an arbitrary example, here's how it works in Flask. This is a pretty common pattern to serve things like CSS files and JS scripts, so I think it would be useful to have. Obviously one can serve individual files by just having a handler that returns a File, but this seems like generally useful functionality. Ideally one would simply be able to do something like rocket::ignite().mount("/static", StaticFiles(some_path).

question request

Most helpful comment

@luser Perhaps something in contrib that you can simply mount. So it would look something like:

use rocket_contrib::StaticFiles;

rocket::ignite().mount("/path_to_serve_on", StaticFiles::from("/path_to_serve_from"));

This would presumably serve files from the /path_to_serve_from directory at the path /path_to_serve_on.

I think something like this would be nice, though I'm not sure if it's actually possible to do this in a clean way at the moment. I'll look into it.

All 24 comments

I am not sure if this helps.. Have you seen the the static_files example ?

Indeed, this is covered in the static_files example.

I had not seen that, thanks! I did eventually find the bit about segments parameters which has that same code as an example, but I still think a helper that simplified this extremely common use case would be nice. It took me a while to find that part of the documentation because I was looking around for "static file serving" and I did not expect to find it under "Dynamic Segments". :)

@luser Perhaps something in contrib that you can simply mount. So it would look something like:

use rocket_contrib::StaticFiles;

rocket::ignite().mount("/path_to_serve_on", StaticFiles::from("/path_to_serve_from"));

This would presumably serve files from the /path_to_serve_from directory at the path /path_to_serve_on.

I think something like this would be nice, though I'm not sure if it's actually possible to do this in a clean way at the moment. I'll look into it.

That sounds great!

I think the way to do it right now is sufficiently simple, just showing how in the guide under "Serving Static Files" or something alike would suffice.

I think it would be best to offer something that supports HTTP caching.

I have a question I am using bootstrap only (/css/bootstrap.min.css, /css/bootstrap-theme.min.css)
It shows errors in console (GET /css/bootstrap-theme.min.css.map:) ??
any reason about this =?
captura de pantalla 2017-04-14 a las 11 21 45

@marti1125 Your browser is sending a request for the source map files (*.map) but they don't exist in the directory. Presumably you have developer tools open.

@SergioBenitez yep it is =D

Without actually running it, it looks like the static files example would be vulnerable to a path traversal attack. Is there anything preventing this?

@quadrupleslap See the docs here and here.

Since the provided example was not enough for, since it didn't even support the Last-Modified header, I wrote my own Fairing for rocket.

Maybe we could build upon this, stabilize it somewhere in the future and get it in contrib then.

https://crates.io/crates/rocket_static_fs

Sadly docs.rs doesn't build the docs cause of an outdated rustc version. But basically the example from the docs shares your apps src folder under /src/:

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_static_fs;

#[get("/")]
fn index() -> &'static str {
    "Hellos, world!"
}

fn main() {
    rocket::ignite()
        .attach(rocket_static_fs::StaticFileServer::new("src", "/src/").unwrap())
        .mount("/", routes![index])
        .launch();
}

I just remembered that tilpner/includedir is another solution to this, if you don't mind pulling your entire website into your executable.

Edit: it's definitely not a comprehensive solution, but it's an interesting place to start.

@quadrupleslap Including everything is the binary is cool, but this also doesn't handle things like Not-Modified. My small project is aimed to provide a pretty solid solution for static file serving with different backends and support for caching, ranges, compression, directory listing. It's not there yet, but since I need these features, it won't be long until it has these properly implemented.

@SergioBenitez Is it still planned to allow paths starting with a single dot as described in https://github.com/SergioBenitez/Rocket/issues/560#issuecomment-364695939 ?

The plan in my previous comment covers your use-case as well by relaxing the constraints on PathBuf. I'll close this out and track in #239.

Doing StaticFiles::from("/path_to_serve_from").allow_hidden() would not work for my use case:

#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate rocket_contrib;

use std::path::{Path, PathBuf};
use std::fs;
use rocket::response::{NamedFile, Responder};
use rocket::response::content::Html;
use rocket::{Request, Response};
use rocket::http::Status;
use rocket::config::{Config, Environment};

fn list_dir<P: AsRef<Path>>(dir: P) -> String {
    let dir = dir.as_ref();
    let rows = fs::read_dir(&dir).ok().map_or_else(|| "error reading dir".to_string(), |x| x.filter_map(|e| e.ok()).map(|entry| {
        let abs_path = entry.path();
        let rel_path = abs_path.strip_prefix(dir).unwrap();
        let path_str = rel_path.to_string_lossy();
        let url = path_str.to_string() + if abs_path.is_dir() { "/" } else { "" };
        format!(r#"<li><a href="{}">{}</a>"#, url, url)
    }).collect::<String>());
    let dir = dir.to_string_lossy();
    let dir = if dir == "." { "/".into() } else { dir.replace('\\', "/") };
    format!(r#"
        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
        <title>Directory listing for {}</title>
        <body>
        <h2>Directory listing for {}</h2>
        <hr>
        <ul>
        {}
        </ul>
        <hr>
        </body>
        </html>
    "#, dir, dir, rows)
}

#[get("/")]
fn index() -> Html<String> {
    Html(list_dir("."))
}

enum PathResp { File(PathBuf), Dir(PathBuf) }

impl Responder<'static> for PathResp {
    fn respond_to(self, req: &Request) -> Result<Response<'static>, Status> {
        match self {
            PathResp::File(path) => NamedFile::open(path).ok().respond_to(req),
            PathResp::Dir(path) => Html(list_dir(&path)).respond_to(req)
        }
    }
}

#[get("/<path..>")]
fn path(path: PathBuf) -> PathResp {
    if path.is_dir() { PathResp::Dir(path) } else { PathResp::File(path) }
}

fn main() {
    let config = Config::build(Environment::Staging)
        .address("0.0.0.0").port(8000).finalize().unwrap();
    rocket::custom(config, true).mount("/", routes![index, path]).launch();
}

(The UnsafePBuf workaround would work but I'd prefer if the FromSegments impl for PathBuf allowed paths starting with a single dot.)

@Boscop In case you just need a simplistic, yet functional static file server for rocket, you may want to take a look at https://crates.io/crates/rocket_static_fs

@SergioBenitez I tried to use the static_files example to setup a static site while supporting other api endpoints as well. However I get this collisions error when loading it up with cargo run:

  Mounting '/':
    => GET /
    => GET /<file..>
    => GET /api/hello
    => GET /api/test
    => POST /api/upload
    => GET /api/upload/<id>
Error: GET /api/upload/<id> and GET /<file..> collide!
Error: Rocket failed to launch due to routing collisions.

So basically I have setup the static serving on / like

#[get("/")]
fn index() -> io::Result<NamedFile> {
    NamedFile::open("dist/index.html")
}

// allow html to reference any file with path /static under folder "static"
#[get("/<file..>")]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("dist/").join(file)).ok()
}

Then added some api endpoint like

#[get("/api/upload/<id>")]
fn retrieve(id: PasteID) -> Option<File> {
    let filename = format!("tmp/{id}", id = id);
    File::open(&filename).ok()
}

BTW, the static html and js files are build/generated by a vue project, which as a single index.html and it reference all the built js and css directly via /js/.... or /css/..... So if I mount the static file endpoint not on root / then the js frontend app won't work normally.

How can I solve this? 馃 Thank you.

Oh I just looked carefully at https://github.com/SergioBenitez/Rocket/issues/723 again it seems I can add rank to solve it. #[get("/<file..>", rank = 10)]

Is this the recommended way?

With ec130f9, you can now do the following:

use rocket_contrib::static_files::StaticFiles;

fn main() {
    rocket::ignite()
        .mount("/path_to_serve_on", StaticFiles::from("/path_to_serve_from"))
        .launch();
}

This sound awesome, will give a try :D Thank you @SergioBenitez 馃挴

@SergioBenitez Hey Sergio again, if I would like to make some user authentication, so that for example if a user has not logged in and want to visit /, then the user will be redirected to some login page (so that all the static files cannot be accessed), I cannot use your recommended way above, right?

I guess I will have to use something like:

#[get("/")]
fn index(_user: User) -> io::Result<NamedFile> {
    NamedFile::open("dist/index.html")
}

// allow html to reference any file with path /static under folder "static"
#[get("/<file..>", rank = 10)]   // use rank here to allow other api endpoint available as well
fn files(file: PathBuf, _user: User) -> Option<NamedFile> {
    NamedFile::open(Path::new("dist/").join(file)).ok()
}

with User setup like in this session example: https://github.com/SergioBenitez/Rocket/blob/master/examples/session/src/main.rs

Does my thoughts make any sense to you? Thanks in advance :)

@liufuyang I'm not sure I've understood what you're saying. I _think_ you're saying that you want:

  • Authorized users to be able to access static files.
  • Unauthorized users to be denied access to static files.

If that's the case, then, just as would be the case in any other web framework with a pluggable static-files server, you're likely better off handling this yourself. In Rocket, the way you've illustrated looks about right.

@SergioBenitez Thank you.
Basically I am using a single Rocket server to host a group of API endpoints and a set of static html plus js code. And those html + js code will make a UI on the client side that does request on the APIs my Rocket server is hosting. Now I need to add some authentication mechanism with oauth2. So that's why I am asking about it :)

Was this page helpful?
0 / 5 - 0 ratings