From 4e277a2b03161bc92ac9089bb69f907fe1f0d6a9 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 8 Aug 2025 18:49:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20A=C3=B1ade=20librer=C3=ADa=20para?= =?UTF-8?q?=20gestionar=20recursos=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 48 +-- Cargo.toml | 9 +- helpers/pagetop-build/Cargo.toml | 2 +- helpers/pagetop-build/src/lib.rs | 2 +- helpers/pagetop-statics/Cargo.toml | 32 ++ helpers/pagetop-statics/LICENSE-APACHE | 201 +++++++++ helpers/pagetop-statics/LICENSE-MIT | 21 + helpers/pagetop-statics/README.md | 50 +++ helpers/pagetop-statics/build.rs | 43 ++ helpers/pagetop-statics/src/lib.rs | 34 ++ helpers/pagetop-statics/src/resource.rs | 249 +++++++++++ helpers/pagetop-statics/src/resource_dir.rs | 118 ++++++ helpers/pagetop-statics/src/resource_files.rs | 396 ++++++++++++++++++ helpers/pagetop-statics/src/sets.rs | 184 ++++++++ helpers/pagetop-statics/tests/file1.txt | 0 helpers/pagetop-statics/tests/file2.txt | 0 helpers/pagetop-statics/tests/file3.info | 0 helpers/pagetop-statics/tests/index.html | 10 + src/lib.rs | 8 +- src/service.rs | 4 +- 20 files changed, 1370 insertions(+), 41 deletions(-) create mode 100644 helpers/pagetop-statics/Cargo.toml create mode 100644 helpers/pagetop-statics/LICENSE-APACHE create mode 100644 helpers/pagetop-statics/LICENSE-MIT create mode 100644 helpers/pagetop-statics/README.md create mode 100644 helpers/pagetop-statics/build.rs create mode 100644 helpers/pagetop-statics/src/lib.rs create mode 100644 helpers/pagetop-statics/src/resource.rs create mode 100644 helpers/pagetop-statics/src/resource_dir.rs create mode 100644 helpers/pagetop-statics/src/resource_files.rs create mode 100644 helpers/pagetop-statics/src/sets.rs create mode 100644 helpers/pagetop-statics/tests/file1.txt create mode 100644 helpers/pagetop-statics/tests/file2.txt create mode 100644 helpers/pagetop-statics/tests/file3.info create mode 100644 helpers/pagetop-statics/tests/index.html diff --git a/Cargo.lock b/Cargo.lock index e9ba9fb..2f15a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,18 +225,6 @@ 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" @@ -658,9 +646,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1076,9 +1064,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -1574,7 +1562,6 @@ dependencies = [ "actix-files", "actix-session", "actix-web", - "actix-web-static-files", "chrono", "colored", "concat-string", @@ -1584,10 +1571,10 @@ dependencies = [ "itoa", "pagetop-build", "pagetop-macros", + "pagetop-statics", "parking_lot", "pastey", "serde", - "static-files", "substring", "tempfile", "terminal_size", @@ -1603,7 +1590,7 @@ name = "pagetop-build" version = "0.1.1" dependencies = [ "grass", - "static-files", + "pagetop-statics", ] [[package]] @@ -1616,6 +1603,18 @@ dependencies = [ "syn", ] +[[package]] +name = "pagetop-statics" +version = "0.0.1" +dependencies = [ + "actix-web", + "change-detection", + "derive_more 0.99.20", + "futures-util", + "mime_guess", + "path-slash", +] + [[package]] name = "parking_lot" version = "0.12.4" @@ -2178,17 +2177,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static-files" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c425c07353535ef55b45420f5a8b0a397cd9bc3d7e5236497ca0d90604aa9b" -dependencies = [ - "change-detection", - "mime_guess", - "path-slash", -] - [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 3eee24d..3600742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,15 +34,14 @@ tracing-actix-web = "0.7.19" fluent-templates = "0.13.0" unic-langid = { version = "0.9.6", features = ["macros"] } -actix-web = "4.11.0" +actix-web = { workspace = true, default-features = true } actix-session = { version = "0.10.1", features = ["cookie-session"] } 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"] } pagetop-macros.workspace = true +pagetop-statics.workspace = true [features] default = [] @@ -60,6 +59,7 @@ resolver = "2" members = [ "helpers/pagetop-build", "helpers/pagetop-macros", + "helpers/pagetop-statics", ] [workspace.package] @@ -69,7 +69,8 @@ license = "MIT OR Apache-2.0" authors = ["Manuel Cillero "] [workspace.dependencies] -static-files = "0.2.5" +actix-web = { version = "4.11.0", default-features = false } pagetop-build = { version = "0.1", path = "helpers/pagetop-build" } pagetop-macros = { version = "0.1", path = "helpers/pagetop-macros" } +pagetop-statics = { version = "0.0", path = "helpers/pagetop-statics" } diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index ba911ef..41e826e 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -17,4 +17,4 @@ authors.workspace = true [dependencies] grass = "0.13.4" -static-files.workspace = true +pagetop-statics.workspace = true diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index 4dcd70c..b360377 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -123,7 +123,7 @@ )] use grass::{from_path, Options, OutputStyle}; -use static_files::{resource_dir, ResourceDir}; +use pagetop_statics::{resource_dir, ResourceDir}; use std::fs::{create_dir_all, remove_dir_all, File}; use std::io::Write; diff --git a/helpers/pagetop-statics/Cargo.toml b/helpers/pagetop-statics/Cargo.toml new file mode 100644 index 0000000..fdb16c0 --- /dev/null +++ b/helpers/pagetop-statics/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "pagetop-statics" +version = "0.0.1" +edition = "2021" + +description = """ + Librería para automatizar la recopilación de recursos estáticos en PageTop. +""" +publish = false + +repository.workspace = true +homepage.workspace = true +license.workspace = true +authors.workspace = true + +[features] +default = ["change-detection"] +sort = [] + +[dependencies] +change-detection = { version = "1.2", optional = true } +mime_guess = "2.0" +path-slash = "0.1" + +actix-web.workspace = true +derive_more = "0.99.17" +futures-util = { version = "0.3", default-features = false, features = ["std"] } + +[build-dependencies] +change-detection = { version = "1.2", optional = true } +mime_guess = "2.0" +path-slash = "0.1" diff --git a/helpers/pagetop-statics/LICENSE-APACHE b/helpers/pagetop-statics/LICENSE-APACHE new file mode 100644 index 0000000..263ddac --- /dev/null +++ b/helpers/pagetop-statics/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Manuel Cillero + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/helpers/pagetop-statics/LICENSE-MIT b/helpers/pagetop-statics/LICENSE-MIT new file mode 100644 index 0000000..cd8af3d --- /dev/null +++ b/helpers/pagetop-statics/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Manuel Cillero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md new file mode 100644 index 0000000..c053e95 --- /dev/null +++ b/helpers/pagetop-statics/README.md @@ -0,0 +1,50 @@ +
+ +

