rocket_assets_fairing/
lib.rs

1//! Easily serve static assets with configurable cache policy from Rocket.
2//!
3//! This create adds a fairing and responder for serving static assets. You should configure an
4//! assets directory, attach the fairing, and then return an `Asset` on whatever route you want.
5//!
6//! # Usage
7//!
8//!   1. Add your assets to the configurable `assets_dir` directory (default: `{rocket_root}/assets`).
9//!   2. Optionally configure the cache policy using `assets_max_age`
10//!   2. Attach [`Assets::fairing()`] and return an [`Asset`] using [`Assets::open()`] (specifying
11//!      the relative file path):
12//! ```rust
13//! use assets_rocket_fairing::{Asset, Assets};
14//! 
15//! #[rocket::main]
16//! async fn main() {
17//!    rocket::build()
18//!        .attach(Assets::fairing())
19//!        .mount("/assets", routes![style])
20//!        .launch()
21//!        .await;
22//! }
23//! 
24//! #[get("/style.css")]
25//! async fn style(assets: &Assets) -> Option<Asset> {
26//!    assets.open("style.css").await.ok()
27//! }
28//! ```
29use normpath::PathExt;
30use rocket::{
31    error,
32    fairing::{self, Fairing, Info, Kind},
33    fs::NamedFile,
34    info, info_,
35    outcome::IntoOutcome,
36    request::{self, FromRequest, Request},
37    response::{self, Responder, Response},
38    Build, Orbit, Rocket,
39};
40use std::io;
41use std::path::{Path, PathBuf};
42
43/// The asset collection located in the configured folder
44pub struct Assets {
45    path: PathBuf,
46    cache_max_age: i32,
47}
48
49impl Assets {
50    /// Returns the fairing to be attached
51    pub fn fairing() -> impl Fairing {
52        AssetsFairing
53    }
54    /// Opens up a named asset file, returning an [`Asset`]
55    pub async fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<Asset> {
56        let mut asset_path = self.path.clone();
57        asset_path.push(path);
58        let file = NamedFile::open(Path::new(&asset_path)).await?;
59        let cache_max_age = self.cache_max_age;
60        Ok(Asset {
61            file,
62            cache_max_age,
63        })
64    }
65}
66#[rocket::async_trait]
67impl<'r> FromRequest<'r> for &'r Assets {
68    type Error = ();
69    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
70        req.rocket().state::<Assets>().or_forward(())
71    }
72}
73
74/// An asset that can be returned from a route
75pub struct Asset {
76    file: NamedFile,
77    cache_max_age: i32,
78}
79impl<'r> Responder<'r, 'static> for Asset {
80    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
81        let cache_control = format!("max-age={}", self.cache_max_age);
82        Response::build_from(self.file.respond_to(req)?)
83            .raw_header("Cache-control", cache_control)
84            .ok()
85    }
86}
87
88struct AssetsFairing;
89#[rocket::async_trait]
90impl Fairing for AssetsFairing {
91    fn info(&self) -> Info {
92        let kind = Kind::Response | Kind::Ignite | Kind::Liftoff;
93        Info {
94            kind,
95            name: "Static Assets",
96        }
97    }
98
99    async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
100        use rocket::figment::value::magic::RelativePathBuf;
101
102        let configured_dir = rocket
103            .figment()
104            .extract_inner::<RelativePathBuf>("assets_dir")
105            .map(|path| path.relative());
106
107        let relative_path = match configured_dir {
108            Ok(dir) => dir,
109            Err(e) if e.missing() => "assets/".into(),
110            Err(e) => {
111                rocket::config::pretty_print_error(e);
112                return Err(rocket);
113            }
114        };
115
116        let path = match relative_path.normalize() {
117            Ok(path) => path.into_path_buf(),
118            Err(e) => {
119                error!(
120                    "Invalid assets directory '{}': {}.",
121                    relative_path.display(),
122                    e
123                );
124                return Err(rocket);
125            }
126        };
127
128        let cache_max_age = rocket
129            .figment()
130            .extract_inner::<i32>("assets_max_age")
131            .unwrap_or(86400);
132
133        Ok(rocket.manage(Assets {
134            path,
135            cache_max_age,
136        }))
137    }
138
139    async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
140        use rocket::{figment::Source, log::PaintExt, yansi::Paint};
141
142        let state = rocket
143            .state::<Assets>()
144            .expect("Template AssetsContext registered in on_ignite");
145
146        info!("{}{}:", Paint::emoji("📐 "), Paint::magenta("Assets"));
147        info_!("directory: {}", Paint::white(Source::from(&*state.path)));
148        info_!("cache max age: {}", Paint::white(state.cache_max_age));
149    }
150}