✨ Añade librería para gestionar recursos estáticos (#1)
Reviewed-on: #1 Co-authored-by: Manuel Cillero <manuel@cillero.es> Co-committed-by: Manuel Cillero <manuel@cillero.es>
This commit is contained in:
parent
8ed0e6621a
commit
47ea9d9f7d
20 changed files with 1371 additions and 41 deletions
48
Cargo.lock
generated
48
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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 <manuel@cillero.es>"]
|
||||
|
||||
[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" }
|
||||
|
|
|
@ -17,4 +17,4 @@ authors.workspace = true
|
|||
|
||||
[dependencies]
|
||||
grass = "0.13.4"
|
||||
static-files.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
|
|
@ -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;
|
||||
|
|
33
helpers/pagetop-statics/Cargo.toml
Normal file
33
helpers/pagetop-statics/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "pagetop-statics"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Librería para automatizar la recopilación de recursos estáticos en PageTop.
|
||||
"""
|
||||
categories = ["development-tools::build-utils"]
|
||||
keywords = ["pagetop", "build", "static", "resources", "file"]
|
||||
|
||||
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"
|
201
helpers/pagetop-statics/LICENSE-APACHE
Normal file
201
helpers/pagetop-statics/LICENSE-APACHE
Normal file
|
@ -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.
|
21
helpers/pagetop-statics/LICENSE-MIT
Normal file
21
helpers/pagetop-statics/LICENSE-MIT
Normal file
|
@ -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.
|
50
helpers/pagetop-statics/README.md
Normal file
50
helpers/pagetop-statics/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Statics</h1>
|
||||
|
||||
<p>Librería para automatizar la recopilación de recursos estáticos en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](#-licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## 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.
|
43
helpers/pagetop-statics/build.rs
Normal file
43
helpers/pagetop-statics/build.rs
Normal file
|
@ -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(())
|
||||
}
|
34
helpers/pagetop-statics/src/lib.rs
Normal file
34
helpers/pagetop-statics/src/lib.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
//! <div align="center">
|
||||
//!
|
||||
//! <h1>PageTop Statics</h1>
|
||||
//!
|
||||
//! <p>Librería para automatizar la recopilación de recursos estáticos en <strong>PageTop</strong>.</p>
|
||||
//!
|
||||
//! [](#-licencia)
|
||||
//!
|
||||
//! </div>
|
||||
//!
|
||||
//! ## 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;
|
249
helpers/pagetop-statics/src/resource.rs
Normal file
249
helpers/pagetop-statics/src/resource.rs
Normal file
|
@ -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<P: AsRef<Path>, G: AsRef<Path>>(
|
||||
project_dir: P,
|
||||
filter: Option<fn(p: &Path) -> 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<P: AsRef<Path>, G: AsRef<Path>>(
|
||||
project_dir: P,
|
||||
filter: Option<fn(p: &Path) -> 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<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
collect_resources_nested(path, filter)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sort")]
|
||||
pub(crate) fn collect_resources<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
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<P: AsRef<Path>>(
|
||||
path: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
) -> io::Result<Vec<(PathBuf, Metadata)>> {
|
||||
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<P: AsRef<Path>, 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<P: AsRef<Path>, 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: Write>(
|
||||
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: Write>(f: &mut F) -> io::Result<()> {
|
||||
writeln!(f, "}}")
|
||||
}
|
||||
|
||||
pub(crate) fn generate_uses<F: Write>(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: Write>(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: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
|
||||
writeln!(f, "{variable_name}")
|
||||
}
|
118
helpers/pagetop-statics/src/resource_dir.rs
Normal file
118
helpers/pagetop-statics/src/resource_dir.rs
Normal file
|
@ -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<P: AsRef<Path>>(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<fn(p: &Path) -> bool>,
|
||||
pub(crate) generated_filename: Option<PathBuf>,
|
||||
pub(crate) generated_fn: Option<String>,
|
||||
pub(crate) module_name: Option<String>,
|
||||
pub(crate) count_per_module: Option<usize>,
|
||||
}
|
||||
|
||||
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<P: AsRef<Path>>(&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<S>(&mut self, generated_fn: S) -> &mut Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
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<S>(&mut self, module_name: S) -> &mut Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
396
helpers/pagetop-statics/src/resource_files.rs
Normal file
396
helpers/pagetop-statics/src/resource_files.rs
Normal file
|
@ -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<String>,
|
||||
inner: Rc<ResourceFilesInner>,
|
||||
}
|
||||
|
||||
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<S: ToString>(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<dyn Guard> {
|
||||
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<ResourceFilesInner>,
|
||||
}
|
||||
|
||||
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<ResourceFilesInner>,
|
||||
}
|
||||
|
||||
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<ServiceRequest> for ResourceFiles {
|
||||
type Config = ();
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Service = ResourceFilesService;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
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<String>,
|
||||
#[deref]
|
||||
inner: Rc<ResourceFilesInner>,
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for ResourceFilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
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::<header::IfMatch>() {
|
||||
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::<header::IfNoneMatch>() {
|
||||
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<T: Send + Sync + 'static>() {}
|
||||
|
||||
#[test]
|
||||
fn test_error_impl() {
|
||||
// ensure backwards compatibility when migrating away from failure
|
||||
assert_send_and_sync::<UriSegmentError>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
impl ResponseError for UriSegmentError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
|
||||
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("/"))
|
||||
}
|
184
helpers/pagetop-statics/src/sets.rs
Normal file
184
helpers/pagetop-statics/src/sets.rs
Normal file
|
@ -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<P, G, S>(
|
||||
project_dir: P,
|
||||
filter: Option<fn(p: &Path) -> bool>,
|
||||
generated_filename: G,
|
||||
module_name: &str,
|
||||
fn_name: &str,
|
||||
set_split_strategie: &mut S,
|
||||
crate_name: &str,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
G: AsRef<Path>,
|
||||
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<File> {
|
||||
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)
|
||||
}
|
0
helpers/pagetop-statics/tests/file1.txt
Normal file
0
helpers/pagetop-statics/tests/file1.txt
Normal file
0
helpers/pagetop-statics/tests/file2.txt
Normal file
0
helpers/pagetop-statics/tests/file2.txt
Normal file
0
helpers/pagetop-statics/tests/file3.info
Normal file
0
helpers/pagetop-statics/tests/file3.info
Normal file
10
helpers/pagetop-statics/tests/index.html
Normal file
10
helpers/pagetop-statics/tests/index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue