~repos /rust-embed
git clone https://pyrossh.dev/repos/rust-embed.git
rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.
48814079
—
pyros2097 10 years ago
Initial commit
- .gitignore +5 -0
- .pants-ignore +3 -0
- Cargo.toml +113 -0
- LICENSE +1 -1
- README.md +177 -0
- changelog.md +398 -0
- examples/actix.rs +31 -0
- examples/axum-spa/README.md +1 -0
- examples/axum-spa/assets/index.html +15 -0
- examples/axum-spa/assets/js/script.js +3 -0
- examples/axum-spa/main.rs +57 -0
- examples/axum.rs +65 -0
- examples/basic.rs +10 -0
- examples/poem.rs +65 -0
- examples/public/images/doc.txt +1 -0
- examples/public/images/flower.jpg +0 -0
- examples/public/images/llama.png +0 -0
- examples/public/index.html +27 -0
- examples/public/main.css +14 -0
- examples/public/main.js +1 -0
- examples/public/symlinks/main.js +1 -0
- examples/rocket.rs +38 -0
- examples/salvo.rs +48 -0
- examples/warp.rs +32 -0
- impl/Cargo.toml +40 -0
- impl/license +1 -0
- impl/readme.md +3 -0
- impl/src/lib.rs +421 -0
- rustfmt.toml +5 -0
- src/lib.rs +80 -0
- tests/allow_missing.rs +15 -0
- tests/custom_crate_path.rs +20 -0
- tests/include_exclude.rs +68 -0
- tests/interpolated_path.rs +37 -0
- tests/lib.rs +58 -0
- tests/metadata.rs +56 -0
- tests/metadata_only.rs +12 -0
- tests/mime_guess.rs +35 -0
- tests/path_traversal_attack.rs +27 -0
- tests/prefix.rs +24 -0
- utils/Cargo.toml +26 -0
- utils/license +1 -0
- utils/readme.md +3 -0
- utils/src/lib.rs +185 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
target
|
|
2
|
+
Cargo.lock
|
|
3
|
+
|
|
4
|
+
.vscode
|
|
5
|
+
.idea
|
.pants-ignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignore": [{ "id": "sonatype-2021-4646", "reason": "We are handling it this case. Sonatype doesn't recognize that." }]
|
|
3
|
+
}
|
Cargo.toml
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rust-embed"
|
|
3
|
+
version = "8.9.0"
|
|
4
|
+
description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
documentation = "https://docs.rs/rust-embed"
|
|
7
|
+
repository = "https://pyrossh.dev/repos/rust-embed"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
keywords = ["http", "rocket", "static", "web", "server"]
|
|
10
|
+
categories = ["web-programming", "filesystem"]
|
|
11
|
+
authors = ["pyrossh"]
|
|
12
|
+
edition = "2018"
|
|
13
|
+
rust-version = "1.70.0"
|
|
14
|
+
|
|
15
|
+
[[example]]
|
|
16
|
+
name = "warp"
|
|
17
|
+
path = "examples/warp.rs"
|
|
18
|
+
required-features = ["warp-ex"]
|
|
19
|
+
|
|
20
|
+
[[example]]
|
|
21
|
+
name = "actix"
|
|
22
|
+
path = "examples/actix.rs"
|
|
23
|
+
required-features = ["actix"]
|
|
24
|
+
|
|
25
|
+
[[example]]
|
|
26
|
+
name = "rocket"
|
|
27
|
+
path = "examples/rocket.rs"
|
|
28
|
+
required-features = ["rocket"]
|
|
29
|
+
|
|
30
|
+
[[example]]
|
|
31
|
+
name = "axum"
|
|
32
|
+
path = "examples/axum.rs"
|
|
33
|
+
required-features = ["axum-ex"]
|
|
34
|
+
|
|
35
|
+
[[example]]
|
|
36
|
+
name = "axum-spa"
|
|
37
|
+
path = "examples/axum-spa/main.rs"
|
|
38
|
+
required-features = ["axum-ex"]
|
|
39
|
+
|
|
40
|
+
[[example]]
|
|
41
|
+
name = "poem"
|
|
42
|
+
path = "examples/poem.rs"
|
|
43
|
+
required-features = ["poem-ex"]
|
|
44
|
+
|
|
45
|
+
[[example]]
|
|
46
|
+
name = "salvo"
|
|
47
|
+
path = "examples/salvo.rs"
|
|
48
|
+
required-features = ["salvo-ex"]
|
|
49
|
+
|
|
50
|
+
[[test]]
|
|
51
|
+
name = "interpolated_path"
|
|
52
|
+
path = "tests/interpolated_path.rs"
|
|
53
|
+
required-features = ["interpolate-folder-path"]
|
|
54
|
+
|
|
55
|
+
[[test]]
|
|
56
|
+
name = "include_exclude"
|
|
57
|
+
path = "tests/include_exclude.rs"
|
|
58
|
+
required-features = ["include-exclude"]
|
|
59
|
+
|
|
60
|
+
[[test]]
|
|
61
|
+
name = "mime_guess"
|
|
62
|
+
path = "tests/mime_guess.rs"
|
|
63
|
+
required-features = ["mime-guess"]
|
|
64
|
+
|
|
65
|
+
[dependencies]
|
|
66
|
+
walkdir = "2.3.2"
|
|
67
|
+
rust-embed-impl = { version = "8.9.0", path = "impl" }
|
|
68
|
+
rust-embed-utils = { version = "8.9.0", path = "utils" }
|
|
69
|
+
|
|
70
|
+
include-flate = { version = "0.3", optional = true }
|
|
71
|
+
actix-web = { version = "4", optional = true }
|
|
72
|
+
mime_guess = { version = "2.0.5", optional = true }
|
|
73
|
+
hex = { version = "0.4.3", optional = true }
|
|
74
|
+
tokio = { version = "1.0", optional = true, features = [
|
|
75
|
+
"macros",
|
|
76
|
+
"rt-multi-thread",
|
|
77
|
+
] }
|
|
78
|
+
warp = { version = "0.3", default-features = false, optional = true }
|
|
79
|
+
rocket = { version = "0.5.0-rc.2", default-features = false, optional = true }
|
|
80
|
+
axum = { version = "0.8", default-features = false, features = [
|
|
81
|
+
"http1",
|
|
82
|
+
"tokio",
|
|
83
|
+
], optional = true }
|
|
84
|
+
poem = { version = "1.3.30", default-features = false, features = [
|
|
85
|
+
"server",
|
|
86
|
+
], optional = true }
|
|
87
|
+
salvo = { version = "0.16", default-features = false, optional = true }
|
|
88
|
+
|
|
89
|
+
[dev-dependencies]
|
|
90
|
+
sha2 = "0.10"
|
|
91
|
+
|
|
92
|
+
[features]
|
|
93
|
+
debug-embed = ["rust-embed-impl/debug-embed", "rust-embed-utils/debug-embed"]
|
|
94
|
+
interpolate-folder-path = ["rust-embed-impl/interpolate-folder-path"]
|
|
95
|
+
compression = ["rust-embed-impl/compression", "include-flate"]
|
|
96
|
+
mime-guess = ["rust-embed-impl/mime-guess", "rust-embed-utils/mime-guess"]
|
|
97
|
+
include-exclude = [
|
|
98
|
+
"rust-embed-impl/include-exclude",
|
|
99
|
+
"rust-embed-utils/include-exclude",
|
|
100
|
+
]
|
|
101
|
+
actix = ["actix-web", "mime_guess"]
|
|
102
|
+
warp-ex = ["warp", "tokio", "mime_guess"]
|
|
103
|
+
axum-ex = ["axum", "tokio", "mime_guess"]
|
|
104
|
+
poem-ex = ["poem", "tokio", "mime_guess", "hex"]
|
|
105
|
+
salvo-ex = ["salvo", "tokio", "mime_guess", "hex"]
|
|
106
|
+
deterministic-timestamps = ["rust-embed-impl/deterministic-timestamps"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
[badges]
|
|
110
|
+
maintenance = { status = "passively-maintained" }
|
|
111
|
+
|
|
112
|
+
[workspace]
|
|
113
|
+
members = ["impl", "utils"]
|
LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2018 pyros2097
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# rust-embed
|
|
2
|
+
|
|
3
|
+
Rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.
|
|
4
|
+
|
|
5
|
+
You can use this to embed your css, js and images into a single executable which can be deployed to your servers. Also it makes it easy to build a very small docker image for you to deploy.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
[dependencies]
|
|
11
|
+
rust-embed="8.9.0"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
You need to add the custom derive macro RustEmbed to your struct with an attribute `folder` which is the path to your static folder.
|
|
17
|
+
|
|
18
|
+
The path resolution works as follows:
|
|
19
|
+
|
|
20
|
+
- In `debug` and when `debug-embed` feature is not enabled, the folder path is resolved relative to where the binary is run from.
|
|
21
|
+
- In `release` or when `debug-embed` feature is enabled, the folder path is resolved relative to where `Cargo.toml` is.
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
#[derive(Embed)]
|
|
25
|
+
#[folder = "examples/public/"]
|
|
26
|
+
struct Asset;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The macro will generate the following code:
|
|
30
|
+
|
|
31
|
+
```rust
|
|
32
|
+
impl Asset {
|
|
33
|
+
pub fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
|
|
34
|
+
...
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn iter() -> impl Iterator<Item = Cow<'static, str>> {
|
|
38
|
+
...
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
impl RustEmbed for Asset {
|
|
42
|
+
fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
|
|
43
|
+
...
|
|
44
|
+
}
|
|
45
|
+
fn iter() -> impl Iterator<Item = Cow<'static, str>> {
|
|
46
|
+
...
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Where EmbeddedFile contains these fields,
|
|
51
|
+
pub struct EmbeddedFile {
|
|
52
|
+
pub data: Cow<'static, [u8]>,
|
|
53
|
+
pub metadata: Metadata,
|
|
54
|
+
}
|
|
55
|
+
pub struct Metadata {
|
|
56
|
+
hash: [u8; 32],
|
|
57
|
+
last_modified: Option<u64>,
|
|
58
|
+
created: Option<u64>,
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Methods
|
|
63
|
+
* `get(file_path: &str) -> Option<rust_embed::EmbeddedFile>`
|
|
64
|
+
|
|
65
|
+
Given a relative path from the assets folder returns the `EmbeddedFile` if found.
|
|
66
|
+
If the feature `debug-embed` is enabled or the binary compiled in release mode the bytes have been embeded in the binary and a `Option<rust_embed::EmbeddedFile>` is returned.
|
|
67
|
+
Otherwise the bytes are read from the file system on each call and a `Option<rust_embed::EmbeddedFile>` is returned.
|
|
68
|
+
|
|
69
|
+
* `iter()`
|
|
70
|
+
|
|
71
|
+
Iterates the files in this assets folder.
|
|
72
|
+
If the feature `debug-embed` is enabled or the binary compiled in release mode a static array to the list of relative paths to the files is returned.
|
|
73
|
+
Otherwise the files are listed from the file system on each call.
|
|
74
|
+
|
|
75
|
+
## Attributes
|
|
76
|
+
* `prefix`
|
|
77
|
+
|
|
78
|
+
You can add `#[prefix = "my_prefix/"]` to the `RustEmbed` struct to add a prefix
|
|
79
|
+
to all of the file paths. This prefix will be required on `get` calls, and will
|
|
80
|
+
be included in the file paths returned by `iter`.
|
|
81
|
+
|
|
82
|
+
* `metadata_only`
|
|
83
|
+
|
|
84
|
+
You can add `#[metadata_only = true]` to the `RustEmbed` struct to exclude file contents from the
|
|
85
|
+
binary. Only file paths and metadata will be embedded.
|
|
86
|
+
|
|
87
|
+
* `allow_missing`
|
|
88
|
+
|
|
89
|
+
You can add `#[allow_missing = true]` to the `RustEmbed` struct to allow the embedded folder to be missing.
|
|
90
|
+
In that case, RustEmbed will be empty.
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
|
|
94
|
+
* `debug-embed`: Always embed the files in the binary, even in debug mode.
|
|
95
|
+
* `compression`: Compress each file when embedding into the binary. Compression is done via [include-flate](https://crates.io/crates/include-flate).
|
|
96
|
+
* `deterministic-timestamps`: Overwrite embedded files' timestamps with `0` to preserve deterministic builds with `debug-embed` or release mode.
|
|
97
|
+
* `interpolate-folder-path`: Allow environment variables to be used in the `folder` path. This will pull the `foo` directory relative to your `Cargo.toml` file.
|
|
98
|
+
```rust
|
|
99
|
+
#[derive(Embed)]
|
|
100
|
+
#[folder = "$CARGO_MANIFEST_DIR/foo"]
|
|
101
|
+
struct Asset;
|
|
102
|
+
```
|
|
103
|
+
* `include-exclude`: Filter files to be embedded with multiple `#[include = "*.txt"]` and `#[exclude = "*.jpg"]` attributes.
|
|
104
|
+
Matching is done on relative file paths, via [globset](https://crates.io/crates/globset). `exclude` attributes have higher priority than `include` attributes.
|
|
105
|
+
```rust
|
|
106
|
+
use rust_embed::Embed;
|
|
107
|
+
|
|
108
|
+
#[derive(Embed)]
|
|
109
|
+
#[folder = "examples/public/"]
|
|
110
|
+
#[include = "*.html"]
|
|
111
|
+
#[include = "images/*"]
|
|
112
|
+
#[exclude = "*.txt"]
|
|
113
|
+
struct Asset;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
|
|
118
|
+
```rust
|
|
119
|
+
use rust_embed::Embed;
|
|
120
|
+
|
|
121
|
+
#[derive(Embed)]
|
|
122
|
+
#[folder = "examples/public/"]
|
|
123
|
+
#[prefix = "prefix/"]
|
|
124
|
+
struct Asset;
|
|
125
|
+
|
|
126
|
+
fn main() {
|
|
127
|
+
let index_html = Asset::get("prefix/index.html").unwrap();
|
|
128
|
+
println!("{:?}", std::str::from_utf8(index_html.data.as_ref()));
|
|
129
|
+
|
|
130
|
+
for file in Asset::iter() {
|
|
131
|
+
println!("{}", file.as_ref());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Integrations
|
|
137
|
+
|
|
138
|
+
1. [Poem](https://github.com/poem-web/poem) for poem framework under feature flag "embed"
|
|
139
|
+
2. [warp_embed](https://docs.rs/warp-embed/latest/warp_embed/) for warp framework
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
cargo run --example basic # dev mode where it reads from the fs
|
|
145
|
+
cargo run --example basic --release # release mode where it reads from binary
|
|
146
|
+
cargo run --example actix --features actix # https://github.com/actix/actix-web
|
|
147
|
+
cargo run --example rocket --features rocket # https://github.com/SergioBenitez/Rocket
|
|
148
|
+
cargo run --example warp --features warp-ex # https://github.com/seanmonstar/warp
|
|
149
|
+
cargo run --example axum --features axum-ex # https://github.com/tokio-rs/axum
|
|
150
|
+
cargo run --example poem --features poem-ex # https://github.com/poem-web/poem
|
|
151
|
+
cargo run --example salvo --features salvo-ex # https://github.com/salvo-rs/salvo
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Testing
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
cargo test --test lib
|
|
158
|
+
cargo test --test lib --features "debug-embed"
|
|
159
|
+
cargo test --test lib --features "compression" --release
|
|
160
|
+
cargo test --test mime_guess --features "mime-guess"
|
|
161
|
+
cargo test --test mime_guess --features "mime-guess" --release
|
|
162
|
+
cargo test --test interpolated_path --features "interpolate-folder-path"
|
|
163
|
+
cargo test --test interpolated_path --features "interpolate-folder-path" --release
|
|
164
|
+
cargo test --test custom_crate_path
|
|
165
|
+
cargo test --test custom_crate_path --release
|
|
166
|
+
cargo build --example basic
|
|
167
|
+
cargo build --example rocket --features rocket
|
|
168
|
+
cargo build --example actix --features actix
|
|
169
|
+
cargo build --example axum --features axum-ex
|
|
170
|
+
cargo build --example warp --features warp-ex
|
|
171
|
+
cargo test --test lib --release
|
|
172
|
+
cargo build --example basic --release
|
|
173
|
+
cargo build --example rocket --features rocket --release
|
|
174
|
+
cargo build --example actix --features actix --release
|
|
175
|
+
cargo build --example axum --features axum-ex --release
|
|
176
|
+
cargo build --example warp --features warp-ex --release
|
|
177
|
+
```
|
changelog.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Thanks to [Mark Drobnak](https://github.com/AzureMarker) for the changelog.
|
|
9
|
+
|
|
10
|
+
## [8.9.0] - 2025-10-31
|
|
11
|
+
|
|
12
|
+
- Ignore paths that couldn't be canonicalized. Thanks to zvolin <mac.zwolinski@gmail.com>.
|
|
13
|
+
|
|
14
|
+
## [8.9.0] - 2025-10-18
|
|
15
|
+
|
|
16
|
+
- fix export of RustEmbed macro. Thanks to LorrensP-2158466 <lorrens.pantelis@student.uhasselt.be>.
|
|
17
|
+
|
|
18
|
+
## [8.7.2] - 2025-05-14
|
|
19
|
+
|
|
20
|
+
- Update repo links to sub-packages
|
|
21
|
+
|
|
22
|
+
## [8.7.1] - 2025-05-05
|
|
23
|
+
|
|
24
|
+
- Update documentation and repo links
|
|
25
|
+
|
|
26
|
+
## [8.7.0] - 2025-04-10
|
|
27
|
+
|
|
28
|
+
- add deterministic timestamps flag for deterministic builds [#259](https://github.com/pyrossh/rust-embed/pull/259). Thanks to [daywalker90](https://github.com/daywalker90)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## [8.6.0] - 2025-02-25
|
|
32
|
+
|
|
33
|
+
- Update include-flate to 0.3 [#246](https://github.com/pyrossh/rust-embed/pull/246). Thanks to [krant](https://github.com/krant)
|
|
34
|
+
- refactor: remove redundant reference and closure [#250](https://github.com/pyrossh/rust-embed/pull/250). Thanks to [hamirmahal](https://github.com/hamirmahal)
|
|
35
|
+
- refactor: replace map().unwrap_or_else(). [#250](https://github.com/pyrossh/rust-embed/pull/255). Thanks to [hamirmahal](https://github.com/hamirmahal)
|
|
36
|
+
- Compatible with Axum 0.7.9 [#253](https://github.com/pyrossh/rust-embed/pull/253). Thanks to [wkmyws](https://github.com/wkmyws)
|
|
37
|
+
- Add allow_missing option to derive macro [#256](https://github.com/pyrossh/rust-embed/pull/256). Thanks to [lirannl](https://github.com/lirannl)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## [8.5.0] - 2024-07-09
|
|
41
|
+
|
|
42
|
+
- Allow users to specify a custom path to the rust_embed crate in generated code[#232](https://github.com/pyrossh/rust-embed/pull/232). Thanks to [Wulf](https://github.com/Wulf)
|
|
43
|
+
- Increase minimum rust-version to v1.7.0.0
|
|
44
|
+
|
|
45
|
+
## [8.4.0] - 2024-05-11
|
|
46
|
+
|
|
47
|
+
- Re-export RustEmbed as Embed [#245](https://github.com/pyrossh/rust-embed/pull/245/files). Thanks to [pyrossh](https://github.com/pyrossh)
|
|
48
|
+
- Do not build glob matchers repeatedly when include-exclude feature is enabled [#244](https://github.com/pyrossh/rust-embed/pull/244/files). Thanks to [osiewicz](https://github.com/osiewicz)
|
|
49
|
+
- Add `metadata_only` attribute [#241](https://github.com/pyrossh/rust-embed/pull/241/files). Thanks to [ddfisher](https://github.com/ddfisher)
|
|
50
|
+
- Replace `expect` with a safer alternative that returns `None` instead [#240](https://github.com/pyrossh/rust-embed/pull/240/files). Thanks to [costinsin](https://github.com/costinsin)
|
|
51
|
+
- Eliminate unnecessary `to_path` call [#239](https://github.com/pyrossh/rust-embed/pull/239/files). Thanks to [smoelius](https://github.com/smoelius)
|
|
52
|
+
|
|
53
|
+
## [8.3.0] - 2024-02-26
|
|
54
|
+
|
|
55
|
+
- Fix symbolic links in debug builds [#235](https://github.com/pyrossh/rust-embed/pull/235/files). Thanks to [Buckram123](https://github.com/Buckram123)
|
|
56
|
+
|
|
57
|
+
## [8.2.0] - 2023-12-29
|
|
58
|
+
|
|
59
|
+
- Fix naming collisions in macros [#230](https://github.com/pyrossh/rust-embed/pull/230/files). Thanks to [hwittenborn](https://github.com/hwittenborn)
|
|
60
|
+
|
|
61
|
+
## [8.1.0] - 2023-12-08
|
|
62
|
+
|
|
63
|
+
- Add created to file metadata. [#225](https://github.com/pyrossh/rust-embed/pull/225/files). Thanks to [ngalaiko](https://github.com/ngalaiko)
|
|
64
|
+
|
|
65
|
+
## [8.0.0] - 2023-08-23
|
|
66
|
+
|
|
67
|
+
- Store file contents statically and use binary search for lookup. [#217](https://github.com/pyrossh/rust-embed/pull/217/files). Thanks to [osiewicz](https://github.com/osiewicz)
|
|
68
|
+
|
|
69
|
+
## [6.8.1] - 2023-06-30
|
|
70
|
+
|
|
71
|
+
- Fix failing compilation under compression feature [#182](https://github.com/pyrossh/rust-embed/issues/182). Thanks to [osiewicz](https://github.com/osiewicz)
|
|
72
|
+
|
|
73
|
+
## [6.8.0] - 2023-06-30
|
|
74
|
+
|
|
75
|
+
- Update `include-flate` to v0.2 [#182](https://github.com/pyrossh/rust-embed/issues/182)
|
|
76
|
+
|
|
77
|
+
## [6.7.0] - 2023-06-09
|
|
78
|
+
|
|
79
|
+
- Update `syn` to v2.0 [#211](https://github.com/pyrossh/rust-embed/issues/211)
|
|
80
|
+
|
|
81
|
+
## [6.6.1] - 2023-03-25
|
|
82
|
+
|
|
83
|
+
- Fix mime-guess feature not working properly [#209](https://github.com/pyrossh/rust-embed/issues/209)
|
|
84
|
+
|
|
85
|
+
## [6.6.0] - 2023-03-05
|
|
86
|
+
|
|
87
|
+
- sort_by_file_name() requires walkdir v2.3.2 [#206](https://github.com/pyrossh/rust-embed/issues/206)
|
|
88
|
+
- Add `mime-guess` feature to statically store mimetype [#192](https://github.com/pyrossh/rust-embed/issues/192)
|
|
89
|
+
|
|
90
|
+
## [6.4.2] - 2022-10-20
|
|
91
|
+
|
|
92
|
+
- Fail the proc macro if include/exclude are used without the feature [#187](https://github.com/pyrossh/rust-embed/issues/187)
|
|
93
|
+
|
|
94
|
+
## [6.4.1] - 2022-09-13
|
|
95
|
+
|
|
96
|
+
- Update sha2 dependency version in utils crate [#186](https://github.com/pyrossh/rust-embed/issues/186)
|
|
97
|
+
|
|
98
|
+
## [6.4.0] - 2022-04-15
|
|
99
|
+
|
|
100
|
+
- Order files by filename [#171](https://github.com/pyros2097/rust-embed/issues/171). Thanks to [apognu](https://github.com/apognu)
|
|
101
|
+
|
|
102
|
+
## [6.3.0] - 2021-11-28
|
|
103
|
+
|
|
104
|
+
- Fixed a security issue in debug mode [#159](https://github.com/pyros2097/rust-embed/issues/159). Thanks to [5225225](https://github.com/5225225)
|
|
105
|
+
|
|
106
|
+
## [6.2.0] - 2021-09-01
|
|
107
|
+
|
|
108
|
+
- Fixed `include-exclude` feature when using cargo v2 resolver
|
|
109
|
+
|
|
110
|
+
## [6.1.0] - 2021-08-31
|
|
111
|
+
|
|
112
|
+
- Added `include-exclude` feature by [mbme](https://github.com/mbme)
|
|
113
|
+
|
|
114
|
+
## [6.0.1] - 2021-08-21
|
|
115
|
+
|
|
116
|
+
- Added doc comments to macro generated functions
|
|
117
|
+
|
|
118
|
+
## [6.0.0] - 2021-08-01
|
|
119
|
+
|
|
120
|
+
Idea came about from [Cody Casterline](https://github.com/NfNitLoop)
|
|
121
|
+
|
|
122
|
+
- Breaking change the `Asset::get()` api has changed and now returns an `EmbeddedFile` which contains a `data` field which is the bytes of the file and
|
|
123
|
+
a `metadata` field which has theses 2 properties associated to the file `hash` and `last_modified`;
|
|
124
|
+
|
|
125
|
+
## [5.9.0] - 2021-01-18
|
|
126
|
+
|
|
127
|
+
### Added
|
|
128
|
+
|
|
129
|
+
- Added path prefix attribute
|
|
130
|
+
|
|
131
|
+
## [5.8.0] - 2021-01-06
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- Fixed compiling with latest version of syn
|
|
136
|
+
|
|
137
|
+
## [5.7.0] - 2020-12-08
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
|
|
141
|
+
- Fix feature flag typo
|
|
142
|
+
|
|
143
|
+
## [5.6.0] - 2020-07-19
|
|
144
|
+
|
|
145
|
+
### Fixed
|
|
146
|
+
|
|
147
|
+
- Fixed windows path error in release mode
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
|
|
151
|
+
- Using github actions for CI now
|
|
152
|
+
|
|
153
|
+
## [5.5.1] - 2020-03-19
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
|
|
157
|
+
- Fixed warnings in latest nightly
|
|
158
|
+
|
|
159
|
+
## [5.5.0] - 2020-02-26
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
|
|
163
|
+
- Fixed the `folder` directory being relative to the current directory.
|
|
164
|
+
It is now relative to `Cargo.toml`.
|
|
165
|
+
|
|
166
|
+
## [5.4.0] - 2020-02-24
|
|
167
|
+
|
|
168
|
+
### Changed
|
|
169
|
+
|
|
170
|
+
- using rust-2018 edition now
|
|
171
|
+
- code cleanup
|
|
172
|
+
- updated examples and crates
|
|
173
|
+
|
|
174
|
+
## [5.3.0] - 2020-02-15
|
|
175
|
+
|
|
176
|
+
### Added
|
|
177
|
+
|
|
178
|
+
- `compression` feature for compressing embedded files
|
|
179
|
+
|
|
180
|
+
## [5.2.0] - 2019-12-05
|
|
181
|
+
|
|
182
|
+
## Changed
|
|
183
|
+
|
|
184
|
+
- updated syn and quote crate to 1.x
|
|
185
|
+
|
|
186
|
+
## [5.1.0] - 2019-07-09
|
|
187
|
+
|
|
188
|
+
## Fixed
|
|
189
|
+
|
|
190
|
+
- error when debug code tries to import the utils crate
|
|
191
|
+
|
|
192
|
+
## [5.0.1] - 2019-07-07
|
|
193
|
+
|
|
194
|
+
## Changed
|
|
195
|
+
|
|
196
|
+
- derive is allowed only on unit structs now
|
|
197
|
+
|
|
198
|
+
## [5.0.0] - 2019-07-05
|
|
199
|
+
|
|
200
|
+
## Added
|
|
201
|
+
|
|
202
|
+
- proper error message stating only unit structs are supported
|
|
203
|
+
|
|
204
|
+
## Fixed
|
|
205
|
+
|
|
206
|
+
- windows latest build
|
|
207
|
+
|
|
208
|
+
## [4.5.0] - 2019-06-29
|
|
209
|
+
|
|
210
|
+
## Added
|
|
211
|
+
|
|
212
|
+
- allow rust embed derive to take env variables in the folder path
|
|
213
|
+
|
|
214
|
+
## [4.4.0] - 2019-05-11
|
|
215
|
+
|
|
216
|
+
### Fixed
|
|
217
|
+
|
|
218
|
+
- a panic when struct has doc comments
|
|
219
|
+
|
|
220
|
+
### Added
|
|
221
|
+
|
|
222
|
+
- a warp example
|
|
223
|
+
|
|
224
|
+
## [4.3.0] - 2019-01-10
|
|
225
|
+
|
|
226
|
+
### Fixed
|
|
227
|
+
|
|
228
|
+
- debug_embed feature was not working at all
|
|
229
|
+
|
|
230
|
+
### Added
|
|
231
|
+
|
|
232
|
+
- a test run for debug_embed feature
|
|
233
|
+
|
|
234
|
+
## [4.2.0] - 2018-12-02
|
|
235
|
+
|
|
236
|
+
### Changed
|
|
237
|
+
|
|
238
|
+
- return `Cow<'static, [u8]>` to preserve static lifetime
|
|
239
|
+
|
|
240
|
+
## [4.1.0] - 2018-10-24
|
|
241
|
+
|
|
242
|
+
### Added
|
|
243
|
+
|
|
244
|
+
- `iter()` method to list files
|
|
245
|
+
|
|
246
|
+
## [4.0.0] - 2018-10-11
|
|
247
|
+
|
|
248
|
+
### Changed
|
|
249
|
+
|
|
250
|
+
- avoid vector allocation by returning `impl AsRef<[u8]>`
|
|
251
|
+
|
|
252
|
+
## [3.0.2] - 2018-09-05
|
|
253
|
+
|
|
254
|
+
### Added
|
|
255
|
+
|
|
256
|
+
- appveyor for testing on windows
|
|
257
|
+
|
|
258
|
+
### Fixed
|
|
259
|
+
|
|
260
|
+
- handle paths in windows correctly
|
|
261
|
+
|
|
262
|
+
## [3.0.1] - 2018-07-24
|
|
263
|
+
|
|
264
|
+
### Added
|
|
265
|
+
|
|
266
|
+
- panic if the folder cannot be found
|
|
267
|
+
|
|
268
|
+
## [3.0.0] - 2018-06-01
|
|
269
|
+
|
|
270
|
+
### Changed
|
|
271
|
+
|
|
272
|
+
- The derive attribute style so we don't need `attr_literals` and it can be used in stable rust now. Thanks to [Mcat12](https://github.com/Mcat12).
|
|
273
|
+
|
|
274
|
+
```rust
|
|
275
|
+
#[folder("assets/")]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
to
|
|
279
|
+
|
|
280
|
+
```rust
|
|
281
|
+
#[folder = "assets/"]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Removed
|
|
285
|
+
|
|
286
|
+
- log dependecy as we are not using it anymore
|
|
287
|
+
|
|
288
|
+
## [2.0.0] - 2018-05-26
|
|
289
|
+
|
|
290
|
+
### Changed
|
|
291
|
+
|
|
292
|
+
- Reimplemented the macro for release to use include_bytes for perf sake. Thanks to [lukad](https://github.com/lukad).
|
|
293
|
+
|
|
294
|
+
## [1.1.1] - 2018-03-19
|
|
295
|
+
|
|
296
|
+
### Changed
|
|
297
|
+
|
|
298
|
+
- Fixed usage error message
|
|
299
|
+
|
|
300
|
+
## [1.1.0] - 2018-03-19
|
|
301
|
+
|
|
302
|
+
### Added
|
|
303
|
+
|
|
304
|
+
- Release mode for custom derive
|
|
305
|
+
|
|
306
|
+
### Changed
|
|
307
|
+
|
|
308
|
+
- Fixed tests in travis
|
|
309
|
+
|
|
310
|
+
## [1.0.0] - 2018-03-18
|
|
311
|
+
|
|
312
|
+
### Changed
|
|
313
|
+
|
|
314
|
+
- Converted the rust-embed macro `embed!` into a Rust Custom Derive Macro `#[derive(RustEmbed)]` which implements get on the struct
|
|
315
|
+
|
|
316
|
+
```rust
|
|
317
|
+
let asset = embed!("examples/public/")
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
to
|
|
321
|
+
|
|
322
|
+
```rust
|
|
323
|
+
#[derive(RustEmbed)]
|
|
324
|
+
#[folder = "examples/public/"]
|
|
325
|
+
struct Asset;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## [0.5.2] - 2018-03-16
|
|
329
|
+
|
|
330
|
+
### Added
|
|
331
|
+
|
|
332
|
+
- rouille example
|
|
333
|
+
|
|
334
|
+
## [0.5.1] - 2018-03-16
|
|
335
|
+
|
|
336
|
+
### Removed
|
|
337
|
+
|
|
338
|
+
- the plugin attribute from crate
|
|
339
|
+
|
|
340
|
+
## [0.5.0] - 2018-03-16
|
|
341
|
+
|
|
342
|
+
### Added
|
|
343
|
+
|
|
344
|
+
- rocket example
|
|
345
|
+
|
|
346
|
+
### Changed
|
|
347
|
+
|
|
348
|
+
- Converted the rust-embed executable into a macro `embed!` which now loads files at compile time during release and from the fs during dev.
|
|
349
|
+
|
|
350
|
+
## [0.4.0] - 2017-03-2
|
|
351
|
+
|
|
352
|
+
### Changed
|
|
353
|
+
|
|
354
|
+
- `generate_assets` to public again
|
|
355
|
+
|
|
356
|
+
## [0.3.5] - 2017-03-2
|
|
357
|
+
|
|
358
|
+
### Added
|
|
359
|
+
|
|
360
|
+
- rust-embed prefix to all logs
|
|
361
|
+
|
|
362
|
+
## [0.3.4] - 2017-03-2
|
|
363
|
+
|
|
364
|
+
### Changed
|
|
365
|
+
|
|
366
|
+
- the lib to be plugin again
|
|
367
|
+
|
|
368
|
+
## [0.3.3] - 2017-03-2
|
|
369
|
+
|
|
370
|
+
### Changed
|
|
371
|
+
|
|
372
|
+
- the lib to be proc-macro from plugin
|
|
373
|
+
|
|
374
|
+
## [0.3.2] - 2017-03-2
|
|
375
|
+
|
|
376
|
+
### Changed
|
|
377
|
+
|
|
378
|
+
- lib name from `rust-embed` to `rust_embed`
|
|
379
|
+
|
|
380
|
+
## [0.3.1] - 2017-03-2
|
|
381
|
+
|
|
382
|
+
### Removed
|
|
383
|
+
|
|
384
|
+
- hyper example
|
|
385
|
+
|
|
386
|
+
## [0.3.0] - 2017-02-26
|
|
387
|
+
|
|
388
|
+
### Added
|
|
389
|
+
|
|
390
|
+
- rust-embed executable which generates rust code to embed resource files into your rust executable
|
|
391
|
+
it creates a file like assets.rs that contains the code for your assets.
|
|
392
|
+
|
|
393
|
+
## [0.2.0] - 2017-03-16
|
|
394
|
+
|
|
395
|
+
### Added
|
|
396
|
+
|
|
397
|
+
- rust-embed executable which generates rust code to embed resource files into your rust executable
|
|
398
|
+
it creates a file like assets.rs that contains the code for your assets.
|
examples/actix.rs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
|
|
2
|
+
use mime_guess::from_path;
|
|
3
|
+
use rust_embed::Embed;
|
|
4
|
+
|
|
5
|
+
#[derive(Embed)]
|
|
6
|
+
#[folder = "examples/public/"]
|
|
7
|
+
struct Asset;
|
|
8
|
+
|
|
9
|
+
fn handle_embedded_file(path: &str) -> HttpResponse {
|
|
10
|
+
match Asset::get(path) {
|
|
11
|
+
Some(content) => HttpResponse::Ok()
|
|
12
|
+
.content_type(from_path(path).first_or_octet_stream().as_ref())
|
|
13
|
+
.body(content.data.into_owned()),
|
|
14
|
+
None => HttpResponse::NotFound().body("404 Not Found"),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[actix_web::get("/")]
|
|
19
|
+
async fn index() -> impl Responder {
|
|
20
|
+
handle_embedded_file("index.html")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[actix_web::get("/dist/{_:.*}")]
|
|
24
|
+
async fn dist(path: web::Path<String>) -> impl Responder {
|
|
25
|
+
handle_embedded_file(path.as_str())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[actix_web::main]
|
|
29
|
+
async fn main() -> std::io::Result<()> {
|
|
30
|
+
HttpServer::new(|| App::new().service(index).service(dist)).bind("127.0.0.1:8000")?.run().await
|
|
31
|
+
}
|
examples/axum-spa/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A small example for hosting single page application (SPA) files with [axum](https://github.com/tokio-rs/axum) and [rust-embed](https://github.com/pyrossh/rust-embed).
|
examples/axum-spa/assets/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Single Page Application</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
I'm the body!
|
|
12
|
+
<script src="./js/script.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
|
|
15
|
+
</html>
|
examples/axum-spa/assets/js/script.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var elem = document.createElement("div");
|
|
2
|
+
elem.innerHTML = "I'm the JS script!<br>" + new Date();
|
|
3
|
+
document.body.appendChild(elem);
|
examples/axum-spa/main.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
use axum::{
|
|
2
|
+
http::{header, StatusCode, Uri},
|
|
3
|
+
response::{Html, IntoResponse, Response},
|
|
4
|
+
routing::Router,
|
|
5
|
+
};
|
|
6
|
+
use rust_embed::Embed;
|
|
7
|
+
use std::net::SocketAddr;
|
|
8
|
+
|
|
9
|
+
static INDEX_HTML: &str = "index.html";
|
|
10
|
+
|
|
11
|
+
#[derive(Embed)]
|
|
12
|
+
#[folder = "examples/axum-spa/assets/"]
|
|
13
|
+
struct Assets;
|
|
14
|
+
|
|
15
|
+
#[tokio::main]
|
|
16
|
+
async fn main() {
|
|
17
|
+
let app = Router::new().fallback(static_handler);
|
|
18
|
+
|
|
19
|
+
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
20
|
+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
21
|
+
println!("listening on {}", addr);
|
|
22
|
+
axum::serve(listener, app.into_make_service()).await.unwrap();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async fn static_handler(uri: Uri) -> impl IntoResponse {
|
|
26
|
+
let path = uri.path().trim_start_matches('/');
|
|
27
|
+
|
|
28
|
+
if path.is_empty() || path == INDEX_HTML {
|
|
29
|
+
return index_html().await;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
match Assets::get(path) {
|
|
33
|
+
Some(content) => {
|
|
34
|
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
35
|
+
|
|
36
|
+
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
|
|
37
|
+
}
|
|
38
|
+
None => {
|
|
39
|
+
if path.contains('.') {
|
|
40
|
+
return not_found().await;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
index_html().await
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async fn index_html() -> Response {
|
|
49
|
+
match Assets::get(INDEX_HTML) {
|
|
50
|
+
Some(content) => Html(content.data).into_response(),
|
|
51
|
+
None => not_found().await,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async fn not_found() -> Response {
|
|
56
|
+
(StatusCode::NOT_FOUND, "404").into_response()
|
|
57
|
+
}
|
examples/axum.rs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
use axum::{
|
|
2
|
+
extract::Path,
|
|
3
|
+
http::{header, StatusCode},
|
|
4
|
+
response::{Html, IntoResponse, Response},
|
|
5
|
+
routing::{get, Router},
|
|
6
|
+
};
|
|
7
|
+
use rust_embed::Embed;
|
|
8
|
+
use std::net::SocketAddr;
|
|
9
|
+
|
|
10
|
+
#[tokio::main]
|
|
11
|
+
async fn main() {
|
|
12
|
+
// Define our app routes, including a fallback option for anything not matched.
|
|
13
|
+
let app = Router::new()
|
|
14
|
+
.route("/", get(index_handler))
|
|
15
|
+
.route("/index.html", get(index_handler))
|
|
16
|
+
.route("/dist/{*file}", get(static_handler))
|
|
17
|
+
.fallback_service(get(not_found));
|
|
18
|
+
|
|
19
|
+
// Start listening on the given address.
|
|
20
|
+
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
21
|
+
println!("listening on {}", addr);
|
|
22
|
+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
23
|
+
axum::serve(listener, app.into_make_service()).await.unwrap();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// We use static route matchers ("/" and "/index.html") to serve our home
|
|
27
|
+
// page.
|
|
28
|
+
async fn index_handler() -> impl IntoResponse {
|
|
29
|
+
static_handler(Path("index.html".to_string())).await
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// We use a wildcard matcher ("/dist/*file") to match against everything
|
|
33
|
+
// within our defined assets directory. This is the directory on our Asset
|
|
34
|
+
// struct below, where folder = "examples/public/".
|
|
35
|
+
async fn static_handler(Path(path): Path<String>) -> impl IntoResponse {
|
|
36
|
+
StaticFile(path)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Finally, we use a fallback route for anything that didn't match.
|
|
40
|
+
async fn not_found() -> Html<&'static str> {
|
|
41
|
+
Html("<h1>404</h1><p>Not Found</p>")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Embed)]
|
|
45
|
+
#[folder = "examples/public/"]
|
|
46
|
+
struct Asset;
|
|
47
|
+
|
|
48
|
+
pub struct StaticFile<T>(pub T);
|
|
49
|
+
|
|
50
|
+
impl<T> IntoResponse for StaticFile<T>
|
|
51
|
+
where
|
|
52
|
+
T: Into<String>,
|
|
53
|
+
{
|
|
54
|
+
fn into_response(self) -> Response {
|
|
55
|
+
let path = self.0.into();
|
|
56
|
+
|
|
57
|
+
match Asset::get(path.as_str()) {
|
|
58
|
+
Some(content) => {
|
|
59
|
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
60
|
+
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
|
|
61
|
+
}
|
|
62
|
+
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
examples/basic.rs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
struct Asset;
|
|
6
|
+
|
|
7
|
+
fn main() {
|
|
8
|
+
let index_html = Asset::get("index.html").unwrap();
|
|
9
|
+
println!("{:?}", std::str::from_utf8(index_html.data.as_ref()));
|
|
10
|
+
}
|
examples/poem.rs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
use poem::{
|
|
2
|
+
async_trait,
|
|
3
|
+
http::{header, Method, StatusCode},
|
|
4
|
+
listener::TcpListener,
|
|
5
|
+
Endpoint, Request, Response, Result, Route, Server,
|
|
6
|
+
};
|
|
7
|
+
#[tokio::main]
|
|
8
|
+
async fn main() -> Result<(), std::io::Error> {
|
|
9
|
+
let app = Route::new().at("/", StaticEmbed).at("/index.html", StaticEmbed).nest("/dist", StaticEmbed);
|
|
10
|
+
|
|
11
|
+
let listener = TcpListener::bind("127.0.0.1:3000");
|
|
12
|
+
let server = Server::new(listener);
|
|
13
|
+
server.run(app).await?;
|
|
14
|
+
Ok(())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(rust_embed::Embed)]
|
|
18
|
+
#[folder = "examples/public/"]
|
|
19
|
+
struct Asset;
|
|
20
|
+
pub(crate) struct StaticEmbed;
|
|
21
|
+
|
|
22
|
+
#[async_trait]
|
|
23
|
+
impl Endpoint for StaticEmbed {
|
|
24
|
+
type Output = Response;
|
|
25
|
+
|
|
26
|
+
async fn call(&self, req: Request) -> Result<Self::Output> {
|
|
27
|
+
if req.method() != Method::GET {
|
|
28
|
+
return Ok(StatusCode::METHOD_NOT_ALLOWED.into());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let mut path = req.uri().path().trim_start_matches('/').trim_end_matches('/').to_string();
|
|
32
|
+
if path.starts_with("dist/") {
|
|
33
|
+
path = path.replace("dist/", "");
|
|
34
|
+
} else if path.is_empty() {
|
|
35
|
+
path = "index.html".to_string();
|
|
36
|
+
}
|
|
37
|
+
let path = path.as_ref();
|
|
38
|
+
|
|
39
|
+
match Asset::get(path) {
|
|
40
|
+
Some(content) => {
|
|
41
|
+
let hash = hex::encode(content.metadata.sha256_hash());
|
|
42
|
+
// if etag is matched, return 304
|
|
43
|
+
if req
|
|
44
|
+
.headers()
|
|
45
|
+
.get(header::IF_NONE_MATCH)
|
|
46
|
+
.map(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
|
|
47
|
+
.unwrap_or(false)
|
|
48
|
+
{
|
|
49
|
+
return Ok(StatusCode::NOT_MODIFIED.into());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// otherwise, return 200 with etag hash
|
|
53
|
+
let body: Vec<u8> = content.data.into();
|
|
54
|
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
55
|
+
Ok(
|
|
56
|
+
Response::builder()
|
|
57
|
+
.header(header::CONTENT_TYPE, mime.as_ref())
|
|
58
|
+
.header(header::ETAG, hash)
|
|
59
|
+
.body(body),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
None => Ok(Response::builder().status(StatusCode::NOT_FOUND).finish()),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
examples/public/images/doc.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Testing 1 2 3
|
examples/public/images/flower.jpg
ADDED
|
Binary file
|
examples/public/images/llama.png
ADDED
|
Binary file
|
examples/public/index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<link rel="stylesheet" type="text/css" href="dist/main.css" />
|
|
5
|
+
<script src="dist/main.js"></script>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div class="bor">
|
|
9
|
+
<table bgcolor=#f0f0f0 cellspacing=0 class="hh">
|
|
10
|
+
<tr>
|
|
11
|
+
<td><img src="dist/images/llama.png" align="left"></td>
|
|
12
|
+
<td>
|
|
13
|
+
<table cellspacing=0>
|
|
14
|
+
<tr><td><b>pyros2097</b></td></tr>
|
|
15
|
+
<tr><td>Gdx Developer</td></tr>
|
|
16
|
+
<tr><td>
|
|
17
|
+
"Awesomeness we can has"
|
|
18
|
+
</td></tr>
|
|
19
|
+
</table>
|
|
20
|
+
</td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
</tr>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
examples/public/main.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
body {
|
|
2
|
+
color:#000000;
|
|
3
|
+
font-family:sans-serif;
|
|
4
|
+
font-size: small;
|
|
5
|
+
font-style: italic;
|
|
6
|
+
font-weight: bold;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
table.hh {
|
|
10
|
+
border:1px solid gray;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
bor { border: 1px;}
|
examples/public/main.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Awesomeness we can has")
|
examples/public/symlinks/main.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../main.js
|
examples/rocket.rs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#[macro_use]
|
|
2
|
+
extern crate rocket;
|
|
3
|
+
|
|
4
|
+
use rocket::http::ContentType;
|
|
5
|
+
use rocket::response::content::RawHtml;
|
|
6
|
+
use rust_embed::Embed;
|
|
7
|
+
|
|
8
|
+
use std::borrow::Cow;
|
|
9
|
+
use std::ffi::OsStr;
|
|
10
|
+
use std::path::PathBuf;
|
|
11
|
+
|
|
12
|
+
#[derive(Embed)]
|
|
13
|
+
#[folder = "examples/public/"]
|
|
14
|
+
struct Asset;
|
|
15
|
+
|
|
16
|
+
#[get("/")]
|
|
17
|
+
fn index() -> Option<RawHtml<Cow<'static, [u8]>>> {
|
|
18
|
+
let asset = Asset::get("index.html")?;
|
|
19
|
+
Some(RawHtml(asset.data))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[get("/dist/<file..>")]
|
|
23
|
+
fn dist(file: PathBuf) -> Option<(ContentType, Cow<'static, [u8]>)> {
|
|
24
|
+
let filename = file.display().to_string();
|
|
25
|
+
let asset = Asset::get(&filename)?;
|
|
26
|
+
let content_type = file
|
|
27
|
+
.extension()
|
|
28
|
+
.and_then(OsStr::to_str)
|
|
29
|
+
.and_then(ContentType::from_extension)
|
|
30
|
+
.unwrap_or(ContentType::Bytes);
|
|
31
|
+
|
|
32
|
+
Some((content_type, asset.data))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[rocket::launch]
|
|
36
|
+
fn rocket() -> _ {
|
|
37
|
+
rocket::build().mount("/", routes![index, dist])
|
|
38
|
+
}
|
examples/salvo.rs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
use salvo::http::{header, StatusCode};
|
|
2
|
+
use salvo::prelude::*;
|
|
3
|
+
|
|
4
|
+
#[tokio::main]
|
|
5
|
+
async fn main() -> Result<(), std::io::Error> {
|
|
6
|
+
let router = Router::new()
|
|
7
|
+
.push(Router::with_path("dist/<**>").get(static_embed))
|
|
8
|
+
.push(Router::with_path("/<**>").get(static_embed));
|
|
9
|
+
|
|
10
|
+
let listener = TcpListener::bind("127.0.0.1:3000");
|
|
11
|
+
Server::new(listener).serve(router).await;
|
|
12
|
+
Ok(())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(rust_embed::Embed)]
|
|
16
|
+
#[folder = "examples/public/"]
|
|
17
|
+
struct Asset;
|
|
18
|
+
|
|
19
|
+
#[fn_handler]
|
|
20
|
+
async fn static_embed(req: &mut Request, res: &mut Response) {
|
|
21
|
+
let mut path: String = req.get_param("**").unwrap_or_default();
|
|
22
|
+
if path.is_empty() {
|
|
23
|
+
path = "index.html".into();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
match Asset::get(&path) {
|
|
27
|
+
Some(content) => {
|
|
28
|
+
let hash = hex::encode(content.metadata.sha256_hash());
|
|
29
|
+
// if etag is matched, return 304
|
|
30
|
+
if req
|
|
31
|
+
.headers()
|
|
32
|
+
.get(header::IF_NONE_MATCH)
|
|
33
|
+
.map(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
|
|
34
|
+
.unwrap_or(false)
|
|
35
|
+
{
|
|
36
|
+
res.set_status_code(StatusCode::NOT_MODIFIED);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// otherwise, return 200 with etag hash
|
|
41
|
+
let body: Vec<u8> = content.data.into();
|
|
42
|
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
43
|
+
res.headers_mut().insert(header::ETAG, hash.parse().unwrap());
|
|
44
|
+
res.render_binary(mime.as_ref().parse().unwrap(), &body);
|
|
45
|
+
}
|
|
46
|
+
None => res.set_status_code(StatusCode::NOT_FOUND),
|
|
47
|
+
}
|
|
48
|
+
}
|
examples/warp.rs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
use warp::{http::header::HeaderValue, path::Tail, reply::Response, Filter, Rejection, Reply};
|
|
3
|
+
|
|
4
|
+
#[derive(Embed)]
|
|
5
|
+
#[folder = "examples/public/"]
|
|
6
|
+
struct Asset;
|
|
7
|
+
|
|
8
|
+
#[tokio::main]
|
|
9
|
+
async fn main() {
|
|
10
|
+
let index_html = warp::path::end().and_then(serve_index);
|
|
11
|
+
let dist = warp::path("dist").and(warp::path::tail()).and_then(serve);
|
|
12
|
+
|
|
13
|
+
let routes = index_html.or(dist);
|
|
14
|
+
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async fn serve_index() -> Result<impl Reply, Rejection> {
|
|
18
|
+
serve_impl("index.html")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async fn serve(path: Tail) -> Result<impl Reply, Rejection> {
|
|
22
|
+
serve_impl(path.as_str())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn serve_impl(path: &str) -> Result<impl Reply, Rejection> {
|
|
26
|
+
let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?;
|
|
27
|
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
28
|
+
|
|
29
|
+
let mut res = Response::new(asset.data.into());
|
|
30
|
+
res.headers_mut().insert("content-type", HeaderValue::from_str(mime.as_ref()).unwrap());
|
|
31
|
+
Ok(res)
|
|
32
|
+
}
|
impl/Cargo.toml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rust-embed-impl"
|
|
3
|
+
version = "8.9.0"
|
|
4
|
+
description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev"
|
|
5
|
+
readme = "readme.md"
|
|
6
|
+
documentation = "https://docs.rs/rust-embed"
|
|
7
|
+
repository = "https://pyrossh.dev/repos/rust-embed"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
keywords = ["http", "rocket", "static", "web", "server"]
|
|
10
|
+
categories = ["web-programming::http-server"]
|
|
11
|
+
authors = ["pyrossh"]
|
|
12
|
+
edition = "2018"
|
|
13
|
+
|
|
14
|
+
[lib]
|
|
15
|
+
proc-macro = true
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
rust-embed-utils = { version = "8.9.0", path = "../utils" }
|
|
19
|
+
|
|
20
|
+
syn = { version = "2", default-features = false, features = [
|
|
21
|
+
"derive",
|
|
22
|
+
"parsing",
|
|
23
|
+
"proc-macro",
|
|
24
|
+
"printing",
|
|
25
|
+
] }
|
|
26
|
+
quote = "1"
|
|
27
|
+
proc-macro2 = "1"
|
|
28
|
+
walkdir = "2.3.1"
|
|
29
|
+
|
|
30
|
+
[dependencies.shellexpand]
|
|
31
|
+
version = "3"
|
|
32
|
+
optional = true
|
|
33
|
+
|
|
34
|
+
[features]
|
|
35
|
+
debug-embed = []
|
|
36
|
+
interpolate-folder-path = ["shellexpand"]
|
|
37
|
+
compression = []
|
|
38
|
+
mime-guess = ["rust-embed-utils/mime-guess"]
|
|
39
|
+
include-exclude = ["rust-embed-utils/include-exclude"]
|
|
40
|
+
deterministic-timestamps = []
|
impl/license
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../license
|
impl/readme.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Rust Embed Implementation
|
|
2
|
+
|
|
3
|
+
The implementation of the rust-embed macro lies here.
|
impl/src/lib.rs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#![recursion_limit = "1024"]
|
|
2
|
+
#![forbid(unsafe_code)]
|
|
3
|
+
#[macro_use]
|
|
4
|
+
extern crate quote;
|
|
5
|
+
extern crate proc_macro;
|
|
6
|
+
|
|
7
|
+
use proc_macro::TokenStream;
|
|
8
|
+
use proc_macro2::TokenStream as TokenStream2;
|
|
9
|
+
use rust_embed_utils::PathMatcher;
|
|
10
|
+
use std::{
|
|
11
|
+
collections::BTreeMap,
|
|
12
|
+
env,
|
|
13
|
+
io::ErrorKind,
|
|
14
|
+
iter::FromIterator,
|
|
15
|
+
path::{Path, PathBuf},
|
|
16
|
+
};
|
|
17
|
+
use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
|
|
18
|
+
|
|
19
|
+
fn embedded(
|
|
20
|
+
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String],
|
|
21
|
+
metadata_only: bool, crate_path: &syn::Path,
|
|
22
|
+
) -> syn::Result<TokenStream2> {
|
|
23
|
+
extern crate rust_embed_utils;
|
|
24
|
+
|
|
25
|
+
let mut match_values = BTreeMap::new();
|
|
26
|
+
let mut list_values = Vec::<String>::new();
|
|
27
|
+
|
|
28
|
+
let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
|
|
29
|
+
let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
|
|
30
|
+
let matcher = PathMatcher::new(&includes, &excludes);
|
|
31
|
+
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), matcher) {
|
|
32
|
+
match_values.insert(
|
|
33
|
+
rel_path.clone(),
|
|
34
|
+
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path)?,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
list_values.push(if let Some(prefix) = prefix {
|
|
38
|
+
format!("{}{}", prefix, rel_path)
|
|
39
|
+
} else {
|
|
40
|
+
rel_path
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let array_len = list_values.len();
|
|
45
|
+
|
|
46
|
+
// If debug-embed is on, unconditionally include the code below. Otherwise,
|
|
47
|
+
// make it conditional on cfg(not(debug_assertions)).
|
|
48
|
+
let not_debug_attr = if cfg!(feature = "debug-embed") {
|
|
49
|
+
quote! {}
|
|
50
|
+
} else {
|
|
51
|
+
quote! { #[cfg(not(debug_assertions))]}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let handle_prefix = if let Some(prefix) = prefix {
|
|
55
|
+
quote! {
|
|
56
|
+
let file_path = file_path.strip_prefix(#prefix)?;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
TokenStream2::new()
|
|
60
|
+
};
|
|
61
|
+
let match_values = match_values.into_iter().map(|(path, bytes)| {
|
|
62
|
+
quote! {
|
|
63
|
+
(#path, #bytes),
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
let value_type = if cfg!(feature = "compression") {
|
|
67
|
+
quote! { fn() -> #crate_path::EmbeddedFile }
|
|
68
|
+
} else {
|
|
69
|
+
quote! { #crate_path::EmbeddedFile }
|
|
70
|
+
};
|
|
71
|
+
let get_value = if cfg!(feature = "compression") {
|
|
72
|
+
quote! {|idx| (ENTRIES[idx].1)()}
|
|
73
|
+
} else {
|
|
74
|
+
quote! {|idx| ENTRIES[idx].1.clone()}
|
|
75
|
+
};
|
|
76
|
+
Ok(quote! {
|
|
77
|
+
#not_debug_attr
|
|
78
|
+
impl #ident {
|
|
79
|
+
/// Get an embedded file and its metadata.
|
|
80
|
+
pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
81
|
+
#handle_prefix
|
|
82
|
+
let key = file_path.replace("\\", "/");
|
|
83
|
+
const ENTRIES: &'static [(&'static str, #value_type)] = &[
|
|
84
|
+
#(#match_values)*];
|
|
85
|
+
let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
|
|
86
|
+
position.ok().map(#get_value)
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn names() -> ::std::slice::Iter<'static, &'static str> {
|
|
91
|
+
const ITEMS: [&str; #array_len] = [#(#list_values),*];
|
|
92
|
+
ITEMS.iter()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Iterates over the file paths in the folder.
|
|
96
|
+
pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
|
|
97
|
+
Self::names().map(|x| ::std::borrow::Cow::from(*x))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#not_debug_attr
|
|
102
|
+
impl #crate_path::RustEmbed for #ident {
|
|
103
|
+
fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
104
|
+
#ident::get(file_path)
|
|
105
|
+
}
|
|
106
|
+
fn iter() -> #crate_path::Filenames {
|
|
107
|
+
#crate_path::Filenames::Embedded(#ident::names())
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn dynamic(
|
|
114
|
+
ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool, crate_path: &syn::Path,
|
|
115
|
+
) -> TokenStream2 {
|
|
116
|
+
let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
|
|
117
|
+
(
|
|
118
|
+
quote! { let file_path = file_path.strip_prefix(#prefix)?; },
|
|
119
|
+
quote! { ::std::borrow::Cow::Owned(format!("{}{}", #prefix, e.rel_path)) },
|
|
120
|
+
)
|
|
121
|
+
} else {
|
|
122
|
+
(TokenStream2::new(), quote! { ::std::borrow::Cow::from(e.rel_path) })
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let declare_includes = quote! {
|
|
126
|
+
const INCLUDES: &[&str] = &[#(#includes),*];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let declare_excludes = quote! {
|
|
130
|
+
const EXCLUDES: &[&str] = &[#(#excludes),*];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// In metadata_only mode, we still need to read file contents to generate the
|
|
134
|
+
// file hash, but then we drop the file data.
|
|
135
|
+
let strip_contents = metadata_only.then_some(quote! {
|
|
136
|
+
.map(|mut file| { file.data = ::std::default::Default::default(); file })
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let non_canonical_folder_path = Path::new(&folder_path);
|
|
140
|
+
let canonical_folder_path = non_canonical_folder_path
|
|
141
|
+
.canonicalize()
|
|
142
|
+
.or_else(|err| match err {
|
|
143
|
+
err if err.kind() == ErrorKind::NotFound => Ok(non_canonical_folder_path.to_owned()),
|
|
144
|
+
err => Err(err),
|
|
145
|
+
})
|
|
146
|
+
.expect("folder path must resolve to an absolute path");
|
|
147
|
+
let canonical_folder_path = canonical_folder_path.to_str().expect("absolute folder path must be valid unicode");
|
|
148
|
+
|
|
149
|
+
quote! {
|
|
150
|
+
#[cfg(debug_assertions)]
|
|
151
|
+
impl #ident {
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
fn matcher() -> #crate_path::utils::PathMatcher {
|
|
155
|
+
#declare_includes
|
|
156
|
+
#declare_excludes
|
|
157
|
+
static PATH_MATCHER: ::std::sync::OnceLock<#crate_path::utils::PathMatcher> = ::std::sync::OnceLock::new();
|
|
158
|
+
PATH_MATCHER.get_or_init(|| #crate_path::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone()
|
|
159
|
+
}
|
|
160
|
+
/// Get an embedded file and its metadata.
|
|
161
|
+
pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
162
|
+
#handle_prefix
|
|
163
|
+
|
|
164
|
+
let rel_file_path = file_path.replace("\\", "/");
|
|
165
|
+
let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path);
|
|
166
|
+
|
|
167
|
+
// Make sure the path requested does not escape the folder path
|
|
168
|
+
let canonical_file_path = file_path.canonicalize().ok()?;
|
|
169
|
+
if !canonical_file_path.starts_with(#canonical_folder_path) {
|
|
170
|
+
// Tried to request a path that is not in the embedded folder
|
|
171
|
+
|
|
172
|
+
// TODO: Currently it allows "path_traversal_attack" for the symlink files
|
|
173
|
+
// For it to be working properly we need to get absolute path first
|
|
174
|
+
// and check that instead if it starts with `canonical_folder_path`
|
|
175
|
+
// https://doc.rust-lang.org/std/path/fn.absolute.html (currently nightly)
|
|
176
|
+
// Should be allowed only if it was a symlink
|
|
177
|
+
let metadata = ::std::fs::symlink_metadata(&file_path).ok()?;
|
|
178
|
+
if !metadata.is_symlink() {
|
|
179
|
+
return ::std::option::Option::None;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let path_matcher = Self::matcher();
|
|
183
|
+
if path_matcher.is_path_included(&rel_file_path) {
|
|
184
|
+
#crate_path::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
|
|
185
|
+
} else {
|
|
186
|
+
::std::option::Option::None
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Iterates over the file paths in the folder.
|
|
191
|
+
pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
|
|
192
|
+
use ::std::path::Path;
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
#crate_path::utils::get_files(::std::string::String::from(#folder_path), Self::matcher())
|
|
196
|
+
.map(|e| #map_iter)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[cfg(debug_assertions)]
|
|
201
|
+
impl #crate_path::RustEmbed for #ident {
|
|
202
|
+
fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
|
|
203
|
+
#ident::get(file_path)
|
|
204
|
+
}
|
|
205
|
+
fn iter() -> #crate_path::Filenames {
|
|
206
|
+
// the return type of iter() is unnamable, so we have to box it
|
|
207
|
+
#crate_path::Filenames::Dynamic(::std::boxed::Box::new(#ident::iter()))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn generate_assets(
|
|
214
|
+
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<String>, includes: Vec<String>, excludes: Vec<String>,
|
|
215
|
+
metadata_only: bool, crate_path: &syn::Path,
|
|
216
|
+
) -> syn::Result<TokenStream2> {
|
|
217
|
+
let embedded_impl = embedded(
|
|
218
|
+
ident,
|
|
219
|
+
relative_folder_path,
|
|
220
|
+
absolute_folder_path.clone(),
|
|
221
|
+
prefix.as_deref(),
|
|
222
|
+
&includes,
|
|
223
|
+
&excludes,
|
|
224
|
+
metadata_only,
|
|
225
|
+
crate_path,
|
|
226
|
+
);
|
|
227
|
+
if cfg!(feature = "debug-embed") {
|
|
228
|
+
return embedded_impl;
|
|
229
|
+
}
|
|
230
|
+
let embedded_impl = embedded_impl?;
|
|
231
|
+
let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes, metadata_only, crate_path);
|
|
232
|
+
|
|
233
|
+
Ok(quote! {
|
|
234
|
+
#embedded_impl
|
|
235
|
+
#dynamic_impl
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn embed_file(
|
|
240
|
+
folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path,
|
|
241
|
+
) -> syn::Result<TokenStream2> {
|
|
242
|
+
let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
|
|
243
|
+
let hash = file.metadata.sha256_hash();
|
|
244
|
+
let (last_modified, created) = if cfg!(feature = "deterministic-timestamps") {
|
|
245
|
+
(quote! { ::std::option::Option::Some(0u64) }, quote! { ::std::option::Option::Some(0u64) })
|
|
246
|
+
} else {
|
|
247
|
+
let last_modified = match file.metadata.last_modified() {
|
|
248
|
+
Some(last_modified) => quote! { ::std::option::Option::Some(#last_modified) },
|
|
249
|
+
None => quote! { ::std::option::Option::None },
|
|
250
|
+
};
|
|
251
|
+
let created = match file.metadata.created() {
|
|
252
|
+
Some(created) => quote! { ::std::option::Option::Some(#created) },
|
|
253
|
+
None => quote! { ::std::option::Option::None },
|
|
254
|
+
};
|
|
255
|
+
(last_modified, created)
|
|
256
|
+
};
|
|
257
|
+
#[cfg(feature = "mime-guess")]
|
|
258
|
+
let mimetype_tokens = {
|
|
259
|
+
let mt = file.metadata.mimetype();
|
|
260
|
+
quote! { , #mt }
|
|
261
|
+
};
|
|
262
|
+
#[cfg(not(feature = "mime-guess"))]
|
|
263
|
+
let mimetype_tokens = TokenStream2::new();
|
|
264
|
+
|
|
265
|
+
let embedding_code = if metadata_only {
|
|
266
|
+
quote! {
|
|
267
|
+
const BYTES: &'static [u8] = &[];
|
|
268
|
+
}
|
|
269
|
+
} else if cfg!(feature = "compression") {
|
|
270
|
+
let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
|
|
271
|
+
// Print some debugging information
|
|
272
|
+
let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
|
|
273
|
+
let full_relative_path = full_relative_path.to_string_lossy();
|
|
274
|
+
quote! {
|
|
275
|
+
#crate_path::flate!(static BYTES: [u8] from #full_relative_path);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
quote! {
|
|
279
|
+
const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
let closure_args = if cfg!(feature = "compression") {
|
|
283
|
+
quote! { || }
|
|
284
|
+
} else {
|
|
285
|
+
quote! {}
|
|
286
|
+
};
|
|
287
|
+
Ok(quote! {
|
|
288
|
+
#closure_args {
|
|
289
|
+
#embedding_code
|
|
290
|
+
|
|
291
|
+
#crate_path::EmbeddedFile {
|
|
292
|
+
data: ::std::borrow::Cow::Borrowed(&BYTES),
|
|
293
|
+
metadata: #crate_path::Metadata::__rust_embed_new([#(#hash),*], #last_modified, #created #mimetype_tokens)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Find all pairs of the `name = "value"` attribute from the derive input
|
|
300
|
+
fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
|
|
301
|
+
ast
|
|
302
|
+
.attrs
|
|
303
|
+
.iter()
|
|
304
|
+
.filter(|value| value.path().is_ident(attr_name))
|
|
305
|
+
.filter_map(|attr| match &attr.meta {
|
|
306
|
+
Meta::NameValue(MetaNameValue {
|
|
307
|
+
value: Expr::Lit(ExprLit { lit: Lit::Str(val), .. }),
|
|
308
|
+
..
|
|
309
|
+
}) => Some(val.value()),
|
|
310
|
+
_ => None,
|
|
311
|
+
})
|
|
312
|
+
.collect()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option<bool> {
|
|
316
|
+
ast
|
|
317
|
+
.attrs
|
|
318
|
+
.iter()
|
|
319
|
+
.find(|value| value.path().is_ident(attr_name))
|
|
320
|
+
.and_then(|attr| match &attr.meta {
|
|
321
|
+
Meta::NameValue(MetaNameValue {
|
|
322
|
+
value: Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }),
|
|
323
|
+
..
|
|
324
|
+
}) => Some(val.value()),
|
|
325
|
+
_ => None,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
|
|
330
|
+
match ast.data {
|
|
331
|
+
Data::Struct(ref data) => match data.fields {
|
|
332
|
+
Fields::Unit => {}
|
|
333
|
+
_ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
|
|
334
|
+
},
|
|
335
|
+
_ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
let crate_path: syn::Path = find_attribute_values(ast, "crate_path")
|
|
339
|
+
.last()
|
|
340
|
+
.map_or_else(|| syn::parse_str("rust_embed").unwrap(), |v| syn::parse_str(v).unwrap());
|
|
341
|
+
|
|
342
|
+
let mut folder_paths = find_attribute_values(ast, "folder");
|
|
343
|
+
if folder_paths.len() != 1 {
|
|
344
|
+
return Err(syn::Error::new_spanned(
|
|
345
|
+
ast,
|
|
346
|
+
"#[derive(RustEmbed)] must contain one attribute like this #[folder = \"examples/public/\"]",
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
let folder_path = folder_paths.remove(0);
|
|
350
|
+
|
|
351
|
+
let prefix = find_attribute_values(ast, "prefix").into_iter().next();
|
|
352
|
+
let includes = find_attribute_values(ast, "include");
|
|
353
|
+
let excludes = find_attribute_values(ast, "exclude");
|
|
354
|
+
let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false);
|
|
355
|
+
let allow_missing = find_bool_attribute(ast, "allow_missing").unwrap_or(false);
|
|
356
|
+
|
|
357
|
+
#[cfg(not(feature = "include-exclude"))]
|
|
358
|
+
if !includes.is_empty() || !excludes.is_empty() {
|
|
359
|
+
return Err(syn::Error::new_spanned(
|
|
360
|
+
ast,
|
|
361
|
+
"Please turn on the `include-exclude` feature to use the `include` and `exclude` attributes",
|
|
362
|
+
));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[cfg(feature = "interpolate-folder-path")]
|
|
366
|
+
let folder_path = shellexpand::full(&folder_path)
|
|
367
|
+
.map_err(|v| syn::Error::new_spanned(ast, v.to_string()))?
|
|
368
|
+
.to_string();
|
|
369
|
+
|
|
370
|
+
// Base relative paths on the Cargo.toml location
|
|
371
|
+
let (relative_path, absolute_folder_path) = if Path::new(&folder_path).is_relative() {
|
|
372
|
+
let absolute_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
|
|
373
|
+
.join(&folder_path)
|
|
374
|
+
.to_str()
|
|
375
|
+
.unwrap()
|
|
376
|
+
.to_owned();
|
|
377
|
+
(Some(folder_path.clone()), absolute_path)
|
|
378
|
+
} else {
|
|
379
|
+
if cfg!(feature = "compression") {
|
|
380
|
+
return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature."));
|
|
381
|
+
}
|
|
382
|
+
(None, folder_path)
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if !Path::new(&absolute_folder_path).exists() && !allow_missing {
|
|
386
|
+
let mut message = format!(
|
|
387
|
+
"#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'",
|
|
388
|
+
absolute_folder_path,
|
|
389
|
+
std::env::current_dir().unwrap().to_str().unwrap()
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Add a message about the interpolate-folder-path feature if the path may
|
|
393
|
+
// include a variable
|
|
394
|
+
if absolute_folder_path.contains('$') && cfg!(not(feature = "interpolate-folder-path")) {
|
|
395
|
+
message += "\nA variable has been detected. RustEmbed can expand variables \
|
|
396
|
+
when the `interpolate-folder-path` feature is enabled.";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return Err(syn::Error::new_spanned(ast, message));
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
generate_assets(
|
|
403
|
+
&ast.ident,
|
|
404
|
+
relative_path.as_deref(),
|
|
405
|
+
absolute_folder_path,
|
|
406
|
+
prefix,
|
|
407
|
+
includes,
|
|
408
|
+
excludes,
|
|
409
|
+
metadata_only,
|
|
410
|
+
&crate_path,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path))]
|
|
415
|
+
pub fn derive_input_object(input: TokenStream) -> TokenStream {
|
|
416
|
+
let ast = parse_macro_input!(input as DeriveInput);
|
|
417
|
+
match impl_rust_embed(&ast) {
|
|
418
|
+
Ok(ok) => ok.into(),
|
|
419
|
+
Err(e) => e.to_compile_error().into(),
|
|
420
|
+
}
|
|
421
|
+
}
|
rustfmt.toml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
merge_derives = true
|
|
2
|
+
fn_params_layout = "Compressed"
|
|
3
|
+
max_width = 160
|
|
4
|
+
tab_spaces = 2
|
|
5
|
+
reorder_imports = true
|
src/lib.rs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#![forbid(unsafe_code)]
|
|
2
|
+
#[cfg(feature = "compression")]
|
|
3
|
+
#[cfg_attr(feature = "compression", doc(hidden))]
|
|
4
|
+
pub use include_flate::flate;
|
|
5
|
+
|
|
6
|
+
extern crate rust_embed_impl;
|
|
7
|
+
pub use rust_embed_impl::*;
|
|
8
|
+
|
|
9
|
+
pub use rust_embed_utils::{EmbeddedFile, Metadata};
|
|
10
|
+
|
|
11
|
+
#[doc(hidden)]
|
|
12
|
+
pub extern crate rust_embed_utils as utils;
|
|
13
|
+
|
|
14
|
+
/// A directory of binary assets.
|
|
15
|
+
///
|
|
16
|
+
/// The files in the specified folder will be embedded into the executable in
|
|
17
|
+
/// release builds. Debug builds will read the data from the file system at
|
|
18
|
+
/// runtime.
|
|
19
|
+
///
|
|
20
|
+
/// This trait is meant to be derived like so:
|
|
21
|
+
/// ```
|
|
22
|
+
/// use rust_embed::Embed;
|
|
23
|
+
///
|
|
24
|
+
/// #[derive(Embed)]
|
|
25
|
+
/// #[folder = "examples/public/"]
|
|
26
|
+
/// struct Asset;
|
|
27
|
+
///
|
|
28
|
+
/// fn main() {}
|
|
29
|
+
/// ```
|
|
30
|
+
pub trait RustEmbed {
|
|
31
|
+
/// Get an embedded file and its metadata.
|
|
32
|
+
///
|
|
33
|
+
/// If the feature `debug-embed` is enabled or the binary was compiled in
|
|
34
|
+
/// release mode, the file information is embedded in the binary and the file
|
|
35
|
+
/// data is returned as a `Cow::Borrowed(&'static [u8])`.
|
|
36
|
+
///
|
|
37
|
+
/// Otherwise, the information is read from the file system on each call and
|
|
38
|
+
/// the file data is returned as a `Cow::Owned(Vec<u8>)`.
|
|
39
|
+
fn get(file_path: &str) -> Option<EmbeddedFile>;
|
|
40
|
+
|
|
41
|
+
/// Iterates over the file paths in the folder.
|
|
42
|
+
///
|
|
43
|
+
/// If the feature `debug-embed` is enabled or the binary is compiled in
|
|
44
|
+
/// release mode, a static array containing the list of relative file paths
|
|
45
|
+
/// is used.
|
|
46
|
+
///
|
|
47
|
+
/// Otherwise, the files are listed from the file system on each call.
|
|
48
|
+
fn iter() -> Filenames;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub use RustEmbed as Embed;
|
|
52
|
+
|
|
53
|
+
/// An iterator over filenames.
|
|
54
|
+
///
|
|
55
|
+
/// This enum exists for optimization purposes, to avoid boxing the iterator in
|
|
56
|
+
/// some cases. Do not try and match on it, as different variants will exist
|
|
57
|
+
/// depending on the compilation context.
|
|
58
|
+
pub enum Filenames {
|
|
59
|
+
/// Release builds use a named iterator type, which can be stack-allocated.
|
|
60
|
+
#[cfg(any(not(debug_assertions), feature = "debug-embed"))]
|
|
61
|
+
Embedded(std::slice::Iter<'static, &'static str>),
|
|
62
|
+
|
|
63
|
+
/// The debug iterator type is currently unnameable and still needs to be
|
|
64
|
+
/// boxed.
|
|
65
|
+
#[cfg(all(debug_assertions, not(feature = "debug-embed")))]
|
|
66
|
+
Dynamic(Box<dyn Iterator<Item = std::borrow::Cow<'static, str>>>),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Iterator for Filenames {
|
|
70
|
+
type Item = std::borrow::Cow<'static, str>;
|
|
71
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
72
|
+
match self {
|
|
73
|
+
#[cfg(any(not(debug_assertions), feature = "debug-embed"))]
|
|
74
|
+
Filenames::Embedded(names) => names.next().map(|x| std::borrow::Cow::from(*x)),
|
|
75
|
+
|
|
76
|
+
#[cfg(all(debug_assertions, not(feature = "debug-embed")))]
|
|
77
|
+
Filenames::Dynamic(boxed) => boxed.next(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
tests/allow_missing.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use std::{path::PathBuf, str::FromStr};
|
|
2
|
+
|
|
3
|
+
use rust_embed::Embed;
|
|
4
|
+
|
|
5
|
+
#[derive(Embed)]
|
|
6
|
+
#[folder = "examples/missing/"]
|
|
7
|
+
#[allow_missing = true]
|
|
8
|
+
struct Asset;
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn missing_is_empty() {
|
|
12
|
+
let path = PathBuf::from_str("./examples/missing").unwrap();
|
|
13
|
+
assert!(!path.exists());
|
|
14
|
+
assert_eq!(Asset::iter().count(), 0);
|
|
15
|
+
}
|
tests/custom_crate_path.rs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// This test checks that the `crate_path` attribute can be used
|
|
2
|
+
/// to specify a custom path to the `rust_embed` crate.
|
|
3
|
+
|
|
4
|
+
mod custom {
|
|
5
|
+
pub mod path {
|
|
6
|
+
pub use rust_embed;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// We introduce a 'rust_embed' module here to break compilation in case
|
|
11
|
+
// the `rust_embed` crate is not loaded correctly.
|
|
12
|
+
//
|
|
13
|
+
// To test this, try commenting out the attribute which specifies the
|
|
14
|
+
// the custom crate path -- you should find that the test fails to compile.
|
|
15
|
+
mod rust_embed {}
|
|
16
|
+
|
|
17
|
+
#[derive(custom::path::rust_embed::RustEmbed)]
|
|
18
|
+
#[crate_path = "custom::path::rust_embed"]
|
|
19
|
+
#[folder = "examples/public/"]
|
|
20
|
+
struct Asset;
|
tests/include_exclude.rs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
struct AllAssets;
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn get_works() {
|
|
9
|
+
assert!(AllAssets::get("index.html").is_some(), "index.html should exist");
|
|
10
|
+
assert!(AllAssets::get("gg.html").is_none(), "gg.html should not exist");
|
|
11
|
+
assert!(AllAssets::get("images/llama.png").is_some(), "llama.png should exist");
|
|
12
|
+
assert!(AllAssets::get("symlinks/main.js").is_some(), "main.js should exist");
|
|
13
|
+
assert_eq!(AllAssets::iter().count(), 7);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Embed)]
|
|
17
|
+
#[folder = "examples/public/"]
|
|
18
|
+
#[include = "*.html"]
|
|
19
|
+
#[include = "images/*"]
|
|
20
|
+
struct IncludeSomeAssets;
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn including_some_assets_works() {
|
|
24
|
+
assert!(IncludeSomeAssets::get("index.html").is_some(), "index.html should exist");
|
|
25
|
+
assert!(IncludeSomeAssets::get("main.js").is_none(), "main.js should not exist");
|
|
26
|
+
assert!(IncludeSomeAssets::get("images/llama.png").is_some(), "llama.png should exist");
|
|
27
|
+
assert_eq!(IncludeSomeAssets::iter().count(), 4);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Embed)]
|
|
31
|
+
#[folder = "examples/public/"]
|
|
32
|
+
#[exclude = "*.html"]
|
|
33
|
+
#[exclude = "images/*"]
|
|
34
|
+
struct ExcludeSomeAssets;
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn excluding_some_assets_works() {
|
|
38
|
+
assert!(ExcludeSomeAssets::get("index.html").is_none(), "index.html should not exist");
|
|
39
|
+
assert!(ExcludeSomeAssets::get("main.js").is_some(), "main.js should exist");
|
|
40
|
+
assert!(ExcludeSomeAssets::get("symlinks/main.js").is_some(), "main.js symlink should exist");
|
|
41
|
+
assert!(ExcludeSomeAssets::get("images/llama.png").is_none(), "llama.png should not exist");
|
|
42
|
+
assert_eq!(ExcludeSomeAssets::iter().count(), 3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Embed)]
|
|
46
|
+
#[folder = "examples/public/"]
|
|
47
|
+
#[include = "images/*"]
|
|
48
|
+
#[exclude = "*.txt"]
|
|
49
|
+
struct ExcludePriorityAssets;
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn exclude_has_higher_priority() {
|
|
53
|
+
assert!(ExcludePriorityAssets::get("images/doc.txt").is_none(), "doc.txt should not exist");
|
|
54
|
+
assert!(ExcludePriorityAssets::get("images/llama.png").is_some(), "llama.png should exist");
|
|
55
|
+
assert_eq!(ExcludePriorityAssets::iter().count(), 2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[derive(Embed)]
|
|
59
|
+
#[folder = "examples/public/symlinks"]
|
|
60
|
+
#[include = "main.js"]
|
|
61
|
+
struct IncludeSymlink;
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn include_symlink() {
|
|
65
|
+
assert_eq!(IncludeSymlink::iter().count(), 1);
|
|
66
|
+
assert_eq!(IncludeSymlink::iter().next(), Some(std::borrow::Cow::Borrowed("main.js")));
|
|
67
|
+
assert!(IncludeSymlink::get("main.js").is_some())
|
|
68
|
+
}
|
tests/interpolated_path.rs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
|
|
3
|
+
/// Test doc comment
|
|
4
|
+
#[derive(Embed)]
|
|
5
|
+
#[folder = "$CARGO_MANIFEST_DIR/examples/public/"]
|
|
6
|
+
struct Asset;
|
|
7
|
+
|
|
8
|
+
#[test]
|
|
9
|
+
fn get_works() {
|
|
10
|
+
assert!(Asset::get("index.html").is_some(), "index.html should exist");
|
|
11
|
+
assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
|
|
12
|
+
assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn iter_works() {
|
|
17
|
+
let mut num_files = 0;
|
|
18
|
+
for file in Asset::iter() {
|
|
19
|
+
assert!(Asset::get(file.as_ref()).is_some());
|
|
20
|
+
num_files += 1;
|
|
21
|
+
}
|
|
22
|
+
assert_eq!(num_files, 7);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn trait_works_generic() {
|
|
27
|
+
trait_works_generic_helper::<Asset>();
|
|
28
|
+
}
|
|
29
|
+
fn trait_works_generic_helper<E: rust_embed::Embed>() {
|
|
30
|
+
let mut num_files = 0;
|
|
31
|
+
for file in E::iter() {
|
|
32
|
+
assert!(E::get(file.as_ref()).is_some());
|
|
33
|
+
num_files += 1;
|
|
34
|
+
}
|
|
35
|
+
assert_eq!(num_files, 7);
|
|
36
|
+
assert!(E::get("gg.html").is_none(), "gg.html should not exist");
|
|
37
|
+
}
|
tests/lib.rs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
use rust_embed::{Embed, RustEmbed};
|
|
2
|
+
|
|
3
|
+
/// Test doc comment
|
|
4
|
+
#[derive(Embed)]
|
|
5
|
+
#[folder = "examples/public/"]
|
|
6
|
+
struct Asset;
|
|
7
|
+
|
|
8
|
+
#[derive(RustEmbed)]
|
|
9
|
+
#[folder = "examples/public/"]
|
|
10
|
+
struct AssetOld;
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn get_works() {
|
|
14
|
+
assert!(Asset::get("index.html").is_some(), "index.html should exist");
|
|
15
|
+
assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
|
|
16
|
+
assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Todo remove this test and rename RustEmbed trait to Embed on a new major release
|
|
20
|
+
#[test]
|
|
21
|
+
fn get_old_name_works() {
|
|
22
|
+
assert!(AssetOld::get("index.html").is_some(), "index.html should exist");
|
|
23
|
+
assert!(AssetOld::get("gg.html").is_none(), "gg.html should not exist");
|
|
24
|
+
assert!(AssetOld::get("images/llama.png").is_some(), "llama.png should exist");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Using Windows-style path separators (`\`) is acceptable
|
|
28
|
+
#[test]
|
|
29
|
+
fn get_windows_style() {
|
|
30
|
+
assert!(
|
|
31
|
+
Asset::get("images\\llama.png").is_some(),
|
|
32
|
+
"llama.png should be accessible via \"images\\lama.png\""
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn iter_works() {
|
|
38
|
+
let mut num_files = 0;
|
|
39
|
+
for file in Asset::iter() {
|
|
40
|
+
assert!(Asset::get(file.as_ref()).is_some());
|
|
41
|
+
num_files += 1;
|
|
42
|
+
}
|
|
43
|
+
assert_eq!(num_files, 7);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn trait_works_generic() {
|
|
48
|
+
trait_works_generic_helper::<Asset>();
|
|
49
|
+
}
|
|
50
|
+
fn trait_works_generic_helper<E: rust_embed::Embed>() {
|
|
51
|
+
let mut num_files = 0;
|
|
52
|
+
for file in E::iter() {
|
|
53
|
+
assert!(E::get(file.as_ref()).is_some());
|
|
54
|
+
num_files += 1;
|
|
55
|
+
}
|
|
56
|
+
assert_eq!(num_files, 7);
|
|
57
|
+
assert!(E::get("gg.html").is_none(), "gg.html should not exist");
|
|
58
|
+
}
|
tests/metadata.rs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
use rust_embed::{Embed, EmbeddedFile};
|
|
2
|
+
use sha2::Digest;
|
|
3
|
+
use std::{fs, time::SystemTime};
|
|
4
|
+
|
|
5
|
+
#[derive(Embed)]
|
|
6
|
+
#[folder = "examples/public/"]
|
|
7
|
+
struct Asset;
|
|
8
|
+
|
|
9
|
+
#[test]
|
|
10
|
+
fn hash_is_accurate() {
|
|
11
|
+
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
12
|
+
let mut hasher = sha2::Sha256::new();
|
|
13
|
+
hasher.update(index_file.data);
|
|
14
|
+
let expected_hash: [u8; 32] = hasher.finalize().into();
|
|
15
|
+
|
|
16
|
+
assert_eq!(index_file.metadata.sha256_hash(), expected_hash);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
#[cfg(not(feature = "deterministic-timestamps"))]
|
|
21
|
+
fn last_modified_is_accurate() {
|
|
22
|
+
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
23
|
+
|
|
24
|
+
let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
|
|
25
|
+
let expected_datetime_utc = metadata.modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
|
26
|
+
|
|
27
|
+
assert_eq!(index_file.metadata.last_modified(), Some(expected_datetime_utc));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
#[cfg(not(feature = "deterministic-timestamps"))]
|
|
32
|
+
fn create_is_accurate() {
|
|
33
|
+
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
34
|
+
|
|
35
|
+
let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
|
|
36
|
+
let expected_datetime_utc = metadata.created().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
|
37
|
+
|
|
38
|
+
assert_eq!(index_file.metadata.created(), Some(expected_datetime_utc));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[test]
|
|
42
|
+
#[cfg(feature = "deterministic-timestamps")]
|
|
43
|
+
fn deterministic_timestamps_are_zero() {
|
|
44
|
+
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
45
|
+
|
|
46
|
+
assert_eq!(
|
|
47
|
+
index_file.metadata.last_modified(),
|
|
48
|
+
Some(0),
|
|
49
|
+
"last_modified should be 0 with deterministic-timestamps"
|
|
50
|
+
);
|
|
51
|
+
assert_eq!(index_file.metadata.created(), Some(0), "created should be 0 with deterministic-timestamps");
|
|
52
|
+
|
|
53
|
+
let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
|
|
54
|
+
let fs_modified = metadata.modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
|
55
|
+
assert_ne!(fs_modified, 0, "Filesystem modified time should not be 0");
|
|
56
|
+
}
|
tests/metadata_only.rs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
use rust_embed::{Embed, EmbeddedFile};
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
#[metadata_only = true]
|
|
6
|
+
struct Asset;
|
|
7
|
+
|
|
8
|
+
#[test]
|
|
9
|
+
fn file_is_empty() {
|
|
10
|
+
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
11
|
+
assert_eq!(index_file.data.len(), 0);
|
|
12
|
+
}
|
tests/mime_guess.rs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
use rust_embed::{Embed, EmbeddedFile};
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
struct Asset;
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn html_mime_is_correct() {
|
|
9
|
+
let html_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
|
|
10
|
+
assert_eq!(html_file.metadata.mimetype(), "text/html");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[test]
|
|
14
|
+
fn css_mime_is_correct() {
|
|
15
|
+
let css_file: EmbeddedFile = Asset::get("main.css").expect("main.css exists");
|
|
16
|
+
assert_eq!(css_file.metadata.mimetype(), "text/css");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn js_mime_is_correct() {
|
|
21
|
+
let js_file: EmbeddedFile = Asset::get("main.js").expect("main.js exists");
|
|
22
|
+
assert_eq!(js_file.metadata.mimetype(), "text/javascript");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn jpg_mime_is_correct() {
|
|
27
|
+
let jpg_file: EmbeddedFile = Asset::get("images/flower.jpg").expect("flower.jpg exists");
|
|
28
|
+
assert_eq!(jpg_file.metadata.mimetype(), "image/jpeg");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn png_mime_is_correct() {
|
|
33
|
+
let png_file: EmbeddedFile = Asset::get("images/llama.png").expect("llama.png exists");
|
|
34
|
+
assert_eq!(png_file.metadata.mimetype(), "image/png");
|
|
35
|
+
}
|
tests/path_traversal_attack.rs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
struct Assets;
|
|
6
|
+
|
|
7
|
+
/// Prevent attempts to access files outside of the embedded folder.
|
|
8
|
+
/// This is mainly a concern when running in debug mode, since that loads from
|
|
9
|
+
/// the file system at runtime.
|
|
10
|
+
#[test]
|
|
11
|
+
fn path_traversal_attack_fails() {
|
|
12
|
+
assert!(Assets::get("../basic.rs").is_none());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(Embed)]
|
|
16
|
+
#[folder = "examples/axum-spa/"]
|
|
17
|
+
struct AxumAssets;
|
|
18
|
+
|
|
19
|
+
// TODO:
|
|
20
|
+
/// Prevent attempts to access symlinks outside of the embedded folder.
|
|
21
|
+
/// This is mainly a concern when running in debug mode, since that loads from
|
|
22
|
+
/// the file system at runtime.
|
|
23
|
+
#[test]
|
|
24
|
+
#[ignore = "see https://github.com/pyrossh/rust-embed/pull/235"]
|
|
25
|
+
fn path_traversal_attack_symlink_fails() {
|
|
26
|
+
assert!(Assets::get("../public/symlinks/main.js").is_none());
|
|
27
|
+
}
|
tests/prefix.rs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use rust_embed::Embed;
|
|
2
|
+
|
|
3
|
+
#[derive(Embed)]
|
|
4
|
+
#[folder = "examples/public/"]
|
|
5
|
+
#[prefix = "prefix/"]
|
|
6
|
+
struct Asset;
|
|
7
|
+
|
|
8
|
+
#[test]
|
|
9
|
+
fn get_with_prefix() {
|
|
10
|
+
assert!(Asset::get("prefix/index.html").is_some());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[test]
|
|
14
|
+
fn get_without_prefix() {
|
|
15
|
+
assert!(Asset::get("index.html").is_none());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn iter_values_have_prefix() {
|
|
20
|
+
for file in Asset::iter() {
|
|
21
|
+
assert!(file.starts_with("prefix/"));
|
|
22
|
+
assert!(Asset::get(file.as_ref()).is_some());
|
|
23
|
+
}
|
|
24
|
+
}
|
utils/Cargo.toml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rust-embed-utils"
|
|
3
|
+
version = "8.9.0"
|
|
4
|
+
description = "Utilities for rust-embed"
|
|
5
|
+
readme = "readme.md"
|
|
6
|
+
documentation = "https://docs.rs/rust-embed"
|
|
7
|
+
repository = "https://pyrossh.dev/repos/rust-embed"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
keywords = ["http", "rocket", "static", "web", "server"]
|
|
10
|
+
categories = ["web-programming::http-server"]
|
|
11
|
+
authors = ["pyrossh"]
|
|
12
|
+
edition = "2018"
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
walkdir = "2.3.1"
|
|
16
|
+
sha2 = "0.10.5"
|
|
17
|
+
mime_guess = { version = "2.0.4", optional = true }
|
|
18
|
+
|
|
19
|
+
[dependencies.globset]
|
|
20
|
+
version = "0.4.8"
|
|
21
|
+
optional = true
|
|
22
|
+
|
|
23
|
+
[features]
|
|
24
|
+
debug-embed = []
|
|
25
|
+
mime-guess = ["mime_guess"]
|
|
26
|
+
include-exclude = ["globset"]
|
utils/license
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../license
|
utils/readme.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Rust Embed Utilities
|
|
2
|
+
|
|
3
|
+
The utilities used by rust-embed and rust-embed-impl lie here.
|
utils/src/lib.rs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#![forbid(unsafe_code)]
|
|
2
|
+
|
|
3
|
+
use sha2::Digest;
|
|
4
|
+
use std::borrow::Cow;
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
use std::time::SystemTime;
|
|
7
|
+
use std::{fs, io};
|
|
8
|
+
|
|
9
|
+
#[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
|
|
10
|
+
pub struct FileEntry {
|
|
11
|
+
pub rel_path: String,
|
|
12
|
+
pub full_canonical_path: String,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
|
|
16
|
+
pub fn get_files(folder_path: String, matcher: PathMatcher) -> impl Iterator<Item = FileEntry> {
|
|
17
|
+
walkdir::WalkDir::new(&folder_path)
|
|
18
|
+
.follow_links(true)
|
|
19
|
+
.sort_by_file_name()
|
|
20
|
+
.into_iter()
|
|
21
|
+
.filter_map(std::result::Result::ok)
|
|
22
|
+
.filter(|e| e.file_type().is_file())
|
|
23
|
+
.filter_map(move |e| {
|
|
24
|
+
let rel_path = path_to_str(e.path().strip_prefix(&folder_path).unwrap());
|
|
25
|
+
let full_canonical_path = path_to_str(std::fs::canonicalize(e.path()).ok()?);
|
|
26
|
+
|
|
27
|
+
let rel_path = if std::path::MAIN_SEPARATOR == '\\' {
|
|
28
|
+
rel_path.replace('\\', "/")
|
|
29
|
+
} else {
|
|
30
|
+
rel_path
|
|
31
|
+
};
|
|
32
|
+
if matcher.is_path_included(&rel_path) {
|
|
33
|
+
Some(FileEntry { rel_path, full_canonical_path })
|
|
34
|
+
} else {
|
|
35
|
+
None
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// A file embedded into the binary
|
|
41
|
+
#[derive(Clone)]
|
|
42
|
+
pub struct EmbeddedFile {
|
|
43
|
+
pub data: Cow<'static, [u8]>,
|
|
44
|
+
pub metadata: Metadata,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Metadata about an embedded file
|
|
48
|
+
#[derive(Clone)]
|
|
49
|
+
pub struct Metadata {
|
|
50
|
+
hash: [u8; 32],
|
|
51
|
+
last_modified: Option<u64>,
|
|
52
|
+
created: Option<u64>,
|
|
53
|
+
#[cfg(feature = "mime-guess")]
|
|
54
|
+
mimetype: Cow<'static, str>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Metadata {
|
|
58
|
+
#[doc(hidden)]
|
|
59
|
+
pub const fn __rust_embed_new(
|
|
60
|
+
hash: [u8; 32], last_modified: Option<u64>, created: Option<u64>, #[cfg(feature = "mime-guess")] mimetype: &'static str,
|
|
61
|
+
) -> Self {
|
|
62
|
+
Self {
|
|
63
|
+
hash,
|
|
64
|
+
last_modified,
|
|
65
|
+
created,
|
|
66
|
+
#[cfg(feature = "mime-guess")]
|
|
67
|
+
mimetype: Cow::Borrowed(mimetype),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The SHA256 hash of the file
|
|
72
|
+
pub fn sha256_hash(&self) -> [u8; 32] {
|
|
73
|
+
self.hash
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The last modified date in seconds since the UNIX epoch. If the underlying
|
|
77
|
+
/// platform/file-system does not support this, None is returned.
|
|
78
|
+
pub fn last_modified(&self) -> Option<u64> {
|
|
79
|
+
self.last_modified
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The created data in seconds since the UNIX epoch. If the underlying
|
|
83
|
+
/// platform/file-system does not support this, None is returned.
|
|
84
|
+
pub fn created(&self) -> Option<u64> {
|
|
85
|
+
self.created
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The mime type of the file
|
|
89
|
+
#[cfg(feature = "mime-guess")]
|
|
90
|
+
pub fn mimetype(&self) -> &str {
|
|
91
|
+
&self.mimetype
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
|
|
96
|
+
let data = fs::read(file_path)?;
|
|
97
|
+
let data = Cow::from(data);
|
|
98
|
+
|
|
99
|
+
let mut hasher = sha2::Sha256::new();
|
|
100
|
+
hasher.update(&data);
|
|
101
|
+
let hash: [u8; 32] = hasher.finalize().into();
|
|
102
|
+
|
|
103
|
+
let source_date_epoch = match std::env::var("SOURCE_DATE_EPOCH") {
|
|
104
|
+
Ok(value) => value.parse::<u64>().ok(),
|
|
105
|
+
Err(_) => None,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let metadata = fs::metadata(file_path)?;
|
|
109
|
+
let last_modified = metadata
|
|
110
|
+
.modified()
|
|
111
|
+
.ok()
|
|
112
|
+
.and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
|
|
113
|
+
.map(|secs| secs.as_secs());
|
|
114
|
+
|
|
115
|
+
let created = metadata
|
|
116
|
+
.created()
|
|
117
|
+
.ok()
|
|
118
|
+
.and_then(|created| created.duration_since(SystemTime::UNIX_EPOCH).ok())
|
|
119
|
+
.map(|secs| secs.as_secs());
|
|
120
|
+
|
|
121
|
+
#[cfg(feature = "mime-guess")]
|
|
122
|
+
let mimetype = mime_guess::from_path(file_path).first_or_octet_stream().to_string();
|
|
123
|
+
|
|
124
|
+
Ok(EmbeddedFile {
|
|
125
|
+
data,
|
|
126
|
+
metadata: Metadata {
|
|
127
|
+
hash,
|
|
128
|
+
last_modified: source_date_epoch.or(last_modified),
|
|
129
|
+
created: source_date_epoch.or(created),
|
|
130
|
+
#[cfg(feature = "mime-guess")]
|
|
131
|
+
mimetype: mimetype.into(),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
|
|
137
|
+
p.as_ref().to_str().expect("Path does not have a string representation").to_owned()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[derive(Clone)]
|
|
141
|
+
pub struct PathMatcher {
|
|
142
|
+
#[cfg(feature = "include-exclude")]
|
|
143
|
+
include_matcher: globset::GlobSet,
|
|
144
|
+
#[cfg(feature = "include-exclude")]
|
|
145
|
+
exclude_matcher: globset::GlobSet,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[cfg(feature = "include-exclude")]
|
|
149
|
+
impl PathMatcher {
|
|
150
|
+
pub fn new(includes: &[&str], excludes: &[&str]) -> Self {
|
|
151
|
+
let mut include_matcher = globset::GlobSetBuilder::new();
|
|
152
|
+
for include in includes {
|
|
153
|
+
include_matcher.add(globset::Glob::new(include).unwrap_or_else(|_| panic!("invalid include pattern '{}'", include)));
|
|
154
|
+
}
|
|
155
|
+
let include_matcher = include_matcher
|
|
156
|
+
.build()
|
|
157
|
+
.unwrap_or_else(|_| panic!("Could not compile included patterns matcher"));
|
|
158
|
+
|
|
159
|
+
let mut exclude_matcher = globset::GlobSetBuilder::new();
|
|
160
|
+
for exclude in excludes {
|
|
161
|
+
exclude_matcher.add(globset::Glob::new(exclude).unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude)));
|
|
162
|
+
}
|
|
163
|
+
let exclude_matcher = exclude_matcher
|
|
164
|
+
.build()
|
|
165
|
+
.unwrap_or_else(|_| panic!("Could not compile excluded patterns matcher"));
|
|
166
|
+
|
|
167
|
+
Self {
|
|
168
|
+
include_matcher,
|
|
169
|
+
exclude_matcher,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
pub fn is_path_included(&self, path: &str) -> bool {
|
|
173
|
+
!self.exclude_matcher.is_match(path) && (self.include_matcher.is_empty() || self.include_matcher.is_match(path))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[cfg(not(feature = "include-exclude"))]
|
|
178
|
+
impl PathMatcher {
|
|
179
|
+
pub fn new(_includes: &[&str], _excludes: &[&str]) -> Self {
|
|
180
|
+
Self {}
|
|
181
|
+
}
|
|
182
|
+
pub fn is_path_included(&self, _path: &str) -> bool {
|
|
183
|
+
true
|
|
184
|
+
}
|
|
185
|
+
}
|