PageTop Statics

+ +

Librería para automatizar la recopilación de recursos estáticos en PageTop.

+ +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-licencia) + +
+ +## Descripción general + +Permite a `PageTop` incluir archivos estáticos en el ejecutable de la aplicación para servirlos de +forma eficiente vía web, con detección de cambios que optimiza el tiempo de compilación. + +Para ello, reúne el código de los *crates* [static-files](https://crates.io/crates/static_files) +(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y +[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión +[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por +[Alexander Korolev](https://crates.io/users/kilork). + +Estas implementaciones se integran en `PageTop` para evitar que cada proyecto tenga que declarar +`static-files` manualmente como dependencia en su `Cargo.toml`. + +## Sobre PageTop + +[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web +clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y +configurables, basadas en HTML, CSS y JavaScript. + + +# 🚧 Advertencia + +`PageTop` es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su +ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos +hasta que se libere la versión **1.0.0**. + + +# 📜 Licencia + +El código está disponible bajo una doble licencia: + + * **Licencia MIT** + ([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT) + + * **Licencia Apache, Versión 2.0** + ([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0) + +Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en +el ecosistema Rust. diff --git a/helpers/pagetop-statics/build.rs b/helpers/pagetop-statics/build.rs new file mode 100644 index 0000000..fcd009c --- /dev/null +++ b/helpers/pagetop-statics/build.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] +#![doc(html_no_source)] +#![allow(clippy::needless_doctest_main)] + +mod resource { + include!("src/resource.rs"); +} +use resource::generate_resources_mapping; +mod resource_dir { + include!("src/resource_dir.rs"); +} +use resource_dir::resource_dir; +mod sets { + include!("src/sets.rs"); +} +use sets::{generate_resources_sets, SplitByCount}; + +use std::{env, path::Path}; + +fn main() -> std::io::Result<()> { + resource_dir("./tests").build_test()?; + + let out_dir = env::var("OUT_DIR").unwrap(); + + generate_resources_mapping( + "./tests", + None, + Path::new(&out_dir).join("generated_mapping.rs"), + "pagetop_statics", + )?; + + generate_resources_sets( + "./tests", + None, + Path::new(&out_dir).join("generated_sets.rs"), + "sets", + "generate", + &mut SplitByCount::new(2), + "pagetop_statics", + )?; + + Ok(()) +} diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs new file mode 100644 index 0000000..fffd1ae --- /dev/null +++ b/helpers/pagetop-statics/src/lib.rs @@ -0,0 +1,34 @@ +//!
+//! +//!

