Compare commits
No commits in common. "05b9a3a5954a4a91b14bcdddee5db2b4aa1bc532" and "facb75d6325e4af489fefa7c9de9b4f25d8dd39c" have entirely different histories.
05b9a3a595
...
facb75d632
@ -28,7 +28,7 @@ SSR_CACHE_SECS=3600
|
|||||||
|
|
||||||
# Compression algorithms for HTTP responses.
|
# Compression algorithms for HTTP responses.
|
||||||
# Comma-separated list, case-insensitive. Supported: gzip, brotli (or br), deflate, zstd.
|
# Comma-separated list, case-insensitive. Supported: gzip, brotli (or br), deflate, zstd.
|
||||||
# Use "all" to enable everything (default when unset); use "none" or "off" to disable.
|
# Leave empty or unset to disable compression entirely.
|
||||||
COMPRESSION_ALGORITHMS=gzip,brotli,deflate,zstd
|
COMPRESSION_ALGORITHMS=gzip,brotli,deflate,zstd
|
||||||
|
|
||||||
# Image serving cache headers (hardcoded defaults)
|
# Image serving cache headers (hardcoded defaults)
|
||||||
|
|||||||
305
src/main.rs
305
src/main.rs
@ -28,42 +28,18 @@ mod theme;
|
|||||||
mod utils;
|
mod utils;
|
||||||
mod webp;
|
mod webp;
|
||||||
|
|
||||||
/// 压缩算法配置。
|
/// 根据 COMPRESSION_ALGORITHMS 环境变量构造 CompressionLayer。
|
||||||
|
/// 环境变量为空或未设置时返回 None,表示不启用压缩。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
fn compression_layer_from_env() -> Option<tower_http::compression::CompressionLayer> {
|
||||||
struct CompressionAlgorithms {
|
use tower_http::compression::CompressionLayer;
|
||||||
gzip: bool,
|
|
||||||
brotli: bool,
|
|
||||||
deflate: bool,
|
|
||||||
zstd: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
let env = std::env::var("COMPRESSION_ALGORITHMS").unwrap_or_default();
|
||||||
impl CompressionAlgorithms {
|
|
||||||
fn all_enabled() -> Self {
|
|
||||||
Self {
|
|
||||||
gzip: true,
|
|
||||||
brotli: true,
|
|
||||||
deflate: true,
|
|
||||||
zstd: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
!self.gzip && !self.brotli && !self.deflate && !self.zstd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析 COMPRESSION_ALGORITHMS 环境变量值。
|
|
||||||
/// ""、"none"、"off" 返回 None;"all" 或未识别到任何算法时启用全部。
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
fn parse_compression_algorithms(env: &str) -> Option<CompressionAlgorithms> {
|
|
||||||
let env = env.trim();
|
let env = env.trim();
|
||||||
if env.is_empty() || env.eq_ignore_ascii_case("none") || env.eq_ignore_ascii_case("off") {
|
if env.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut all = false;
|
|
||||||
let mut gzip = false;
|
let mut gzip = false;
|
||||||
let mut brotli = false;
|
let mut brotli = false;
|
||||||
let mut deflate = false;
|
let mut deflate = false;
|
||||||
@ -71,7 +47,6 @@ fn parse_compression_algorithms(env: &str) -> Option<CompressionAlgorithms> {
|
|||||||
|
|
||||||
for part in env.split(',') {
|
for part in env.split(',') {
|
||||||
match part.trim().to_lowercase().as_str() {
|
match part.trim().to_lowercase().as_str() {
|
||||||
"all" => all = true,
|
|
||||||
"gzip" => gzip = true,
|
"gzip" => gzip = true,
|
||||||
"brotli" | "br" => brotli = true,
|
"brotli" | "br" => brotli = true,
|
||||||
"deflate" => deflate = true,
|
"deflate" => deflate = true,
|
||||||
@ -83,102 +58,16 @@ fn parse_compression_algorithms(env: &str) -> Option<CompressionAlgorithms> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if all {
|
if !gzip && !brotli && !deflate && !zstd {
|
||||||
return Some(CompressionAlgorithms::all_enabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
let algorithms = CompressionAlgorithms {
|
|
||||||
gzip,
|
|
||||||
brotli,
|
|
||||||
deflate,
|
|
||||||
zstd,
|
|
||||||
};
|
|
||||||
if algorithms.is_empty() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(algorithms)
|
let mut layer = CompressionLayer::new();
|
||||||
}
|
layer = layer.gzip(gzip);
|
||||||
|
layer = layer.br(brotli);
|
||||||
/// 根据 COMPRESSION_ALGORITHMS 环境变量构造 CompressionLayer。
|
layer = layer.deflate(deflate);
|
||||||
/// 未设置或设置为 "all" 时启用全部算法;设置为 ""、"none" 或 "off" 时禁用。
|
layer = layer.zstd(zstd);
|
||||||
#[cfg(feature = "server")]
|
Some(layer)
|
||||||
fn compression_layer_from_env() -> Option<tower_http::compression::CompressionLayer> {
|
|
||||||
use tower_http::compression::CompressionLayer;
|
|
||||||
|
|
||||||
let env = std::env::var("COMPRESSION_ALGORITHMS").unwrap_or_else(|_| "all".to_string());
|
|
||||||
let algorithms = parse_compression_algorithms(&env)?;
|
|
||||||
|
|
||||||
Some(
|
|
||||||
CompressionLayer::new()
|
|
||||||
.gzip(algorithms.gzip)
|
|
||||||
.br(algorithms.brotli)
|
|
||||||
.deflate(algorithms.deflate)
|
|
||||||
.zstd(algorithms.zstd),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根据请求路径和方法决定公开页面的 Cache-Control 头。
|
|
||||||
/// 返回 None 表示不添加缓存头(保留现有行为或避免覆盖)。
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
fn cache_control_for_path(
|
|
||||||
path: &str,
|
|
||||||
method: &axum::http::Method,
|
|
||||||
) -> Option<axum::http::HeaderValue> {
|
|
||||||
use axum::http::{HeaderValue, Method};
|
|
||||||
|
|
||||||
// 只对 GET/HEAD 请求添加缓存头
|
|
||||||
if *method != Method::GET && *method != Method::HEAD {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 接口:不缓存(可能涉及认证、写操作)
|
|
||||||
if path.starts_with("/api") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 管理后台和认证页面:不缓存
|
|
||||||
if path.starts_with("/admin") || path == "/login" || path == "/register" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 静态资源:长期缓存(Dioxus/WASM 资源通常带内容哈希)
|
|
||||||
if path.starts_with("/_dioxus/")
|
|
||||||
|| path.starts_with("/wasm/")
|
|
||||||
|| path.ends_with(".wasm")
|
|
||||||
|| path.ends_with(".js")
|
|
||||||
|| path == "/style.css"
|
|
||||||
|| path == "/highlight.css"
|
|
||||||
{
|
|
||||||
return Some(HeaderValue::from_static("public, max-age=31536000, immutable"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公开页面:5 分钟新鲜期,过期后 1 小时内可提供过期内容并后台重新验证
|
|
||||||
Some(HeaderValue::from_static(
|
|
||||||
"public, max-age=300, stale-while-revalidate=3600",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Axum 中间件:为公开页面和静态资源附加 Cache-Control 头。
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
async fn add_cache_control(
|
|
||||||
req: axum::extract::Request,
|
|
||||||
next: axum::middleware::Next,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
use axum::http::header;
|
|
||||||
|
|
||||||
let path = req.uri().path().to_string();
|
|
||||||
let method = req.method().clone();
|
|
||||||
let cache_value = cache_control_for_path(&path, &method);
|
|
||||||
|
|
||||||
let mut response = next.run(req).await;
|
|
||||||
|
|
||||||
if let Some(value) = cache_value {
|
|
||||||
// 仅当响应尚未设置 Cache-Control 时才添加,避免覆盖已有策略
|
|
||||||
response.headers_mut().entry(header::CACHE_CONTROL).or_insert(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 程序入口
|
/// 程序入口
|
||||||
@ -253,12 +142,13 @@ fn main() {
|
|||||||
let dioxus_app =
|
let dioxus_app =
|
||||||
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
|
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
|
||||||
|
|
||||||
// 合并 Dioxus + 缓存头/可选压缩/30s 超时中间件
|
// 合并 Dioxus + 可选压缩/30s 超时中间件
|
||||||
let mut app_routes = dioxus_app.layer(axum::middleware::from_fn(add_cache_control));
|
let app_routes = if let Some(layer) = compression_layer_from_env() {
|
||||||
if let Some(layer) = compression_layer_from_env() {
|
dioxus_app.layer(layer)
|
||||||
app_routes = app_routes.layer(layer);
|
} else {
|
||||||
|
dioxus_app
|
||||||
}
|
}
|
||||||
let app_routes = app_routes.layer(TimeoutLayer::with_status_code(
|
.layer(TimeoutLayer::with_status_code(
|
||||||
StatusCode::REQUEST_TIMEOUT,
|
StatusCode::REQUEST_TIMEOUT,
|
||||||
Duration::from_secs(30),
|
Duration::from_secs(30),
|
||||||
));
|
));
|
||||||
@ -292,160 +182,3 @@ fn main() {
|
|||||||
dioxus::launch(AppRouter);
|
dioxus::launch(AppRouter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "server"))]
|
|
||||||
mod tests {
|
|
||||||
use super::{cache_control_for_path, parse_compression_algorithms, CompressionAlgorithms};
|
|
||||||
use axum::http::Method;
|
|
||||||
|
|
||||||
fn cache_value(path: &str, method: Method) -> Option<String> {
|
|
||||||
cache_control_for_path(path, &method)
|
|
||||||
.map(|v| v.to_str().unwrap().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn public_page_is_cached() {
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/", Method::GET),
|
|
||||||
Some("public, max-age=300, stale-while-revalidate=3600".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/post/hello-world", Method::GET),
|
|
||||||
Some("public, max-age=300, stale-while-revalidate=3600".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/tags/rust", Method::GET),
|
|
||||||
Some("public, max-age=300, stale-while-revalidate=3600".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn static_assets_are_cached_long_term() {
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/style.css", Method::GET),
|
|
||||||
Some("public, max-age=31536000, immutable".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/highlight.css", Method::GET),
|
|
||||||
Some("public, max-age=31536000, immutable".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/wasm/app.wasm", Method::GET),
|
|
||||||
Some("public, max-age=31536000, immutable".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/_dioxus/assets/main.js", Method::GET),
|
|
||||||
Some("public, max-age=31536000, immutable".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn api_and_admin_and_auth_are_not_cached() {
|
|
||||||
assert_eq!(cache_value("/api/posts", Method::GET), None);
|
|
||||||
assert_eq!(cache_value("/admin", Method::GET), None);
|
|
||||||
assert_eq!(cache_value("/admin/posts", Method::GET), None);
|
|
||||||
assert_eq!(cache_value("/login", Method::GET), None);
|
|
||||||
assert_eq!(cache_value("/register", Method::GET), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn non_get_requests_are_not_cached() {
|
|
||||||
assert_eq!(cache_value("/", Method::POST), None);
|
|
||||||
assert_eq!(cache_value("/post/hello-world", Method::POST), None);
|
|
||||||
assert_eq!(cache_value("/style.css", Method::POST), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn head_requests_are_cached_like_get() {
|
|
||||||
assert_eq!(
|
|
||||||
cache_value("/", Method::HEAD),
|
|
||||||
Some("public, max-age=300, stale-while-revalidate=3600".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_all_enables_everything() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("all"),
|
|
||||||
Some(CompressionAlgorithms::all_enabled())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_default_env_is_all() {
|
|
||||||
// 模拟未设置环境变量时的默认值
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("all"),
|
|
||||||
Some(CompressionAlgorithms::all_enabled())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_empty_none_off_disable() {
|
|
||||||
assert_eq!(parse_compression_algorithms(""), None);
|
|
||||||
assert_eq!(parse_compression_algorithms("none"), None);
|
|
||||||
assert_eq!(parse_compression_algorithms("NONE"), None);
|
|
||||||
assert_eq!(parse_compression_algorithms("off"), None);
|
|
||||||
assert_eq!(parse_compression_algorithms("OFF"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_single_algorithm() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("gzip"),
|
|
||||||
Some(CompressionAlgorithms {
|
|
||||||
gzip: true,
|
|
||||||
brotli: false,
|
|
||||||
deflate: false,
|
|
||||||
zstd: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("br"),
|
|
||||||
Some(CompressionAlgorithms {
|
|
||||||
gzip: false,
|
|
||||||
brotli: true,
|
|
||||||
deflate: false,
|
|
||||||
zstd: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_multiple_algorithms() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("gzip, zstd"),
|
|
||||||
Some(CompressionAlgorithms {
|
|
||||||
gzip: true,
|
|
||||||
brotli: false,
|
|
||||||
deflate: false,
|
|
||||||
zstd: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_case_insensitive_and_whitespace_tolerant() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("GZIP, Brotli, Deflate, Zstd"),
|
|
||||||
Some(CompressionAlgorithms::all_enabled())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms(" gzip , br , deflate , zstd "),
|
|
||||||
Some(CompressionAlgorithms::all_enabled())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compression_unknown_algorithms_are_ignored() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_compression_algorithms("gzip, unknown, lz4"),
|
|
||||||
Some(CompressionAlgorithms {
|
|
||||||
gzip: true,
|
|
||||||
brotli: false,
|
|
||||||
deflate: false,
|
|
||||||
zstd: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user