diff --git a/Cargo.lock b/Cargo.lock index 24c9616..659ed18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,29 @@ 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.20", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + [[package]] name = "actix-http" version = "3.11.0" @@ -34,7 +57,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "flate2", "foldhash", @@ -149,7 +172,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 2.0.1", "encoding_rs", "foldhash", "futures-core", @@ -185,6 +208,18 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-web-static-files" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf6d1ef6d7a60e084f9e0595e2a5234abda14e76c105ecf8e2d0e8800c41a1f" +dependencies = [ + "actix-web", + "derive_more 0.99.20", + "futures-util", + "static-files", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -502,6 +537,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cookie" version = "0.16.2" @@ -590,6 +631,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -632,12 +686,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -809,6 +857,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -942,6 +991,12 @@ dependencies = [ "itoa", ] +[[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.10.1" @@ -1352,13 +1407,14 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pagetop" -version = "0.0.8" +version = "0.0.9" dependencies = [ + "actix-files", "actix-web", + "actix-web-static-files", "chrono", "colored", "config", - "dunce", "figlet-rs", "fluent-templates", "itoa", @@ -1742,6 +1798,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.0.7" @@ -1797,6 +1862,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -2357,6 +2428,12 @@ dependencies = [ "wasm-bindgen", ] +[[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.1" diff --git a/Cargo.toml b/Cargo.toml index 3ef8468..851c620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pagetop" -version = "0.0.8" +version = "0.0.9" edition = "2021" description = """\ @@ -33,6 +33,8 @@ fluent-templates = "0.13.0" unic-langid = { version = "0.9.6", features = ["macros"] } actix-web = "4.11.0" +actix-web-files = { package = "actix-files", version = "0.6.6" } +actix-web-static-files = "4.0.1" static-files.workspace = true serde = { version = "1.0", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index 4519c44..800f783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,9 @@ pub use pagetop_macros::{html, main, test, AutoDefault}; +/// Representa un conjunto de recursos asociados a `$STATIC` en [`include_files!`]. +pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>; + // API ********************************************************************************************* // Funciones y macros útiles. diff --git a/src/prelude.rs b/src/prelude.rs index cc38877..c27b997 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use crate::{html, main, test}; -pub use crate::AutoDefault; +pub use crate::{AutoDefault, StaticResources}; // MACROS. @@ -14,6 +14,8 @@ pub use crate::hm; pub use crate::include_config; // crate::locale pub use crate::include_locales; +// crate::service +pub use crate::{include_files, include_files_service}; // API. diff --git a/src/service.rs b/src/service.rs index 90b1375..1adaead 100644 --- a/src/service.rs +++ b/src/service.rs @@ -5,5 +5,129 @@ pub use actix_web::dev::Server; pub use actix_web::dev::ServiceFactory as Factory; pub use actix_web::dev::ServiceRequest as Request; pub use actix_web::dev::ServiceResponse as Response; -pub use actix_web::{http, rt, test}; +pub use actix_web::{http, rt, web}; pub use actix_web::{App, Error, HttpServer}; + +#[doc(hidden)] +pub use actix_web::test; + +/// Incluye en código un conjunto de recursos previamente preparado con `build.rs`. +/// +/// # Formas de uso +/// +/// * `include_files!(media)` - Incluye el conjunto de recursos llamado `media`. Normalmente se +/// usará esta forma. +/// +/// * `include_files!(BLOG_HM => blog)` - Asigna a la variable estática `BLOG_HM` un conjunto de +/// recursos llamado `blog`. +/// +/// # Argumentos +/// +/// * `$bundle` – Nombre del conjunto de recursos generado por `build.rs` (consultar +/// [`pagetop_build`](https://docs.rs/pagetop-build)). +/// * `$STATIC` – Identificador para la variable estática de tipo +/// [`StaticResources`](`crate::StaticResources`). +/// +/// # Ejemplos +/// +/// ```rust,ignore +/// include_files!(assets); // Uso habitual. +/// +/// include_files!(STATIC_ASSETS => assets); +/// ``` +#[macro_export] +macro_rules! include_files { + // Forma 1: incluye un conjunto de recursos por nombre. + ( $bundle:ident ) => { + $crate::util::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + } + }; + // Forma 2: asigna a una variable estática $STATIC un conjunto de recursos. + ( $STATIC:ident => $bundle:ident ) => { + $crate::util::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + pub static $STATIC: std::sync::LazyLock = std::sync::LazyLock::new( + []::$bundle + ); + } + }; +} + +/// Configura un servicio web para publicar los recursos embebidos con [`include_files!`]. +/// +/// El código expandido de la macro decide durante el arranque de la aplicación si debe servir los +/// archivos de los recursos embebidos o directamente desde el sistema de ficheros, si se ha +/// indicado una ruta válida a un directorio de recursos. +/// +/// # Argumentos +/// +/// * `$scfg` – Instancia de [`ServiceConfig`](crate::service::web::ServiceConfig) donde aplicar la +/// configuración del servicio web. +/// * `$bundle` – Nombre del conjunto de recursos incluido con [`include_files!`]. +/// * `$route` – Ruta URL de origen desde la que se servirán los archivos. +/// * `[ $root, $relative ]` *(opcional)* – Directorio raíz y ruta relativa para construir la ruta +/// absoluta donde buscar los archivos en el sistema de ficheros (ver +/// [`absolute_dir`](crate::util::absolute_dir)). Si no existe, se usarán los recursos embebidos. +/// +/// # Ejemplos +/// +/// ```rust,ignore +/// use pagetop::prelude::*; +/// +/// include_files!(assets); +/// +/// pub struct MyExtension; +/// +/// impl ExtensionTrait for MyExtension { +/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { +/// include_files_service!(scfg, assets => "/public"); +/// } +/// } +/// ``` +/// +/// Y para buscar los recursos en el sistema de ficheros (si existe la ruta absoluta): +/// +/// ```rust,ignore +/// include_files_service!(cfg, assets => "/public", ["/var/www", "assets"]); +/// +/// // También desde el directorio actual de ejecución. +/// include_files_service!(cfg, assets => "/public", ["", "static"]); +/// ``` +#[macro_export] +macro_rules! include_files_service { + ( $scfg:ident, $bundle:ident => $route:expr $(, [$root:expr, $relative:expr])? ) => {{ + $crate::util::paste! { + let span = $crate::trace::debug_span!("Configuring static files ", path = $route); + let _ = span.in_scope(|| { + // Determina si se sirven recursos embebidos (`true`) o desde disco (`false`). + #[allow(unused_mut)] + let mut serve_embedded:bool = true; + $( + // Si `$root` y `$relative` no están vacíos, se comprueba la ruta absoluta. + if !$root.is_empty() && !$relative.is_empty() { + if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) { + // Servimos directamente desde el sistema de ficheros. + $scfg.service($crate::service::ActixFiles::new( + $route, + absolute, + ).show_files_listing()); + serve_embedded = false + } + } + )? + // Si no se localiza el directorio, se exponen entonces los recursos embebidos. + if serve_embedded { + $scfg.service($crate::service::ResourceFiles::new( + $route, + []::$bundle(), + )); + } + }); + } + }}; +} diff --git a/src/util.rs b/src/util.rs index 07a6abd..7a48001 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,56 @@ //! Funciones y macros útiles. +use crate::trace; + +use std::io; +use std::path::{Path, PathBuf}; + + +/// Devuelve la ruta absoluta a un directorio existente. +/// +/// * Si `relative_path` es una ruta absoluta, entonces se ignora `root_path`. +/// * Si la ruta final es relativa, se convierte en absoluta respecto al directorio actual. +/// * Devuelve error si la ruta no existe o no es un directorio. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let root = "/home/user"; +/// let rel = "documents"; +/// println!("{:#?}", util::absolute_dir(root, rel)); +/// ``` +pub fn absolute_dir(root_path: P, relative_path: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + // Une ambas rutas: + // - Si `relative_path` es absoluta, el `join` la devuelve tal cual, descartando `root_path`. + // - Si el resultado es aún relativo, lo será respecto al directorio actual. + let candidate = root_path.as_ref().join(relative_path.as_ref()); + + // Resuelve `.`/`..`, enlaces simbólicos y obtiene la ruta absoluta en un único paso. + let absolute_dir = candidate.canonicalize()?; + + // Asegura que realmente es un directorio existente. + if absolute_dir.is_dir() { + Ok(absolute_dir) + } else { + Err({ + let msg = format!("Path \"{}\" is not a directory", absolute_dir.display()); + trace::warn!(msg); + io::Error::new(io::ErrorKind::InvalidInput, msg) + }) + } +} + // MACROS ÚTILES *********************************************************************************** +#[doc(hidden)] +pub use paste::paste; + #[macro_export] /// Macro para construir una colección de pares clave-valor. /// diff --git a/tests/util.rs b/tests/util.rs new file mode 100644 index 0000000..2d52064 --- /dev/null +++ b/tests/util.rs @@ -0,0 +1,70 @@ +use pagetop::prelude::*; + +use std::{fs, io}; +use tempfile::TempDir; + +#[cfg(unix)] +mod unix { + use super::*; + + #[pagetop::test] + async fn ok_absolute_dir() -> io::Result<()> { + let _app = service::test::init_service(Application::new().test()).await; + + // /tmp//sub + let td = TempDir::new()?; + let root = td.path(); + let sub = root.join("sub"); + fs::create_dir(&sub)?; + + let abs = util::absolute_dir(root, "sub")?; + assert_eq!(abs, std::fs::canonicalize(&sub)?); + Ok(()) + } + + #[pagetop::test] + async fn error_not_a_directory() -> io::Result<()> { + let _app = service::test::init_service(Application::new().test()).await; + + let td = TempDir::new()?; + let file = td.path().join("foo.txt"); + fs::write(&file, b"data")?; + + let err = util::absolute_dir(td.path(), "foo.txt").unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + Ok(()) + } +} + +#[cfg(windows)] +mod windows { + use super::*; + + #[pagetop::test] + async fn ok_absolute_dir() -> io::Result<()> { + let _app = service::test::init_service(Application::new().test()).await; + + // C:\Users\…\Temp\… + let td = TempDir::new()?; + let root = td.path(); + let sub = root.join("sub"); + fs::create_dir(&sub)?; + + let abs = util::absolute_dir(root, sub.as_path())?; + assert_eq!(abs, std::fs::canonicalize(&sub)?); + Ok(()) + } + + #[pagetop::test] + async fn error_not_a_directory() -> io::Result<()> { + let _app = service::test::init_service(Application::new().test()).await; + + let td = TempDir::new()?; + let file = td.path().join("foo.txt"); + fs::write(&file, b"data")?; + + let err = util::absolute_dir(td.path(), "foo.txt").unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + Ok(()) + } +}