Mejora la integración de archivos estáticos #5

Merged
manuelcillero merged 2 commits from about-include-files into main 2025-08-16 12:24:38 +02:00
13 changed files with 251 additions and 99 deletions

View file

@ -83,41 +83,33 @@ Este código compila el archivo `main.scss` de la carpeta `static` del proyecto,
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
# 📦 Módulos generados
# 📦 Archivos generados
Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar
[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
donde se incluyen los recursos necesarios para compilar el proyecto. Por ejemplo, para
`with_name("guides")` se crea un archivo llamado `guides.rs`.
donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para
`with_name("guides")` se genera un archivo llamado `guides.rs`.
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto.
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se
usen nombres diferentes.
Normalmente no habrá que acceder a estos módulos; bastará con incluirlos en el proyecto con
[`include_files!`](https://docs.rs/pagetop/latest/pagetop/macro.include_files.html), y luego con
[`include_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.include_files_service.html)
configurar un servicio web para servir los recursos desde la ruta indicada:
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html)
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
```rust,ignore
use pagetop::prelude::*;
include_files!(guides);
pub struct MyExtension;
impl Extension for MyExtension {
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
include_files_service!(scfg, guides => "/ruta/a/guides");
static_files_service!(scfg, guides => "/ruta/a/guides");
}
}
```
También se puede asignar el conjunto de recursos a una variable global; p.ej. `GUIDES`:
```rust,ignore
include_files!(GUIDES => guides);
```
# 🚧 Advertencia

View file

@ -84,40 +84,32 @@ Este código compila el archivo `main.scss` de la carpeta `static` del proyecto,
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
# 📦 Módulos generados
# 📦 Archivos generados
Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar
[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
donde se incluyen los recursos necesarios para compilar el proyecto. Por ejemplo, para
`with_name("guides")` se crea un archivo llamado `guides.rs`.
donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para
`with_name("guides")` se genera un archivo llamado `guides.rs`.
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto.
No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se
usen nombres diferentes.
Normalmente no habrá que acceder a estos módulos; bastará con incluirlos en el proyecto con
[`include_files!`](https://docs.rs/pagetop/latest/pagetop/macro.include_files.html), y luego con
[`include_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.include_files_service.html)
configurar un servicio web para servir los recursos desde la ruta indicada:
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html)
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
```rust,ignore
use pagetop::prelude::*;
include_files!(guides);
pub struct MyExtension;
impl Extension for MyExtension {
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
include_files_service!(scfg, guides => "/ruta/a/guides");
static_files_service!(scfg, guides => "/ruta/a/guides");
}
}
```
También se puede asignar el conjunto de recursos a una variable global; p.ej. `GUIDES`:
```rust,ignore
include_files!(GUIDES => guides);
```
*/
#![doc(

View file

@ -110,11 +110,13 @@
//! }
//! ```
use crate::util;
use config::builder::DefaultState;
use config::{Config, ConfigBuilder, File};
use std::env;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::LazyLock;
// Nombre del directorio de configuración por defecto.
@ -125,25 +127,12 @@ const DEFAULT_RUN_MODE: &str = "default";
/// Valores originales cargados desde los archivos de configuración como pares `clave = valor`.
pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(|| {
// Determina el directorio de configuración:
// - Usa CONFIG_DIR si está definido en el entorno (p.ej.: CONFIG_DIR=/etc/myapp ./myapp).
// - Si no, intenta DEFAULT_CONFIG_DIR dentro del proyecto (en CARGO_MANIFEST_DIR).
// - Si nada de esto aplica, entonces usa DEFAULT_CONFIG_DIR relativo al ejecutable.
let config_dir: PathBuf = if let Ok(env_dir) = env::var("CONFIG_DIR") {
env_dir.into()
} else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let manifest_config = Path::new(&manifest_dir).join(DEFAULT_CONFIG_DIR);
if manifest_config.exists() {
manifest_config
} else {
DEFAULT_CONFIG_DIR.into()
}
} else {
DEFAULT_CONFIG_DIR.into()
};
// CONFIG_DIR (si existe) o DEFAULT_CONFIG_DIR. Si no se puede resolver, se usa tal cual.
let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into());
let config_dir = util::resolve_absolute_dir(&dir).unwrap_or_else(|_| PathBuf::from(&dir));
// Determina el modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Por defecto usa
// DEFAULT_RUN_MODE si no está definida (p.ej.: PAGETOP_RUN_MODE=production ./myapp).
// Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Si no está definida, se usa
// por defecto, DEFAULT_RUN_MODE (p.ej.: PAGETOP_RUN_MODE=production).
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into());
Config::builder()

View file