PageTop Statics

+//! +//!

Librería para automatizar la recopilación de recursos estáticos en PageTop.

+//! +//! [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-licencia) +//! +//!
+//! +//! ## Sobre PageTop +//! +//! [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la +//! web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles +//! y configurables, basadas en HTML, CSS y JavaScript. + +#![doc(test(no_crate_inject))] +#![doc( + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" +)] +#![allow(clippy::needless_doctest_main)] + +/// Resource definition and single module based generation. +pub mod resource; +pub use resource::Resource as StaticResource; + +mod resource_dir; +pub use resource_dir::{resource_dir, ResourceDir}; + +mod resource_files; +pub use resource_files::{ResourceFiles, UriSegmentError}; + +/// Support for module based generations. Use it for large data sets (more than 128 Mb). +pub mod sets; diff --git a/helpers/pagetop-statics/src/resource.rs b/helpers/pagetop-statics/src/resource.rs new file mode 100644 index 0000000..0b81969 --- /dev/null +++ b/helpers/pagetop-statics/src/resource.rs @@ -0,0 +1,249 @@ +use path_slash::PathExt; +use std::{ + fs::{self, File, Metadata}, + io::{self, Write}, + path::{Path, PathBuf}, + time::SystemTime, +}; + +/// Static files resource. +pub struct Resource { + pub data: &'static [u8], + pub modified: u64, + pub mime_type: &'static str, +} + +/// Used internally in generated functions. +#[inline] +pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource { + Resource { + data, + modified, + mime_type, + } +} + +pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r"; + +/// Generate resources for `project_dir` using `filter`. +/// Result saved in `generated_filename` and function named as `fn_name`. +/// +/// in `build.rs`: +/// ```rust +/// use std::{env, path::Path}; +/// use pagetop_statics::resource::generate_resources; +/// +/// fn main() { +/// let out_dir = env::var("OUT_DIR").unwrap(); +/// let generated_filename = Path::new(&out_dir).join("generated.rs"); +/// generate_resources("./tests", None, generated_filename, "generate", "pagetop_statics").unwrap(); +/// } +/// ``` +/// +/// in `main.rs`: +/// ```rust +/// include!(concat!(env!("OUT_DIR"), "/generated.rs")); +/// +/// fn main() { +/// let generated_file = generate(); +/// +/// assert_eq!(generated_file.len(), 4); +/// } +/// ``` +pub fn generate_resources, G: AsRef>( + project_dir: P, + filter: Option bool>, + generated_filename: G, + fn_name: &str, + crate_name: &str, +) -> io::Result<()> { + let resources = collect_resources(&project_dir, filter)?; + + let mut f = File::create(&generated_filename)?; + + generate_function_header(&mut f, fn_name, crate_name)?; + generate_uses(&mut f, crate_name)?; + + generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?; + generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?; + generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?; + + generate_function_end(&mut f)?; + + Ok(()) +} + +/// Generate resource mapping for `project_dir` using `filter`. +/// Result saved in `generated_filename` as anonymous block which returns HashMap<&'static str, Resource>. +/// +/// in `build.rs`: +/// ```rust +/// +/// use std::{env, path::Path}; +/// use pagetop_statics::resource::generate_resources_mapping; +/// +/// fn main() { +/// let out_dir = env::var("OUT_DIR").unwrap(); +/// let generated_filename = Path::new(&out_dir).join("generated_mapping.rs"); +/// generate_resources_mapping("./tests", None, generated_filename, "pagetop_statics").unwrap(); +/// } +/// ``` +/// +/// in `main.rs`: +/// ```rust +/// use std::collections::HashMap; +/// +/// use pagetop_statics::StaticResource; +/// +/// fn generate_mapping() -> HashMap<&'static str, StaticResource> { +/// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs")) +/// } +/// +/// fn main() { +/// let generated_file = generate_mapping(); +/// +/// assert_eq!(generated_file.len(), 4); +/// +/// } +/// ``` +pub fn generate_resources_mapping, G: AsRef>( + project_dir: P, + filter: Option bool>, + generated_filename: G, + crate_name: &str, +) -> io::Result<()> { + let resources = collect_resources(&project_dir, filter)?; + + let mut f = File::create(&generated_filename)?; + writeln!(f, "{{")?; + + generate_uses(&mut f, crate_name)?; + + generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?; + + generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?; + + generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?; + + writeln!(f, "}}")?; + Ok(()) +} + +#[cfg(not(feature = "sort"))] +pub(crate) fn collect_resources>( + path: P, + filter: Option bool>, +) -> io::Result> { + collect_resources_nested(path, filter) +} + +#[cfg(feature = "sort")] +pub(crate) fn collect_resources>( + path: P, + filter: Option bool>, +) -> io::Result> { + let mut resources = collect_resources_nested(path, filter)?; + resources.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(resources) +} + +#[inline] +fn collect_resources_nested>( + path: P, + filter: Option bool>, +) -> io::Result> { + let mut result = vec![]; + + for entry in fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + + if let Some(ref filter) = filter { + if !filter(path.as_ref()) { + continue; + } + } + + if path.is_dir() { + let nested = collect_resources(path, filter)?; + result.extend(nested); + } else { + result.push((path, entry.metadata()?)); + } + } + + Ok(result) +} + +pub(crate) fn generate_resource_inserts, W: Write>( + f: &mut W, + project_dir: &P, + variable_name: &str, + resources: Vec<(PathBuf, Metadata)>, +) -> io::Result<()> { + for resource in &resources { + generate_resource_insert(f, project_dir, variable_name, resource)?; + } + Ok(()) +} + +#[allow(clippy::unnecessary_debug_formatting)] +pub(crate) fn generate_resource_insert, W: Write>( + f: &mut W, + project_dir: &P, + variable_name: &str, + resource: &(PathBuf, Metadata), +) -> io::Result<()> { + let (path, metadata) = resource; + let abs_path = path.canonicalize()?; + let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap(); + + let modified = if let Ok(Ok(modified)) = metadata + .modified() + .map(|x| x.duration_since(SystemTime::UNIX_EPOCH)) + { + modified.as_secs() + } else { + 0 + }; + let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream(); + writeln!( + f, + "{}.insert({:?},n(i!({:?}),{:?},{:?}));", + variable_name, &key_path, &abs_path, modified, &mime_type, + ) +} + +pub(crate) fn generate_function_header( + f: &mut F, + fn_name: &str, + crate_name: &str, +) -> io::Result<()> { + writeln!( + f, + "#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{", + ) +} + +pub(crate) fn generate_function_end(f: &mut F) -> io::Result<()> { + writeln!(f, "}}") +} + +pub(crate) fn generate_uses(f: &mut F, crate_name: &str) -> io::Result<()> { + writeln!( + f, + "use ::{crate_name}::resource::new_resource as n; +use ::std::include_bytes as i;", + ) +} + +pub(crate) fn generate_variable_header(f: &mut F, variable_name: &str) -> io::Result<()> { + writeln!( + f, + "let mut {variable_name} = ::std::collections::HashMap::new();", + ) +} + +pub(crate) fn generate_variable_return(f: &mut F, variable_name: &str) -> io::Result<()> { + writeln!(f, "{variable_name}") +} diff --git a/helpers/pagetop-statics/src/resource_dir.rs b/helpers/pagetop-statics/src/resource_dir.rs new file mode 100644 index 0000000..805e1ed --- /dev/null +++ b/helpers/pagetop-statics/src/resource_dir.rs @@ -0,0 +1,118 @@ +use super::sets::{generate_resources_sets, SplitByCount}; +use std::{ + env, io, + path::{Path, PathBuf}, +}; + +/// Generate resources for `resource_dir`. +/// +/// ```rust,no_run +/// // Generate resources for ./tests dir with file name generated.rs +/// // stored in path defined by OUT_DIR environment variable. +/// // Function name is 'generate' +/// use pagetop_statics::resource_dir; +/// +/// resource_dir("./tests").build().unwrap(); +/// ``` +pub fn resource_dir>(resource_dir: P) -> ResourceDir { + ResourceDir { + resource_dir: resource_dir.as_ref().into(), + ..Default::default() + } +} + +/// Resource dir. +/// +/// A builder structure allows to change default settings for: +/// - file filter +/// - generated file name +/// - generated function name +#[derive(Default)] +pub struct ResourceDir { + pub(crate) resource_dir: PathBuf, + pub(crate) filter: Option bool>, + pub(crate) generated_filename: Option, + pub(crate) generated_fn: Option, + pub(crate) module_name: Option, + pub(crate) count_per_module: Option, +} + +pub const DEFAULT_MODULE_NAME: &str = "sets"; +pub const DEFAULT_COUNT_PER_MODULE: usize = 256; + +impl ResourceDir { + /// Generates resources for current configuration. + pub fn build(self) -> io::Result<()> { + self.internal_build("pagetop") + } + + /// Generates resources for testing current configuration. + #[allow(dead_code)] + pub(crate) fn build_test(self) -> io::Result<()> { + self.internal_build("pagetop_statics") + } + + fn internal_build(self, crate_name: &str) -> io::Result<()> { + let generated_filename = self.generated_filename.unwrap_or_else(|| { + let out_dir = env::var("OUT_DIR").unwrap(); + + Path::new(&out_dir).join("generated.rs") + }); + let generated_fn = self.generated_fn.unwrap_or_else(|| "generate".into()); + + let module_name = self + .module_name + .unwrap_or_else(|| format!("{}_{}", &generated_fn, DEFAULT_MODULE_NAME)); + + let count_per_module = self.count_per_module.unwrap_or(DEFAULT_COUNT_PER_MODULE); + + generate_resources_sets( + &self.resource_dir, + self.filter, + &generated_filename, + module_name.as_str(), + &generated_fn, + &mut SplitByCount::new(count_per_module), + crate_name, + ) + } + + /// Sets the file filter. + pub fn with_filter(&mut self, filter: fn(p: &Path) -> bool) -> &mut Self { + self.filter = Some(filter); + self + } + + /// Sets the generated filename. + pub fn with_generated_filename>(&mut self, generated_filename: P) -> &mut Self { + self.generated_filename = Some(generated_filename.as_ref().into()); + self + } + + /// Sets the generated function name. + pub fn with_generated_fn(&mut self, generated_fn: S) -> &mut Self + where + S: Into, + { + self.generated_fn = Some(generated_fn.into()); + self + } + + /// Sets the generated module name. + /// + /// Default value is based on generated function name and the suffix "sets". + /// Generated module would be overriden by each call. + pub fn with_module_name(&mut self, module_name: S) -> &mut Self + where + S: Into, + { + self.module_name = Some(module_name.into()); + self + } + + /// Sets maximal count of files per module. + pub fn with_count_per_module(&mut self, count_per_module: usize) -> &mut Self { + self.count_per_module = Some(count_per_module); + self + } +} diff --git a/helpers/pagetop-statics/src/resource_files.rs b/helpers/pagetop-statics/src/resource_files.rs new file mode 100644 index 0000000..b487bca --- /dev/null +++ b/helpers/pagetop-statics/src/resource_files.rs @@ -0,0 +1,396 @@ +use super::resource::Resource; +use actix_web::{ + dev::{ + always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, + ServiceRequest, ServiceResponse, + }, + error::Error, + guard::{Guard, GuardContext}, + http::{ + header::{self, ContentType}, + Method, StatusCode, + }, + HttpMessage, HttpRequest, HttpResponse, ResponseError, +}; +use derive_more::{Deref, Display, Error}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; +use std::{collections::HashMap, ops::Deref, rc::Rc}; + +/// Static resource files handling +/// +/// `ResourceFiles` service must be registered with `App::service` method. +/// +/// ```rust +/// use std::collections::HashMap; +/// +/// use actix_web::App; +/// +/// fn main() { +/// // serve root directory with default options: +/// // - resolve index.html +/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); +/// let app = App::new() +/// .service(pagetop_statics::ResourceFiles::new("/", files)); +/// // or subpath with additional option to not resolve index.html +/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); +/// let app = App::new() +/// .service(pagetop_statics::ResourceFiles::new("/imgs", files) +/// .do_not_resolve_defaults()); +/// } +/// ``` +#[allow(clippy::needless_doctest_main)] +pub struct ResourceFiles { + not_resolve_defaults: bool, + use_guard: bool, + not_found_resolves_to: Option, + inner: Rc, +} + +pub struct ResourceFilesInner { + path: String, + files: HashMap<&'static str, Resource>, +} + +const INDEX_HTML: &str = "index.html"; + +impl ResourceFiles { + pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self { + let inner = ResourceFilesInner { + path: path.into(), + files, + }; + Self { + inner: Rc::new(inner), + not_resolve_defaults: false, + not_found_resolves_to: None, + use_guard: false, + } + } + + /// By default trying to resolve '.../' to '.../index.html' if it exists. + /// Turn off this resolution by calling this function. + pub fn do_not_resolve_defaults(mut self) -> Self { + self.not_resolve_defaults = true; + self + } + + /// Resolves not found references to this path. + /// + /// This can be useful for angular-like applications. + pub fn resolve_not_found_to(mut self, path: S) -> Self { + self.not_found_resolves_to = Some(path.to_string()); + self + } + + /// Resolves not found references to root path. + /// + /// This can be useful for angular-like applications. + pub fn resolve_not_found_to_root(self) -> Self { + self.resolve_not_found_to(INDEX_HTML) + } + + /// If this is called, we will use an [actix_web::guard::Guard] to check if this request should be handled. + /// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s. + /// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used. + /// + /// Can be useful if you want to share files on a (sub)path that's also used by a different route handler. + pub fn skip_handler_when_not_found(mut self) -> Self { + self.use_guard = true; + self + } + + fn select_guard(&self) -> Box { + if self.not_resolve_defaults { + Box::new(NotResolveDefaultsGuard::from(self)) + } else { + Box::new(ResolveDefaultsGuard::from(self)) + } + } +} + +impl Deref for ResourceFiles { + type Target = ResourceFilesInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +struct NotResolveDefaultsGuard { + inner: Rc, +} + +impl Guard for NotResolveDefaultsGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + self.inner + .files + .contains_key(ctx.head().uri.path().trim_start_matches('/')) + } +} + +impl From<&ResourceFiles> for NotResolveDefaultsGuard { + fn from(files: &ResourceFiles) -> Self { + Self { + inner: files.inner.clone(), + } + } +} + +struct ResolveDefaultsGuard { + inner: Rc, +} + +impl Guard for ResolveDefaultsGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + let path = ctx.head().uri.path().trim_start_matches('/'); + self.inner.files.contains_key(path) + || ((path.is_empty() || path.ends_with('/')) + && self + .inner + .files + .contains_key((path.to_string() + INDEX_HTML).as_str())) + } +} + +impl From<&ResourceFiles> for ResolveDefaultsGuard { + fn from(files: &ResourceFiles) -> Self { + Self { + inner: files.inner.clone(), + } + } +} + +impl HttpServiceFactory for ResourceFiles { + fn register(self, config: &mut AppService) { + let prefix = self.path.trim_start_matches('/'); + let rdef = if config.is_root() { + ResourceDef::root_prefix(prefix) + } else { + ResourceDef::prefix(prefix) + }; + let guards = if self.use_guard && self.not_found_resolves_to.is_none() { + Some(vec![self.select_guard()]) + } else { + None + }; + config.register_service(rdef, guards, self, None); + } +} + +impl ServiceFactory for ResourceFiles { + type Config = (); + type Response = ServiceResponse; + type Error = Error; + type Service = ResourceFilesService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + ok(ResourceFilesService { + resolve_defaults: !self.not_resolve_defaults, + not_found_resolves_to: self.not_found_resolves_to.clone(), + inner: self.inner.clone(), + }) + .boxed_local() + } +} + +#[derive(Deref)] +pub struct ResourceFilesService { + resolve_defaults: bool, + not_found_resolves_to: Option, + #[deref] + inner: Rc, +} + +impl Service for ResourceFilesService { + type Response = ServiceResponse; + type Error = Error; + type Future = Ready>; + + always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + match *req.method() { + Method::HEAD | Method::GET => (), + _ => { + return ok(ServiceResponse::new( + req.into_parts().0, + HttpResponse::MethodNotAllowed() + .insert_header(ContentType::plaintext()) + .insert_header((header::ALLOW, "GET, HEAD")) + .body("This resource only supports GET and HEAD."), + )); + } + } + + let req_path = req.match_info().unprocessed(); + let mut item = self.files.get(req_path); + + if item.is_none() + && self.resolve_defaults + && (req_path.is_empty() || req_path.ends_with('/')) + { + let index_req_path = req_path.to_string() + INDEX_HTML; + item = self.files.get(index_req_path.trim_start_matches('/')); + } + + let (req, response) = if item.is_some() { + let (req, _) = req.into_parts(); + let response = respond_to(&req, item); + (req, response) + } else { + let real_path = match get_pathbuf(req_path) { + Ok(item) => item, + Err(e) => return ok(req.error_response(e)), + }; + + let (req, _) = req.into_parts(); + + let mut item = self.files.get(real_path.as_str()); + + if item.is_none() && self.not_found_resolves_to.is_some() { + let not_found_path = self.not_found_resolves_to.as_ref().unwrap(); + item = self.files.get(not_found_path.as_str()); + } + + let response = respond_to(&req, item); + (req, response) + }; + + ok(ServiceResponse::new(req, response)) + } +} + +fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse { + if let Some(file) = item { + let etag = Some(header::EntityTag::new_strong(format!( + "{:x}:{:x}", + file.data.len(), + file.modified + ))); + + let precondition_failed = !any_match(etag.as_ref(), req); + + let not_modified = !none_match(etag.as_ref(), req); + + let mut resp = HttpResponse::build(StatusCode::OK); + resp.insert_header((header::CONTENT_TYPE, file.mime_type)); + + if let Some(etag) = etag { + resp.insert_header(header::ETag(etag)); + } + + if precondition_failed { + return resp.status(StatusCode::PRECONDITION_FAILED).finish(); + } else if not_modified { + return resp.status(StatusCode::NOT_MODIFIED).finish(); + } + + resp.body(file.data) + } else { + HttpResponse::NotFound().body("Not found") + } +} + +/// Returns true if `req` has no `If-Match` header or one which matches `etag`. +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + None | Some(header::IfMatch::Any) => true, + Some(header::IfMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.strong_eq(some_etag) { + return true; + } + } + } + false + } + } +} + +/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Some(header::IfNoneMatch::Any) => false, + Some(header::IfNoneMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } + } + true + } + None => true, + } +} + +/// Error type representing invalid characters in a URI path segment. +/// +/// This enum is used to report specific formatting errors in individual segments of a URI path, +/// such as starting, ending, or containing disallowed characters. Each variant wraps the offending +/// character that caused the error. +#[derive(Debug, PartialEq, Display, Error)] +pub enum UriSegmentError { + /// The segment started with the wrapped invalid character. + #[display(fmt = "The segment started with the wrapped invalid character")] + BadStart(#[error(not(source))] char), + + /// The segment contained the wrapped invalid character. + #[display(fmt = "The segment contained the wrapped invalid character")] + BadChar(#[error(not(source))] char), + + /// The segment ended with the wrapped invalid character. + #[display(fmt = "The segment ended with the wrapped invalid character")] + BadEnd(#[error(not(source))] char), +} + +#[cfg(test)] +mod tests_error_impl { + use super::*; + + fn assert_send_and_sync() {} + + #[test] + fn test_error_impl() { + // ensure backwards compatibility when migrating away from failure + assert_send_and_sync::(); + } +} + +/// Return `BadRequest` for `UriSegmentError` +impl ResponseError for UriSegmentError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST) + } +} + +fn get_pathbuf(path: &str) -> Result { + let mut buf = Vec::new(); + for segment in path.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(buf.join("/")) +} diff --git a/helpers/pagetop-statics/src/sets.rs b/helpers/pagetop-statics/src/sets.rs new file mode 100644 index 0000000..1d9299d --- /dev/null +++ b/helpers/pagetop-statics/src/sets.rs @@ -0,0 +1,184 @@ +use std::{ + fs::{self, File, Metadata}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use super::resource::{ + collect_resources, generate_function_end, generate_function_header, generate_resource_insert, + generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME, +}; + +/// Defines the split strategie. +pub trait SetSplitStrategie { + /// Register next file from resources. + fn register(&mut self, path: &Path, metadata: &Metadata); + /// Determine, should we split modules now. + fn should_split(&self) -> bool; + /// Resets internal counters after split. + fn reset(&mut self); +} + +/// Split modules by files count. +pub struct SplitByCount { + current: usize, + max: usize, +} + +impl SplitByCount { + pub fn new(max: usize) -> Self { + Self { current: 0, max } + } +} + +impl SetSplitStrategie for SplitByCount { + fn register(&mut self, _path: &Path, _metadata: &Metadata) { + self.current += 1; + } + + fn should_split(&self) -> bool { + self.current >= self.max + } + + fn reset(&mut self) { + self.current = 0; + } +} + +/// Generate resources for `project_dir` using `filter` +/// breaking them into separate modules using `set_split_strategie` (recommended for large > 128 Mb setups). +/// +/// Result saved in module named `module_name`. It exports +/// only one function named `fn_name`. It is then exported from +/// `generated_filename`. `generated_filename` is also used to determine +/// the parent directory for the module. +/// +/// in `build.rs`: +/// ```rust +/// +/// use std::{env, path::Path}; +/// use pagetop_statics::sets::{generate_resources_sets, SplitByCount}; +/// +/// fn main() { +/// let out_dir = env::var("OUT_DIR").unwrap(); +/// let generated_filename = Path::new(&out_dir).join("generated_sets.rs"); +/// generate_resources_sets( +/// "./tests", +/// None, +/// generated_filename, +/// "sets", +/// "generate", +/// &mut SplitByCount::new(2), +/// "pagetop_statics", +/// ) +/// .unwrap(); +/// } +/// ``` +/// +/// in `main.rs`: +/// ```rust +/// include!(concat!(env!("OUT_DIR"), "/generated_sets.rs")); +/// +/// fn main() { +/// let generated_file = generate(); +/// +/// assert_eq!(generated_file.len(), 4); +/// +/// } +/// ``` +pub fn generate_resources_sets( + project_dir: P, + filter: Option bool>, + generated_filename: G, + module_name: &str, + fn_name: &str, + set_split_strategie: &mut S, + crate_name: &str, +) -> io::Result<()> +where + P: AsRef, + G: AsRef, + S: SetSplitStrategie, +{ + let resources = collect_resources(&project_dir, filter)?; + + let mut generated_file = File::create(&generated_filename)?; + + let module_dir = generated_filename.as_ref().parent().map_or_else( + || PathBuf::from(module_name), + |parent| parent.join(module_name), + ); + fs::create_dir_all(&module_dir)?; + + let mut module_file = File::create(module_dir.join("mod.rs"))?; + + generate_uses(&mut module_file, crate_name)?; + writeln!( + module_file, + " +use ::{crate_name}::StaticResource; +use ::std::collections::HashMap;" + )?; + + let mut modules_count = 1; + + let mut set_file = create_set_module_file(&module_dir, modules_count)?; + let mut should_split = set_split_strategie.should_split(); + + for resource in &resources { + let (path, metadata) = &resource; + if should_split { + set_split_strategie.reset(); + modules_count += 1; + generate_function_end(&mut set_file)?; + set_file = create_set_module_file(&module_dir, modules_count)?; + } + set_split_strategie.register(path, metadata); + should_split = set_split_strategie.should_split(); + + generate_resource_insert(&mut set_file, &project_dir, DEFAULT_VARIABLE_NAME, resource)?; + } + + generate_function_end(&mut set_file)?; + + for module_index in 1..=modules_count { + writeln!(module_file, "mod set_{module_index};")?; + } + + generate_function_header(&mut module_file, fn_name, crate_name)?; + + generate_variable_header(&mut module_file, DEFAULT_VARIABLE_NAME)?; + + for module_index in 1..=modules_count { + writeln!( + module_file, + "set_{module_index}::generate(&mut {DEFAULT_VARIABLE_NAME});", + )?; + } + + generate_variable_return(&mut module_file, DEFAULT_VARIABLE_NAME)?; + + generate_function_end(&mut module_file)?; + + writeln!( + generated_file, + "mod {module_name}; +pub use {module_name}::{fn_name};", + )?; + + Ok(()) +} + +fn create_set_module_file(module_dir: &Path, module_index: usize) -> io::Result { + let mut set_module = File::create(module_dir.join(format!("set_{module_index}.rs")))?; + + writeln!( + set_module, + "#[allow(clippy::wildcard_imports)] +use super::*; +#[allow(clippy::unreadable_literal)] +pub(crate) fn generate({DEFAULT_VARIABLE_NAME}: &mut HashMap<&'static str, StaticResource>) {{", + )?; + + Ok(set_module) +} diff --git a/helpers/pagetop-statics/tests/file1.txt b/helpers/pagetop-statics/tests/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/helpers/pagetop-statics/tests/file2.txt b/helpers/pagetop-statics/tests/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/helpers/pagetop-statics/tests/file3.info b/helpers/pagetop-statics/tests/file3.info new file mode 100644 index 0000000..e69de29 diff --git a/helpers/pagetop-statics/tests/index.html b/helpers/pagetop-statics/tests/index.html new file mode 100644 index 0000000..36f505f --- /dev/null +++ b/helpers/pagetop-statics/tests/index.html @@ -0,0 +1,10 @@ + + + + + + Document + + + + diff --git a/src/lib.rs b/src/lib.rs index e0da361..bbc4530 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,21 +98,23 @@ use std::ops::Deref; 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). pub struct StaticResources { - bundle: HashMap<&'static str, static_files::Resource>, + bundle: HashMap<&'static str, StaticResource>, } impl StaticResources { /// Crea un contenedor para un conjunto de recursos generado por `build.rs` (consultar /// [`pagetop_build`](https://docs.rs/pagetop-build)). - pub fn new(bundle: HashMap<&'static str, static_files::Resource>) -> Self { + pub fn new(bundle: HashMap<&'static str, StaticResource>) -> Self { Self { bundle } } } impl Deref for StaticResources { - type Target = HashMap<&'static str, static_files::Resource>; + type Target = HashMap<&'static str, StaticResource>; fn deref(&self) -> &Self::Target { &self.bundle diff --git a/src/service.rs b/src/service.rs index 89ba496..47f1420 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,9 +8,9 @@ pub use actix_web::dev::ServiceRequest as Request; pub use actix_web::dev::ServiceResponse as Response; pub use actix_web::{cookie, http, rt, web}; pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer}; - pub use actix_web_files::Files as ActixFiles; -pub use actix_web_static_files::ResourceFiles; + +pub use pagetop_statics::ResourceFiles; #[doc(hidden)] pub use actix_web::test;