Skip to content

Commit 1023a64

Browse files
marlonbaetencikzh
andauthored
Rewrite from proc-macro to build.rs (#15)
* Implemented build.rs flow, for single directory * Improve logging, require environment variable * Updated README * Add force-embed feature, support multiple directories (WIP) * Macro and/or build script * Always use OUT_DIR * Fix windows paths --------- Co-authored-by: cikzh <[email protected]>
1 parent 8b913b0 commit 1023a64

File tree

22 files changed

+510
-297
lines changed

22 files changed

+510
-297
lines changed

.gitignore

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
.vscode/
22
target/
3-
/Cargo.lock
3+
Cargo.lock
44

5-
6-
# Added by cargo
7-
#
8-
# already existing elements were commented out
9-
10-
/target
11-
#/Cargo.lock

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
[workspace]
2-
members = ["memory-serve", "memory-serve-macros", "examples/test"]
2+
members = ["memory-serve", "memory-serve-macros", "memory-serve-core"]
3+
exclude = ["example"]
34
resolver = "2"
45

56
[workspace.package]
6-
version = "0.6.0"
7+
version = "1.0.0-beta.0"
78
edition = "2021"
89
license = "Apache-2.0 OR MIT"
910
repository = "https://github.com/tweedegolf/memory-serve"

README.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,48 @@ memory-serve is designed to work with [axum](https://github.com/tokio-rs/axum)
3333

3434
## Usage
3535

36-
Provide a relative path to the directory containing your static assets
37-
to the [`load_assets!`] macro. This macro creates a data structure intended to
38-
be consumed by [`MemoryServe::new`]. Calling [`MemoryServe::into_router()`] on
39-
the resulting instance produces a axum
36+
There are two mechanisms to include assets at compile time.
37+
38+
1. Specify the path using a enviroment variable `ASSET_PATH` and call: `MemoryServe::from_env()` (best-practice)
39+
2. Call the `load_assets!` macro, and pass this to the constructor: `MemoryServe::new(load_assets!("/foo/bar"))`
40+
41+
The environment variable is handled by a build script and instructs cargo to re-evaluate when an asset in the directory changes.
42+
The output of the macro might be cached between build.
43+
44+
Both options try to be smart in resolving absolute and relative paths.
45+
46+
When an instance of `MemoryServe` is created, we can bind these to your axum instance.
47+
Calling [`MemoryServe::into_router()`] on the `MemoryServe` instance produces an axum
4048
[`Router`](https://docs.rs/axum/latest/axum/routing/struct.Router.html) that
4149
can either be merged in another `Router` or used directly in a server by
4250
calling [`Router::into_make_service()`](https://docs.rs/axum/latest/axum/routing/struct.Router.html#method.into_make_service).
4351

52+
### Named directories
53+
54+
Multiple directories can be included using different environment variables, all prefixed by `ASSET_PATH_`.
55+
For example: if you specify `ASSET_PATH_FOO` and `ASSET_PATH_BAR` the memory serve instances can be loaded
56+
using `MemoryServe::from_env_name("FOO")` and `MemoryServe::from_env_name("BAR")` respectively.
57+
58+
### Features
59+
60+
Use the `force-embed` feature flag to always include assets in the binary - also in debug builds.
61+
62+
### Environment variables
63+
64+
Use `MEMORY_SERVE_ROOT` to specify a root directory for relative paths provided to the `load_assets!` macro (or th `ASSET_PATH` variable).
65+
66+
Uee `MEMORY_SERVE_QUIET=1` to not print log messages at compile time.
67+
4468
## Example
4569

4670
```rust,no_run
4771
use axum::{response::Html, routing::get, Router};
48-
use memory_serve::{load_assets, MemoryServe};
72+
use memory_serve::{MemoryServe, load_assets};
4973
use std::net::SocketAddr;
5074
5175
#[tokio::main]
5276
async fn main() {
53-
let memory_router = MemoryServe::new(load_assets!("static"))
77+
let memory_router = MemoryServe::new(load_assets!("../static"))
5478
.index_file(Some("/index.html"))
5579
.into_router();
5680

example/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "memory-serve-test"
3+
edition = "2021"
4+
5+
[dependencies]
6+
memory-serve = { path = "../memory-serve" }
7+
axum = "0.7"
8+
tokio = { version = "1.0", features = ["full"] }
9+
tracing-subscriber = "0.3"
10+
tracing = "0.1"

examples/test/src/main.rs renamed to example/src/main.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use axum::{response::Html, routing::get, Router};
2-
use memory_serve::{load_assets, MemoryServe};
2+
use memory_serve::{MemoryServe, load_assets};
33
use std::net::SocketAddr;
4-
use tracing::info;
4+
use tracing::{info, Level};
55

66
#[tokio::main]
77
async fn main() {
8-
tracing_subscriber::fmt().init();
8+
tracing_subscriber::fmt()
9+
.with_max_level(Level::TRACE)
10+
.init();
911

10-
let memory_router = MemoryServe::new(load_assets!("../../static"))
12+
let memory_router = MemoryServe::new(load_assets!("../static"))
1113
.index_file(Some("/index.html"))
1214
.into_router();
1315

examples/test/Cargo.toml

Lines changed: 0 additions & 14 deletions
This file was deleted.

memory-serve-core/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "memory-serve-core"
3+
description = "Shared code for memory-serve and memory-serve-macros"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
publish.workspace = true
9+
10+
[dependencies]
11+
sha256 = "1.4"
12+
brotli = "7.0"
13+
mime_guess = "2.0"
14+
walkdir = "2"

memory-serve-core/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../README.md

memory-serve-macros/src/asset.rs renamed to memory-serve-core/src/asset.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
use syn::LitByteStr;
1+
use std::path::PathBuf;
22

33
/// Internal data structure
4-
pub(crate) struct Asset {
5-
pub(crate) route: String,
6-
pub(crate) path: String,
7-
pub(crate) etag: String,
8-
pub(crate) content_type: String,
9-
pub(crate) bytes: LitByteStr,
10-
pub(crate) brotli_bytes: LitByteStr,
4+
pub struct Asset {
5+
pub route: String,
6+
pub path: PathBuf,
7+
pub etag: String,
8+
pub content_type: String,
9+
pub compressed_bytes: Option<Vec<u8>>,
1110
}
1211

1312
impl PartialEq for Asset {

memory-serve-core/src/code.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use crate::{asset::Asset, list::list_assets};
4+
5+
/// Generate code with metadata and contents for the assets
6+
pub fn assets_to_code(asset_dir: &str, path: &Path, embed: bool, log: fn(&str)) -> String {
7+
let out_dir: String = std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set.");
8+
let out_dir = PathBuf::from(&out_dir);
9+
10+
log(&format!("Loading static assets from {asset_dir}"));
11+
12+
if embed {
13+
log("Embedding assets into binary");
14+
} else {
15+
log("Not embedding assets into binary, assets will load dynamically");
16+
}
17+
18+
let assets = list_assets(path, embed, log);
19+
20+
// using a string is faster than using quote ;)
21+
let mut code = "&[".to_string();
22+
23+
for asset in assets {
24+
let Asset {
25+
route,
26+
path,
27+
etag,
28+
content_type,
29+
compressed_bytes,
30+
} = asset;
31+
32+
let bytes = if !embed {
33+
"None".to_string()
34+
} else if let Some(compressed_bytes) = &compressed_bytes {
35+
let file_name = path.file_name().expect("Unable to get file name.");
36+
let file_path = Path::new(&out_dir).join(file_name);
37+
std::fs::write(&file_path, compressed_bytes).expect("Unable to write file to out dir.");
38+
39+
format!("Some(include_bytes!(r\"{}\"))", file_path.to_string_lossy())
40+
} else {
41+
format!("Some(include_bytes!(r\"{}\"))", path.to_string_lossy())
42+
};
43+
44+
let is_compressed = compressed_bytes.is_some();
45+
46+
code.push_str(&format!(
47+
"
48+
memory_serve::Asset {{
49+
route: r\"{route}\",
50+
path: r{path:?},
51+
content_type: \"{content_type}\",
52+
etag: \"{etag}\",
53+
bytes: {bytes},
54+
is_compressed: {is_compressed},
55+
}},"
56+
));
57+
}
58+
59+
code.push(']');
60+
61+
code
62+
}

memory-serve-core/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
mod asset;
2+
mod code;
3+
mod list;
4+
mod util;
5+
6+
pub use asset::Asset;
7+
pub use code::assets_to_code;
8+
9+
/// File mime types that can possibly be compressed
10+
pub const COMPRESS_TYPES: &[&str] = &[
11+
"text/html",
12+
"text/css",
13+
"application/json",
14+
"text/javascript",
15+
"application/javascript",
16+
"application/xml",
17+
"text/xml",
18+
"image/svg+xml",
19+
"application/wasm",
20+
];

memory-serve-core/src/list.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use std::path::Path;
2+
3+
use walkdir::WalkDir;
4+
5+
use crate::{
6+
asset::Asset,
7+
util::{compress_brotli, path_to_content_type, path_to_route},
8+
COMPRESS_TYPES,
9+
};
10+
11+
/// List all assets in the given directory (recursively) and return a list of assets with metadata
12+
pub fn list_assets(base_path: &Path, embed: bool, log: fn(&str)) -> Vec<Asset> {
13+
let mut assets: Vec<Asset> = WalkDir::new(base_path)
14+
.into_iter()
15+
.filter_map(|entry| entry.ok())
16+
.filter_map(|entry| {
17+
let path = entry.path().to_owned();
18+
let route = path_to_route(base_path, entry.path());
19+
20+
let Ok(metadata) = entry.metadata() else {
21+
log(&format!(
22+
"skipping file {route}, could not get file metadata"
23+
));
24+
return None;
25+
};
26+
27+
// skip directories
28+
if !metadata.is_file() {
29+
return None;
30+
};
31+
32+
// skip empty
33+
if metadata.len() == 0 {
34+
log(&format!("skipping file {route}: file empty"));
35+
return None;
36+
}
37+
38+
let Some(content_type) = path_to_content_type(entry.path()) else {
39+
log(&format!(
40+
"skipping file {route}, could not determine file extension"
41+
));
42+
return None;
43+
};
44+
45+
// do not load assets into the binary in debug / development mode
46+
if !embed {
47+
log(&format!("including {route} (dynamically)"));
48+
49+
return Some(Asset {
50+
route,
51+
path: path.to_owned(),
52+
content_type,
53+
etag: Default::default(),
54+
compressed_bytes: None,
55+
});
56+
}
57+
58+
let Ok(bytes) = std::fs::read(entry.path()) else {
59+
log(&format!("skipping file {route}: file is not readable"));
60+
return None;
61+
};
62+
63+
let etag: String = sha256::digest(&bytes);
64+
let original_size = bytes.len();
65+
let is_compress_type = COMPRESS_TYPES.contains(&content_type.as_str());
66+
let brotli_bytes = if is_compress_type {
67+
compress_brotli(&bytes)
68+
} else {
69+
None
70+
};
71+
72+
let mut asset = Asset {
73+
route: route.clone(),
74+
path: path.to_owned(),
75+
content_type,
76+
etag,
77+
compressed_bytes: None,
78+
};
79+
80+
if is_compress_type {
81+
match brotli_bytes {
82+
Some(brotli_bytes) if brotli_bytes.len() >= original_size => {
83+
log(&format!(
84+
"including {route} {original_size} bytes (compression unnecessary)"
85+
));
86+
}
87+
Some(brotli_bytes) => {
88+
log(&format!(
89+
"including {route} {original_size} -> {} bytes (compressed)",
90+
brotli_bytes.len()
91+
));
92+
93+
asset.compressed_bytes = Some(brotli_bytes);
94+
}
95+
None => {
96+
log(&format!(
97+
"including {route} {original_size} bytes (compression failed)"
98+
));
99+
}
100+
}
101+
} else {
102+
log(&format!("including {route} {original_size} bytes"));
103+
}
104+
105+
Some(asset)
106+
})
107+
.collect();
108+
109+
assets.sort();
110+
111+
assets
112+
}

0 commit comments

Comments
 (0)