feat(api): pre-compress static assets with brotli and gzip
- build.rs: generate .gz and .br compressed variants at build time, embed both into the FRONTEND constant - dist.rs: content-type for SPA fallback uses index.html path explicitly, explicit extension mapping for Vite content-hashed filenames - frontend lib: get_frontend_asset_compressed returns (data, encoding, etag)
This commit is contained in:
parent
73ba6329ea
commit
215846b1db
373
Cargo.lock
generated
373
Cargo.lock
generated
@ -110,7 +110,7 @@ dependencies = [
|
||||
"actix-utils",
|
||||
"base64 0.22.1",
|
||||
"bitflags",
|
||||
"brotli",
|
||||
"brotli 8.0.2",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.1.1",
|
||||
@ -443,7 +443,6 @@ name = "agent"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"agent-tool-derive",
|
||||
"async-openai",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"config",
|
||||
@ -454,13 +453,18 @@ dependencies = [
|
||||
"once_cell",
|
||||
"qdrant-client",
|
||||
"regex",
|
||||
"reqwest 0.13.2",
|
||||
"rig-core",
|
||||
"rust_decimal",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -918,6 +922,12 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "as-any"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063"
|
||||
|
||||
[[package]]
|
||||
name = "as-slice"
|
||||
version = "0.2.1"
|
||||
@ -950,46 +960,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-openai"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec08254d61379df136135d3d1ac04301be7699fd7d9e57655c63ac7d650a6922"
|
||||
dependencies = [
|
||||
"async-openai-macros",
|
||||
"backoff",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"derive_builder",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"rand 0.9.2",
|
||||
"reqwest 0.12.28",
|
||||
"reqwest-eventsource",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-openai-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81872a8e595e8ceceab71c6ba1f9078e313b452a1e31934e6763ef5d308705e4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
@ -1193,12 +1163,9 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"getrandom 0.2.17",
|
||||
"instant",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.5",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1404,12 +1371,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor 4.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
@ -1418,7 +1396,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
"brotli-decompressor 5.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "4.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1658,7 +1646,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2102,7 +2090,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
@ -2197,6 +2185,47 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deluxe"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488"
|
||||
dependencies = [
|
||||
"deluxe-core",
|
||||
"deluxe-macros",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deluxe-core"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deluxe-macros"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117"
|
||||
dependencies = [
|
||||
"deluxe-core",
|
||||
"heck 0.4.1",
|
||||
"if_chain",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@ -2832,6 +2861,8 @@ dependencies = [
|
||||
name = "frontend"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"brotli 7.0.0",
|
||||
"flate2",
|
||||
"lazy_static",
|
||||
"md5",
|
||||
"walkdir",
|
||||
@ -3761,9 +3792,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3904,6 +3937,12 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "if_chain"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
@ -4244,7 +4283,7 @@ checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
@ -4312,7 +4351,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"json-patch",
|
||||
"k8s-openapi",
|
||||
"schemars",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
@ -4950,6 +4989,15 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@ -5384,6 +5432,15 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.18.5"
|
||||
@ -5913,13 +5970,23 @@ dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.25.8+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6580,6 +6647,26 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@ -6632,6 +6719,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@ -6641,21 +6729,24 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs 0.8.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
@ -6704,22 +6795,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-eventsource"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde"
|
||||
dependencies = [
|
||||
"eventsource-stream",
|
||||
"futures-core",
|
||||
"futures-timer",
|
||||
"mime",
|
||||
"nom 7.1.3",
|
||||
"pin-project-lite",
|
||||
"reqwest 0.12.28",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
@ -6736,6 +6811,54 @@ version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
|
||||
[[package]]
|
||||
name = "rig-core"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f7a3f0c7c00eaced15a68ee16e1bd6bb709ff598d11b9aedac8b628217dc09"
|
||||
dependencies = [
|
||||
"as-any",
|
||||
"async-stream",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"eventsource-stream",
|
||||
"fastrand",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"glob",
|
||||
"http 1.4.0",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"nanoid",
|
||||
"ordered-float 5.3.0",
|
||||
"pin-project-lite",
|
||||
"reqwest 0.12.28",
|
||||
"rig-derive",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rig-derive"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7590f1ffc5cef2af569072500c3ee02836c6cfb9faee9b6f0fc140428a50891"
|
||||
dependencies = [
|
||||
"convert_case 0.10.0",
|
||||
"deluxe",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@ -6785,7 +6908,6 @@ version = "0.2.9"
|
||||
dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"async-openai",
|
||||
"chrono",
|
||||
"config",
|
||||
"dashmap",
|
||||
@ -7141,7 +7263,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
"schemars_derive 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@ -7158,6 +7293,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -7384,7 +7531,6 @@ version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -7535,7 +7681,6 @@ dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"async-openai",
|
||||
"avatar",
|
||||
"base64 0.22.1",
|
||||
"base64ct",
|
||||
@ -8172,6 +8317,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@ -8261,6 +8412,27 @@ dependencies = [
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
@ -8516,6 +8688,12 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
@ -8525,6 +8703,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.8+spec-1.1.0"
|
||||
@ -8532,9 +8721,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"toml_datetime",
|
||||
"toml_datetime 1.1.0+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8543,7 +8732,7 @@ version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8753,6 +8942,18 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-task",
|
||||
"pin-project",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@ -9439,6 +9640,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
@ -9642,6 +9854,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
|
||||
151
libs/api/dist.rs
151
libs/api/dist.rs
@ -26,6 +26,87 @@ fn cache_control_header(path: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the client explicitly accepts the given encoding via Accept-Encoding header.
|
||||
/// "*" is treated as accepting all encodings.
|
||||
fn accepts_encoding(req: &HttpRequest, target: &str) -> bool {
|
||||
let header_val = match req.headers().get(header::ACCEPT_ENCODING) {
|
||||
Some(v) => v.to_str().unwrap_or(""),
|
||||
None => return false,
|
||||
};
|
||||
for part in header_val.split(',') {
|
||||
let part = part.trim();
|
||||
// Strip quality value if present
|
||||
let enc = part.split(';').next().unwrap_or(part).trim();
|
||||
if enc.eq_ignore_ascii_case(target) || enc == "*" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Determines the Content-Type for a path, with explicit fallback for common frontend extensions.
|
||||
fn content_type_for_path(path: &str) -> String {
|
||||
if let Some(ext) = path.rsplit('.').next() {
|
||||
match ext {
|
||||
"html" | "htm" => return "text/html; charset=utf-8".into(),
|
||||
"js" | "mjs" => return "application/javascript; charset=utf-8".into(),
|
||||
"jsx" | "ts" | "tsx" => return "text/plain; charset=utf-8".into(),
|
||||
"css" => return "text/css; charset=utf-8".into(),
|
||||
"json" => return "application/json; charset=utf-8".into(),
|
||||
"svg" => return "image/svg+xml".into(),
|
||||
"png" => return "image/png".into(),
|
||||
"jpg" | "jpeg" => return "image/jpeg".into(),
|
||||
"gif" => return "image/gif".into(),
|
||||
"webp" => return "image/webp".into(),
|
||||
"ico" => return "image/x-icon".into(),
|
||||
"woff" => return "font/woff".into(),
|
||||
"woff2" => return "font/woff2".into(),
|
||||
"ttf" => return "font/ttf".into(),
|
||||
"otf" => return "font/otf".into(),
|
||||
"txt" => return "text/plain; charset=utf-8".into(),
|
||||
"xml" => return "application/xml; charset=utf-8".into(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
MimeGuess::from_path(path).first_or_octet_stream().to_string()
|
||||
}
|
||||
|
||||
/// Build an HttpResponse for the given asset.
|
||||
/// Sets Content-Type, Cache-Control, ETag, and Content-Encoding (only if non-empty).
|
||||
fn build_asset_response(
|
||||
req: &HttpRequest,
|
||||
data: &[u8],
|
||||
etag: &str,
|
||||
path: &str,
|
||||
cc: &str,
|
||||
content_encoding: &str,
|
||||
) -> HttpResponse {
|
||||
// 304 Not Modified when client has the same ETag
|
||||
if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
|
||||
if let Ok(client_etag) = if_none_match.to_str() {
|
||||
if client_etag == etag {
|
||||
let mut resp = HttpResponse::NotModified();
|
||||
resp.insert_header(("Cache-Control", cc));
|
||||
resp.insert_header(("ETag", etag));
|
||||
if !content_encoding.is_empty() {
|
||||
resp.insert_header(("Content-Encoding", content_encoding));
|
||||
}
|
||||
return resp.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mime = content_type_for_path(path);
|
||||
let mut resp = HttpResponse::Ok();
|
||||
resp.content_type(mime);
|
||||
resp.insert_header(("Cache-Control", cc));
|
||||
resp.insert_header(("ETag", etag));
|
||||
if !content_encoding.is_empty() {
|
||||
resp.insert_header(("Content-Encoding", content_encoding));
|
||||
}
|
||||
resp.body(data.to_vec())
|
||||
}
|
||||
|
||||
pub async fn serve_frontend(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
|
||||
let path = path.into_inner();
|
||||
let path_str = if path.is_empty() || path == "/" {
|
||||
@ -33,37 +114,53 @@ pub async fn serve_frontend(req: HttpRequest, path: web::Path<String>) -> HttpRe
|
||||
} else {
|
||||
path.as_str()
|
||||
};
|
||||
|
||||
let cc = cache_control_header(path_str);
|
||||
|
||||
match frontend::get_frontend_asset_with_etag(path_str) {
|
||||
Some((data, etag)) => {
|
||||
// Check If-None-Match for conditional request
|
||||
if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
|
||||
if let Ok(client_etag) = if_none_match.to_str() {
|
||||
if client_etag == etag {
|
||||
return HttpResponse::NotModified()
|
||||
.insert_header(("Cache-Control", cc))
|
||||
.insert_header(("ETag", etag))
|
||||
.finish();
|
||||
}
|
||||
// Try brotli first (best compression), then gzip, then uncompressed.
|
||||
// Only serve compressed variant if client explicitly accepts it AND we have one.
|
||||
let (data, encoding, etag, content_path) =
|
||||
match frontend::get_frontend_asset_compressed(path_str) {
|
||||
Some(r) => (r.0, r.1, r.2, path_str),
|
||||
None => {
|
||||
// Path not found — try index.html as SPA fallback.
|
||||
// Also use "index.html" for Content-Type detection (text/html).
|
||||
match frontend::get_frontend_asset_with_etag("index.html") {
|
||||
Some((data, etag)) => (data, "", etag, "index.html"),
|
||||
None => return HttpResponse::NotFound().finish(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mime = MimeGuess::from_path(path_str).first_or_octet_stream();
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime.as_ref())
|
||||
.insert_header(("Cache-Control", cc))
|
||||
.insert_header(("ETag", etag))
|
||||
.body(data.to_vec())
|
||||
}
|
||||
None => match frontend::get_frontend_asset_with_etag("index.html") {
|
||||
Some((data, etag)) => HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.insert_header(("Cache-Control", "no-cache, no-store, must-revalidate"))
|
||||
.insert_header(("ETag", etag))
|
||||
.body(data.to_vec()),
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
},
|
||||
if !encoding.is_empty() && accepts_encoding(&req, &encoding) {
|
||||
build_asset_response(&req, data, etag, content_path, cc, &encoding)
|
||||
} else {
|
||||
build_asset_response(&req, data, etag, content_path, cc, "")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_content_type_for_html() {
|
||||
assert_eq!(
|
||||
content_type_for_path("index.html"),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type_for_hashed_js() {
|
||||
assert_eq!(
|
||||
content_type_for_path("index-De8T6ILu.js"),
|
||||
"application/javascript; charset=utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type_for_extensionless() {
|
||||
let ct = content_type_for_path("auth/login");
|
||||
assert!(!ct.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,3 +9,5 @@ lazy_static.workspace = true
|
||||
[build-dependencies]
|
||||
walkdir.workspace = true
|
||||
md5.workspace = true
|
||||
brotli = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use md5::compute as md5_hash;
|
||||
use std::{env, fs, path::PathBuf, process::Command};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_pnpm(args: &[&str], cwd: &str) {
|
||||
let mut cmd = if cfg!(target_os = "windows") {
|
||||
@ -21,7 +24,7 @@ fn run_pnpm(args: &[&str], cwd: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_all_file(path: PathBuf) -> Vec<PathBuf> {
|
||||
fn find_all_files(path: PathBuf) -> Vec<PathBuf> {
|
||||
let mut files = vec![];
|
||||
for entry in fs::read_dir(path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
@ -29,12 +32,40 @@ fn find_all_file(path: PathBuf) -> Vec<PathBuf> {
|
||||
if path.is_file() {
|
||||
files.push(path);
|
||||
} else if path.is_dir() {
|
||||
files.extend(find_all_file(path));
|
||||
files.extend(find_all_files(path));
|
||||
}
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
/// Returns true if the file type is already compressed and not worth re-compressing.
|
||||
fn is_compressed_asset(path: &str) -> bool {
|
||||
path.ends_with(".png")
|
||||
|| path.ends_with(".jpg")
|
||||
|| path.ends_with(".jpeg")
|
||||
|| path.ends_with(".gif")
|
||||
|| path.ends_with(".webp")
|
||||
|| path.ends_with(".avif")
|
||||
|| path.ends_with(".ico")
|
||||
|| path.ends_with(".woff")
|
||||
|| path.ends_with(".woff2")
|
||||
|| path.ends_with(".ttf")
|
||||
|| path.ends_with(".otf")
|
||||
|| path.ends_with(".eot")
|
||||
|| path.ends_with(".zip")
|
||||
|| path.ends_with(".gz")
|
||||
|| path.ends_with(".br")
|
||||
|| path.ends_with(".xz")
|
||||
|| path.ends_with(".mp4")
|
||||
|| path.ends_with(".mp3")
|
||||
|| path.ends_with(".webm")
|
||||
|| path.ends_with(".ogg")
|
||||
|| path.ends_with(".wasm")
|
||||
}
|
||||
|
||||
/// Minimum file size (bytes) before compression is applied.
|
||||
const MIN_COMPRESS_SIZE: usize = 256;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../../package.json");
|
||||
println!("cargo:rerun-if-changed=../../pnpm-lock.yaml");
|
||||
@ -44,45 +75,97 @@ fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let project_root = manifest_dir.parent().unwrap().parent().unwrap();
|
||||
|
||||
let node_modules = project_root.join("node_modules");
|
||||
let _cache_file = node_modules.join(".cache_hash");
|
||||
|
||||
// Build frontend using pnpm in project root
|
||||
println!("cargo:warning=Building frontend...");
|
||||
run_pnpm(&["run", "build"], project_root.to_str().unwrap());
|
||||
|
||||
// Embed dist/ into OUT_DIR as blob files + generated .rs
|
||||
let dist = project_root.join("dist");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let blob_dir = out_dir.join("dist_blobs");
|
||||
fs::create_dir_all(&blob_dir).unwrap();
|
||||
|
||||
let mut strs = vec![];
|
||||
for file in find_all_file(dist.clone()) {
|
||||
let key = file.strip_prefix(&dist).unwrap()
|
||||
for file in find_all_files(dist.clone()) {
|
||||
let key = file
|
||||
.strip_prefix(&dist)
|
||||
.unwrap()
|
||||
.components()
|
||||
.collect::<PathBuf>()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let safe_name = key.replace('/', "_").replace('\\', "_");
|
||||
let blob_path = blob_dir.join(&safe_name);
|
||||
fs::copy(&file, &blob_path).unwrap();
|
||||
|
||||
// Compute ETag (MD5 hex of content) at build time
|
||||
let content = fs::read(&file).unwrap();
|
||||
let hash = md5_hash(&content);
|
||||
let etag_literal = format!("\"{:x}\"", hash);
|
||||
fs::write(&blob_path, &content).unwrap();
|
||||
|
||||
let hash = md5::compute(&content);
|
||||
let etag_literal = format!("\"{:x}\"", hash);
|
||||
let key_literal = format!("\"{}\"", key.replace('"', "\\\""));
|
||||
|
||||
let (gz_literal, br_literal) =
|
||||
if !is_compressed_asset(&key) && content.len() >= MIN_COMPRESS_SIZE {
|
||||
let gz_path = blob_dir.join(format!("{}.gz", safe_name));
|
||||
let br_path = blob_dir.join(format!("{}.br", safe_name));
|
||||
|
||||
let gz_data = {
|
||||
let mut encoder =
|
||||
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
let _ = encoder.write_all(&content);
|
||||
encoder.finish().unwrap()
|
||||
};
|
||||
let br_data = {
|
||||
let mut encoder = brotli::CompressorWriter::new(Vec::new(), 4096, 11, 22);
|
||||
let _ = encoder.write_all(&content);
|
||||
encoder.flush().unwrap();
|
||||
encoder.into_inner()
|
||||
};
|
||||
|
||||
if gz_data.len() < content.len() {
|
||||
fs::write(&gz_path, &gz_data).unwrap();
|
||||
}
|
||||
if br_data.len() < content.len() {
|
||||
fs::write(&br_path, &br_data).unwrap();
|
||||
}
|
||||
|
||||
let gz = if gz_data.len() < content.len() {
|
||||
format!(
|
||||
"Some((include_bytes!(\"dist_blobs/{}.gz\").to_vec(), \"gzip\"))",
|
||||
safe_name
|
||||
)
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
let br = if br_data.len() < content.len() {
|
||||
format!(
|
||||
"Some((include_bytes!(\"dist_blobs/{}.br\").to_vec(), \"br\"))",
|
||||
safe_name
|
||||
)
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
(gz, br)
|
||||
} else {
|
||||
("None".to_string(), "None".to_string())
|
||||
};
|
||||
|
||||
strs.push(format!(
|
||||
" ({}, include_bytes!(\"dist_blobs/{}\"), {}),",
|
||||
key_literal, safe_name, etag_literal
|
||||
r#" (
|
||||
{},
|
||||
include_bytes!("dist_blobs/{}"),
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
),"#,
|
||||
key_literal,
|
||||
safe_name,
|
||||
etag_literal,
|
||||
gz_literal,
|
||||
br_literal
|
||||
));
|
||||
}
|
||||
|
||||
let out_file = out_dir.join("frontend.rs");
|
||||
let generated = format!(
|
||||
"lazy_static::lazy_static! {{\n pub static ref FRONTEND: Vec<(&'static str, &'static [u8], &'static str)> = vec![\n{} ];\n}}\n",
|
||||
"lazy_static::lazy_static! {{\n pub static ref FRONTEND: Vec<(\n &'static str,\n &'static [u8],\n &'static str,\n Option<(Vec<u8>, &'static str)>,\n Option<(Vec<u8>, &'static str)>,\n )> = vec![\n{}\n ];\n}}\n",
|
||||
strs.join("\n")
|
||||
);
|
||||
fs::write(&out_file, generated).unwrap();
|
||||
|
||||
@ -4,12 +4,35 @@ include!(concat!(env!("OUT_DIR"), "/frontend.rs"));
|
||||
|
||||
/// Returns the embedded frontend static asset for the given path, or `None` if not found.
|
||||
pub fn get_frontend_asset(path: &str) -> Option<&'static [u8]> {
|
||||
FRONTEND.iter().find(|(k, _, _)| *k == path).map(|(_, v, _)| *v)
|
||||
FRONTEND.iter().find(|(k, _, _, _, _)| *k == path).map(|(_, v, _, _, _)| *v)
|
||||
}
|
||||
|
||||
/// Returns the embedded frontend static asset and its ETag for the given path.
|
||||
pub fn get_frontend_asset_with_etag(path: &str) -> Option<(&'static [u8], &'static str)> {
|
||||
FRONTEND.iter()
|
||||
.find(|(k, _, _)| *k == path)
|
||||
.map(|(_, v, etag)| (v as &_, etag as &_))
|
||||
FRONTEND
|
||||
.iter()
|
||||
.find(|(k, _, _, _, _)| *k == path)
|
||||
.map(|(_, v, etag, _, _)| (v as &[u8], *etag))
|
||||
}
|
||||
|
||||
/// Returns the best compressed variant (brotli > gzip) and its ETag, if smaller than uncompressed.
|
||||
/// Returns `(data, encoding, etag)` — `encoding` is "" when no compressed variant exists.
|
||||
pub fn get_frontend_asset_compressed(path: &str) -> Option<(&'static [u8], &'static str, &'static str)> {
|
||||
FRONTEND
|
||||
.iter()
|
||||
.find(|(k, _, _, _, _)| *k == path)
|
||||
.map(|(_, v, etag, brotli, gzip)| {
|
||||
// brotli preferred; fall back to gzip; fall back to uncompressed
|
||||
if let Some((ref data, enc)) = *brotli {
|
||||
if !enc.is_empty() {
|
||||
return (data.as_slice(), enc, *etag);
|
||||
}
|
||||
}
|
||||
if let Some((ref data, enc)) = *gzip {
|
||||
if !enc.is_empty() {
|
||||
return (data.as_slice(), enc, *etag);
|
||||
}
|
||||
}
|
||||
(v as &[u8], "", *etag)
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user