diff --git a/Cargo.lock b/Cargo.lock index 04d44e9..6ea4004 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,29 +19,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "actix-files" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" -dependencies = [ - "actix-http", - "actix-service", - "actix-utils", - "actix-web", - "bitflags", - "bytes", - "derive_more 0.99.18", - "futures-core", - "http-range", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "v_htmlescape", -] - [[package]] name = "actix-http" version = "3.9.0" @@ -1483,12 +1460,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" - [[package]] name = "httparse" version = "1.9.5" @@ -2486,6 +2457,40 @@ dependencies = [ "syn", ] +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2605,12 +2610,15 @@ dependencies = [ name = "rustical_frontend" version = "0.1.0" dependencies = [ - "actix-files", "actix-session", "actix-web", "anyhow", "askama", "askama_actix", + "futures-core", + "hex", + "mime_guess", + "rust-embed", "rustical_store", "serde", "thiserror", @@ -2679,6 +2687,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3573,12 +3590,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "v_htmlescape" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" - [[package]] name = "valuable" version = "0.1.0" @@ -3597,6 +3608,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3709,6 +3730,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index a41bd6e..618552c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,10 @@ quick-xml = { version = "0.37", features = [ "serde-types", "serialize", ] } +rust-embed = "8.5" +futures-core = "0.3.31" +hex = "0.4.3" +mime_guess = "2.0.5" itertools = "0.13" log = "0.4" strum = { version = "0.26", features = ["strum_macros", "derive"] } diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 69c974b..d960e5d 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -16,4 +16,7 @@ thiserror = { workspace = true } tokio = { workspace = true } actix-web = { workspace = true } rustical_store = { workspace = true } -actix-files = "0.6" +rust-embed.workspace = true +futures-core.workspace = true +hex.workspace = true +mime_guess.workspace = true diff --git a/crates/frontend/src/assets.rs b/crates/frontend/src/assets.rs new file mode 100644 index 0000000..8e7ced4 --- /dev/null +++ b/crates/frontend/src/assets.rs @@ -0,0 +1,113 @@ +use std::marker::PhantomData; + +use actix_web::{ + body::BoxBody, + dev::{ + HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse, + }, + http::{header, Method}, + HttpResponse, +}; +use futures_core::future::LocalBoxFuture; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "frontend/dist/assets"] +pub struct Assets; + +pub struct EmbedService +where + E: 'static + RustEmbed, +{ + _embed: PhantomData, + prefix: String, +} + +impl EmbedService +where + E: 'static + RustEmbed, +{ + pub fn new(prefix: String) -> Self { + Self { + prefix, + _embed: PhantomData, + } + } +} + +impl HttpServiceFactory for EmbedService +where + E: 'static + RustEmbed, +{ + fn register(self, config: &mut actix_web::dev::AppService) { + let resource_def = if config.is_root() { + ResourceDef::root_prefix(&self.prefix) + } else { + ResourceDef::prefix(&self.prefix) + }; + config.register_service(resource_def, None, self, None); + } +} + +impl ServiceFactory for EmbedService +where + E: 'static + RustEmbed, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Config = (); + type Service = EmbedService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let prefix = self.prefix.clone(); + Box::pin(async move { + Ok(Self { + prefix, + _embed: PhantomData, + }) + }) + } +} + +impl Service for EmbedService +where + E: 'static + RustEmbed, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + actix_web::dev::always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + Box::pin(async move { + if req.method() != Method::GET && req.method() != Method::HEAD { + return Ok(req.into_response(HttpResponse::MethodNotAllowed())); + } + let path = req.match_info().unprocessed().trim_start_matches('/'); + + match E::get(path) { + Some(file) => { + let data = file.data; + let hash = hex::encode(file.metadata.sha256_hash()); + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + let body = if req.method() == Method::HEAD { + Default::default() + } else { + data + }; + Ok(req.into_response( + HttpResponse::Ok() + .content_type(mime) + .insert_header((header::ETAG, hash)) + .body(body), + )) + } + None => Ok(req.into_response(HttpResponse::NotFound())), + } + }) + } +} diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 9834e15..7ac39a7 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -6,6 +6,7 @@ use actix_web::{ Responder, }; use askama::Template; +use assets::{Assets, EmbedService}; use routes::login::{route_get_login, route_post_login}; use rustical_store::{ auth::{AuthenticationMiddleware, AuthenticationProvider, User}, @@ -13,6 +14,7 @@ use rustical_store::{ }; use std::sync::Arc; +mod assets; mod config; mod routes; @@ -72,11 +74,7 @@ pub fn configure_frontend ) .app_data(Data::from(auth_provider)) .app_data(Data::from(store.clone())) - .service( - // TODO: Bundle assets in a neat way - actix_files::Files::new("/assets", "crates/frontend/frontend/dist/assets") - .prefer_utf8(true), - ) + .service(EmbedService::::new("/assets".to_owned())) .service( web::resource("/user/{user}").route(web::method(Method::GET).to(route_user::)), )