@ -1,7 +1,7 @@
use crate::core::action::add_action;
use crate::core::extension::ExtensionRef;
use crate::core::theme::all::THEMES;
use crate::{global, include_files, include_files_service, service, trace};
use crate::{global, service, static_files_service, trace};
use parking_lot::RwLock;
@ -125,8 +125,6 @@ pub fn initialize_extensions() {
// CONFIGURA LOS SERVICIOS *************************************************************************
include_files!(assets);
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
// Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos.
#[cfg(debug_assertions)]
@ -140,7 +138,5 @@ pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
extension.configure_service(scfg);
}
include_files_service!(
scfg, assets => "/", [&global::SETTINGS.dev.pagetop_project_dir, "static"]
);
static_files_service!(scfg, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/");
}

View file

@ -13,7 +13,7 @@ include_config!(SETTINGS: Settings => [
"app.startup_banner" => "Slant",
// [dev]
"dev.pagetop_project_dir" => "",
"dev.pagetop_static_dir" => "",
// [log]
"log.enabled" => true,
@ -68,11 +68,15 @@ pub struct App {
#[derive(Debug, Deserialize)]
/// Sección `[Dev]` de la configuración. Forma parte de [`Settings`].
pub struct Dev {
/// Los archivos estáticos requeridos por `PageTop` se integran por defecto en el binario
/// ejecutable. Sin embargo, durante el desarrollo puede resultar útil servirlos desde su propio
/// directorio para evitar recompilar cada vez que se modifican. En ese caso, este ajuste debe
/// indicar la ruta absoluta al directorio raíz del proyecto.
pub pagetop_project_dir: String,
/// Directorio desde el que servir los archivos estáticos de `PageTop`.
///
/// Por defecto, los archivos se integran en el binario de la aplicación. Si aquí se indica una
/// ruta válida, ya sea absoluta o relativa al directorio del proyecto o del binario en
/// ejecución, se servirán desde el sistema de ficheros en su lugar. Esto es especialmente útil
/// en desarrollo, ya que evita recompilar el proyecto por cambios en estos archivos.
///
/// Si la cadena está vacía, se ignora este ajuste.
pub pagetop_static_dir: String,
}
#[derive(Debug, Deserialize)]

View file

@ -12,8 +12,7 @@ use crate::AutoDefault;
///
/// > **Nota**
/// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden
/// > incluirse en el proyecto utilizando [`include_files!`](crate::include_files) y servirse con
/// > [`include_files_service!`](crate::include_files_service).
/// > servirse usando [`static_files_service!`](crate::static_files_service).
///
/// # Ejemplo
///

View file

@ -30,8 +30,7 @@ enum Source {
///
/// > **Nota**
/// > Los archivos de los *scripts* deben estar disponibles en el servidor web de la aplicación.
/// > Pueden incluirse en el proyecto utilizando [`include_files!`](crate::include_files) y servirse
/// > con [`include_files_service!`](crate::include_files_service).
/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service).
///
/// # Ejemplo
///

View file

@ -55,8 +55,7 @@ impl TargetMedia {
///
/// > **Nota**
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden
/// > incluirse en el proyecto utilizando [`include_files!`](crate::include_files) y servirse con
/// > [`include_files_service!`](crate::include_files_service).
/// > servirse usando [`static_files_service!`](crate::static_files_service).
///
/// # Ejemplo
///

View file

@ -103,7 +103,8 @@ pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault};
pub use pagetop_statics::{resource, StaticResource};
/// Conjunto de recursos asociados a `$STATIC` en [`include_files!`](crate::include_files).
/// Contenedor para un conjunto de recursos embebidos.
#[derive(AutoDefault)]
pub struct StaticResources {
bundle: HashMap<&'static str, StaticResource>,
}

View file

@ -15,7 +15,8 @@ pub use crate::include_config;
// crate::locale
pub use crate::include_locales;
// crate::service
pub use crate::{include_files, include_files_service};
#[allow(deprecated)]
pub use crate::{include_files, include_files_service, static_files_service};
// crate::core::action
pub use crate::actions_boxed;

View file

@ -15,6 +15,9 @@ pub use pagetop_statics::ResourceFiles;
#[doc(hidden)]
pub use actix_web::test;
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
/// en su lugar.
///
/// Incluye en código un conjunto de recursos previamente preparado con `build.rs`.
///
/// # Formas de uso
@ -39,6 +42,7 @@ pub use actix_web::test;
///
/// include_files!(STATIC_ASSETS => assets);
/// ```
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
#[macro_export]
macro_rules! include_files {
// Forma 1: incluye un conjunto de recursos por nombre.
@ -63,6 +67,9 @@ macro_rules! include_files {
};
}
/// **Obsoleto desde la versión 0.3.0**: usar [`static_files_service!`](crate::static_files_service)
/// en su lugar.
///
/// 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
@ -104,6 +111,7 @@ macro_rules! include_files {
/// // También desde el directorio actual de ejecución.
/// include_files_service!(cfg, assets => "/public", ["", "static"]);
/// ```
#[deprecated(since = "0.3.0", note = "Use `static_files_service!` instead")]
#[macro_export]
macro_rules! include_files_service {
( $scfg:ident, $bundle:ident => $route:expr $(, [$root:expr, $relative:expr])? ) => {{
@ -137,3 +145,114 @@ macro_rules! include_files_service {
}
}};
}
/// Configura un servicio web para publicar archivos estáticos.
///
/// La macro ofrece tres modos para configurar el servicio:
///
/// - **Sistema de ficheros o embebido** (`[$path, $bundle]`): trata de servir los archivos desde
/// `$path`; y si es una cadena vacía, no existe o no es un directorio, entonces usará el conjunto
/// de recursos `$bundle` integrado en el binario.
/// - **Sólo embebido** (`[$bundle]`): sirve siempre desde el conjunto de recursos `$bundle`
/// integrado en el binario.
/// - **Sólo sistema de ficheros** (`$path`): sin usar corchetes, sirve únicamente desde el sistema
/// de ficheros si existe; en otro caso no registra el servicio.
///
/// # Argumentos
///
/// * `$scfg` Instancia de [`ServiceConfig`](crate::service::web::ServiceConfig) donde aplicar la
/// configuración.
/// * `$path` Ruta al directorio local con los archivos estáticos.
/// * `$bundle` Nombre del conjunto de recursos que esta macro integra en el binario.
/// * `$route` Ruta URL base desde la que se servirán los archivos.
///
/// # Ejemplos
///
/// ```rust,ignore
/// use pagetop::prelude::*;
///
/// pub struct MyExtension;
///
/// impl Extension for MyExtension {
/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
/// // Forma 1) Sistema de ficheros o embebido.
/// static_files_service!(scfg, ["/var/www/static", assets] => "/public");
///
/// // Forma 2) Siempre embebido.
/// static_files_service!(scfg, [assets] => "/public");
///
/// // Forma 3) Sólo sistema de ficheros (no requiere `assets`).
/// static_files_service!(scfg, "/var/www/static" => "/public");
/// }
/// }
/// ```
#[macro_export]
macro_rules! static_files_service {
// Forma 1: primero intenta servir desde el sistema de ficheros; si falla, sirve embebido.
( $scfg:ident, [$path:expr, $bundle:ident] => $route:expr $(,)? ) => {{
let span = $crate::trace::debug_span!(
"Configuring static files (file system or embedded)",
mode = "fs_or_embedded",
route = $route,
);
let _ = span.in_scope(|| {
let mut serve_embedded: bool = true;
if !::std::path::Path::new(&$path).as_os_str().is_empty() {
if let Ok(absolute) = $crate::util::resolve_absolute_dir($path) {
$scfg.service($crate::service::ActixFiles::new($route, absolute));
serve_embedded = false;
}
}
if serve_embedded {
$crate::util::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
$scfg.service($crate::service::ResourceFiles::new(
$route,
[<static_files_ $bundle>]::$bundle(),
));
}
}
});
}};
// Forma 2: sirve siempre embebido.
( $scfg:ident, [$bundle:ident] => $route:expr $(,)? ) => {{
let span = $crate::trace::debug_span!(
"Configuring static files (using embedded only)",
mode = "embedded",
route = $route,
);
let _ = span.in_scope(|| {
$crate::util::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
$scfg.service($crate::service::ResourceFiles::new(
$route,
[<static_files_ $bundle>]::$bundle(),
));
}
});
}};
// Forma 3: intenta servir desde el sistema de ficheros.
( $scfg:ident, $path:expr => $route:expr $(,)? ) => {{
let span = $crate::trace::debug_span!(
"Configuring static files (file system only)",
mode = "fs",
route = $route,
);
let _ = span.in_scope(|| match $crate::util::resolve_absolute_dir($path) {
Ok(absolute) => {
$scfg.service($crate::service::ActixFiles::new($route, absolute));
}
Err(e) => {
$crate::trace::warn!(
"Static dir not found or invalid for route `{}`: {:?} ({e})",
$route,
$path,
);
}
});
}};
}

View file

@ -2,35 +2,44 @@
use crate::trace;
use std::env;
use std::io;
use std::path::{Path, PathBuf};
// FUNCIONES ÚTILES ********************************************************************************
/// Devuelve la ruta absoluta a un directorio existente.
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
///
/// * 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.
/// - Si la ruta es relativa, se resuelve respecto al directorio del proyecto según la variable de
/// entorno `CARGO_MANIFEST_DIR` (si existe) o, en su defecto, respecto al directorio actual de
/// trabajo.
/// - Normaliza y valida la ruta final (resuelve `.`/`..` y enlaces simbólicos).
/// - Devuelve error si la ruta no existe o no es un directorio.
///
/// # Ejemplo
/// # Ejemplos
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let root = "/home/user";
/// let rel = "documents";
/// println!("{:#?}", util::absolute_dir(root, rel));
/// // Ruta relativa, se resuelve respecto a CARGO_MANIFEST_DIR o al directorio actual (`cwd`).
/// println!("{:#?}", util::resolve_absolute_dir("documents"));
///
/// // Ruta absoluta, se normaliza y valida tal cual.
/// println!("{:#?}", util::resolve_absolute_dir("/var/www"));
/// ```
pub fn absolute_dir<P, Q>(root_path: P, relative_path: Q) -> io::Result<PathBuf>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
// 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());
pub fn resolve_absolute_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
let candidate = if path.is_absolute() {
path.to_path_buf()
} else {
// Directorio base CARGO_MANIFEST_DIR si está disponible; o current_dir() en su defecto.
env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.or_else(|| env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."))
.join(path)
};
// Resuelve `.`/`..`, enlaces simbólicos y obtiene la ruta absoluta en un único paso.
let absolute_dir = candidate.canonicalize()?;
@ -47,6 +56,16 @@ where
}
}
/// Devuelve la ruta absoluta a un directorio existente.
#[deprecated(since = "0.3.0", note = "Use [`resolve_absolute_dir`] instead")]
pub fn absolute_dir<P, Q>(root_path: P, relative_path: Q) -> io::Result<PathBuf>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
resolve_absolute_dir(root_path.as_ref().join(relative_path.as_ref()))
}
// MACROS ÚTILES ***********************************************************************************
#[doc(hidden)]

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*;
use std::{fs, io};
use std::{env, fs, io};
use tempfile::TempDir;
#[cfg(unix)]
@ -13,15 +13,36 @@ mod unix {
// /tmp/<rand>/sub
let td = TempDir::new()?;
let root = td.path();
let sub = root.join("sub");
let sub = td.path().join("sub");
fs::create_dir(&sub)?;
let abs = util::absolute_dir(root, "sub")?;
let abs = util::resolve_absolute_dir(&sub)?;
assert_eq!(abs, std::fs::canonicalize(&sub)?);
Ok(())
}
#[pagetop::test]
async fn ok_relative_dir_with_manifest() -> io::Result<()> {
let _app = service::test::init_service(Application::new().test()).await;
let td = TempDir::new()?;
let sub = td.path().join("sub");
fs::create_dir(&sub)?;
// Fija CARGO_MANIFEST_DIR para que "sub" se resuelva contra td.path()
let prev_manifest_dir = env::var_os("CARGO_MANIFEST_DIR");
env::set_var("CARGO_MANIFEST_DIR", td.path());
let res = util::resolve_absolute_dir("sub");
// Restaura entorno.
match prev_manifest_dir {
Some(v) => env::set_var("CARGO_MANIFEST_DIR", v),
None => env::remove_var("CARGO_MANIFEST_DIR"),
}
assert_eq!(res?, 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;
@ -30,7 +51,7 @@ mod unix {
let file = td.path().join("foo.txt");
fs::write(&file, b"data")?;
let err = util::absolute_dir(td.path(), "foo.txt").unwrap_err();
let err = util::resolve_absolute_dir(&file).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
@ -46,15 +67,36 @@ mod windows {
// C:\Users\...\Temp\...
let td = TempDir::new()?;
let root = td.path();
let sub = root.join("sub");
let sub = td.path().join("sub");
fs::create_dir(&sub)?;
let abs = util::absolute_dir(root, sub.as_path())?;
let abs = util::resolve_absolute_dir(&sub)?;
assert_eq!(abs, std::fs::canonicalize(&sub)?);
Ok(())
}
#[pagetop::test]
async fn ok_relative_dir_with_manifest() -> io::Result<()> {
let _app = service::test::init_service(Application::new().test()).await;
let td = TempDir::new()?;
let sub = td.path().join("sub");
fs::create_dir(&sub)?;
// Fija CARGO_MANIFEST_DIR para que "sub" se resuelva contra td.path()
let prev_manifest_dir = env::var_os("CARGO_MANIFEST_DIR");
env::set_var("CARGO_MANIFEST_DIR", td.path());
let res = util::resolve_absolute_dir("sub");
// Restaura entorno.
match prev_manifest_dir {
Some(v) => env::set_var("CARGO_MANIFEST_DIR", v),
None => env::remove_var("CARGO_MANIFEST_DIR"),
}
assert_eq!(res?, 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;
@ -63,7 +105,7 @@ mod windows {
let file = td.path().join("foo.txt");
fs::write(&file, b"data")?;
let err = util::absolute_dir(td.path(), "foo.txt").unwrap_err();
let err = util::resolve_absolute_dir(&file).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
Ok(())
}