diff --git a/Cargo.lock b/Cargo.lock index fea2c17..85e833f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -124,6 +139,27 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bytemuck" version = "1.25.2" @@ -166,6 +202,7 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ + "brotli", "compression-core", "flate2", "memchr", @@ -581,7 +618,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pixhelf" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "axum", @@ -590,6 +627,7 @@ dependencies = [ "mime_guess", "natord", "serde", + "serde_json", "tempfile", "tokio", "tower", diff --git a/Cargo.toml b/Cargo.toml index f8cc267..1ba69f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pixhelf" -version = "0.1.2" +version = "0.1.3" edition = "2024" build = "build.rs" @@ -12,9 +12,10 @@ image = { version = "0.25.10", default-features = false, features = ["jpeg", "pn mime_guess = "2.0.5" natord = "1.0.9" serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.149" tokio = { version = "1.53.1", features = ["fs", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] } tower = { version = "0.5.3", features = ["util"] } -tower-http = { version = "0.6.11", features = ["compression-gzip", "fs", "set-header", "trace"] } +tower-http = { version = "0.6.11", features = ["compression-br", "compression-gzip", "fs", "set-header", "trace"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } walkdir = "2.5.0" diff --git a/README.md b/README.md index 73c6c38..89c7653 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pixhelf -基于 Axum 和 React 的自托管图片画廊。React 生产资源会嵌入 Rust 可执行文件,运行时只需要一个程序文件和外部图库目录。 +基于 Axum 和 Preact 的自托管图片画廊。Preact 生产资源会嵌入 Rust 可执行文件,运行时只需要一个程序文件和外部图库目录。 ## 运行 @@ -10,7 +10,9 @@ ./target/release/pixhelf ``` -HTTP 服务会立即开放,并优先生成首批 60 张、最长边 720px 的 WebP 缩略图。其余缩略图由后台任务持续处理;新增、修改或删除图片会在定期扫描后自动同步。图库为空时也可正常启动并等待后续图片。 +HTTP 服务会立即开放,并优先生成首批 60 张缩略图。每张图片只生成一套最长边 720px、质量 82 的高质量 WebP 缩略图;其余缩略图由后台任务持续处理。新增、修改或删除图片会在定期扫描后自动同步,图库为空时也可正常启动并等待后续图片。 + +首屏图库数据与压缩后的关键样式会随 HTML 一并下发,首张图片会提前加载。JS 使用内容版本化的长期缓存,API 使用 ETag 协商缓存,并按浏览器能力启用 Brotli 或 gzip 压缩。 缩略图缓存在 `./.pixhelf-cache/thumbnails` 下的版本化目录,原图不会被修改。缓存目录必须位于图库目录之外。 diff --git a/build.rs b/build.rs index 641d08c..8fe703d 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -use std::{env, path::PathBuf, process::Command}; +use std::{env, fs, path::PathBuf, process::Command}; fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest directory")); @@ -16,15 +16,32 @@ fn main() { } println!("cargo:rerun-if-env-changed=PIXHELF_SKIP_FRONTEND_BUILD"); - if env::var_os("PIXHELF_SKIP_FRONTEND_BUILD").is_some() { - return; + if env::var_os("PIXHELF_SKIP_FRONTEND_BUILD").is_none() { + let status = Command::new("npm") + .args(["run", "build", "--silent"]) + .current_dir(&frontend_dir) + .status() + .expect( + "failed to run the frontend build; install Node.js and run `npm ci` in frontend/", + ); + + assert!(status.success(), "frontend build failed"); } - let status = Command::new("npm") - .args(["run", "build", "--silent"]) - .current_dir(frontend_dir) - .status() - .expect("failed to run the frontend build; install Node.js and run `npm ci` in frontend/"); - - assert!(status.success(), "frontend build failed"); + let asset_version = asset_version(&frontend_dir); + println!("cargo:rustc-env=PIXHELF_ASSET_VERSION={asset_version}"); +} + +fn asset_version(frontend_dir: &std::path::Path) -> String { + let mut hash = 0xcbf29ce484222325u64; + for relative in ["dist/assets/app.js", "dist/assets/app.css"] { + let path = frontend_dir.join(relative); + let content = fs::read(&path) + .unwrap_or_else(|error| panic!("cannot read built asset {}: {error}", path.display())); + for byte in relative.bytes().chain(content) { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x100000001b3); + } + } + format!("{hash:016x}") } diff --git a/frontend/index.html b/frontend/index.html index 2b66b8b..12beca6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,11 +8,13 @@ /> +