salvo-static-files
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSalvo Static File Serving
Salvo 静态文件服务
This skill helps serve static files in Salvo applications, including directories, single files, and embedded assets.
本技能可帮助在Salvo应用中提供静态文件服务,包括目录、单个文件和嵌入式资源。
Setup
配置
toml
[dependencies]
salvo = { version = "0.89.0", features = ["serve-static"] }toml
[dependencies]
salvo = { version = "0.89.0", features = ["serve-static"] }For embedded files
用于嵌入式文件
rust-embed = "8"
undefinedrust-embed = "8"
undefinedServing a Directory
托管目录
rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
StaticDir::new(["static", "public"]) // Multiple fallback directories
.defaults("index.html") // Default file for directories
.auto_list(true) // Enable directory listing
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
StaticDir::new(["static", "public"]) // 多个备选目录
.defaults("index.html") // 目录的默认文件
.auto_list(true) // 启用目录列表
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}StaticDir Options
StaticDir 配置选项
rust
use salvo::serve_static::StaticDir;
let static_handler = StaticDir::new(["static"])
// Default file when accessing directories
.defaults("index.html")
// Enable directory listing
.auto_list(true)
// Include hidden files (starting with .)
.include_dot_files(false)
// Set cache control headers
.cache_control("max-age=3600");rust
use salvo::serve_static::StaticDir;
let static_handler = StaticDir::new(["static"])
// 访问目录时的默认文件
.defaults("index.html")
// 启用目录列表
.auto_list(true)
// 包含隐藏文件(以.开头)
.include_dot_files(false)
// 设置缓存控制头
.cache_control("max-age=3600");Serving a Single File
托管单个文件
rust
use salvo::prelude::*;
use salvo::serve_static::StaticFile;
#[tokio::main]
async fn main() {
let router = Router::new()
.push(Router::with_path("favicon.ico").get(StaticFile::new("static/favicon.ico")))
.push(Router::with_path("robots.txt").get(StaticFile::new("static/robots.txt")));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticFile;
#[tokio::main]
async fn main() {
let router = Router::new()
.push(Router::with_path("favicon.ico").get(StaticFile::new("static/favicon.ico")))
.push(Router::with_path("robots.txt").get(StaticFile::new("static/robots.txt")));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Embedded Static Files
嵌入式静态文件
Embed files at compile time for single-binary deployment:
rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::static_embed;
#[derive(RustEmbed)]
#[folder = "static"] // Folder to embed
struct Assets;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html") // SPA fallback
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}在编译时嵌入文件,实现单二进制文件部署:
rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::static_embed;
#[derive(RustEmbed)]
#[folder = "static"] // 要嵌入的文件夹
struct Assets;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html") // SPA 回退页面
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Combined API and Static Files
API 与静态文件结合
rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[handler]
async fn api_users() -> Json<Vec<String>> {
Json(vec!["Alice".to_string(), "Bob".to_string()])
}
#[handler]
async fn api_posts() -> Json<Vec<String>> {
Json(vec!["Post 1".to_string(), "Post 2".to_string()])
}
#[tokio::main]
async fn main() {
let router = Router::new()
// API routes
.push(
Router::with_path("api")
.push(Router::with_path("users").get(api_users))
.push(Router::with_path("posts").get(api_posts))
)
// Static files for everything else
.push(
Router::with_path("{*path}").get(
StaticDir::new(["static"])
.defaults("index.html")
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[handler]
async fn api_users() -> Json<Vec<String>> {
Json(vec!["Alice".to_string(), "Bob".to_string()])
}
#[handler]
async fn api_posts() -> Json<Vec<String>> {
Json(vec!["Post 1".to_string(), "Post 2".to_string()])
}
#[tokio::main]
async fn main() {
let router = Router::new()
// API 路由
.push(
Router::with_path("api")
.push(Router::with_path("users").get(api_users))
.push(Router::with_path("posts").get(api_posts))
)
// 其他路径均返回静态文件
.push(
Router::with_path("{*path}").get(
StaticDir::new(["static"])
.defaults("index.html")
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}SPA (Single Page Application) Support
SPA(单页应用)支持
rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::static_embed;
#[derive(RustEmbed)]
#[folder = "dist"] // Vue/React build output
struct Assets;
#[tokio::main]
async fn main() {
let router = Router::new()
// API routes first
.push(Router::with_path("api/{**rest}").get(api_handler))
// SPA - serve index.html for all other routes
.push(
Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html") // All routes fall back to index.html
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::static_embed;
#[derive(RustEmbed)]
#[folder = "dist"] // Vue/React 构建输出目录
struct Assets;
#[tokio::main]
async fn main() {
let router = Router::new()
// 优先匹配API路由
.push(Router::with_path("api/{**rest}").get(api_handler))
// SPA - 所有其他路由均返回index.html
.push(
Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html") // 所有路由回退到index.html
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Serving Different Asset Types
托管不同类型的资源
rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::new()
// CSS files
.push(
Router::with_path("css/{*path}").get(
StaticDir::new(["static/css"])
.cache_control("max-age=31536000") // 1 year for hashed assets
)
)
// JavaScript files
.push(
Router::with_path("js/{*path}").get(
StaticDir::new(["static/js"])
.cache_control("max-age=31536000")
)
)
// Images
.push(
Router::with_path("images/{*path}").get(
StaticDir::new(["static/images"])
.cache_control("max-age=86400") // 1 day
)
)
// Uploads (user content, no long cache)
.push(
Router::with_path("uploads/{*path}").get(
StaticDir::new(["uploads"])
.cache_control("max-age=3600") // 1 hour
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::new()
// CSS文件
.push(
Router::with_path("css/{*path}").get(
StaticDir::new(["static/css"])
.cache_control("max-age=31536000") // 带哈希的资源缓存1年
)
)
// JavaScript文件
.push(
Router::with_path("js/{*path}").get(
StaticDir::new(["static/js"])
.cache_control("max-age=31536000")
)
)
// 图片
.push(
Router::with_path("images/{*path}").get(
StaticDir::new(["static/images"])
.cache_control("max-age=86400") // 缓存1天
)
)
// 上传文件(用户内容,不长期缓存)
.push(
Router::with_path("uploads/{*path}").get(
StaticDir::new(["uploads"])
.cache_control("max-age=3600") // 缓存1小时
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}File Downloads
文件下载
rust
use salvo::prelude::*;
use salvo::fs::NamedFile;
#[handler]
async fn download_file(req: &mut Request, res: &mut Response) {
let filename: String = req.param("filename").unwrap();
let file_path = format!("downloads/{}", filename);
// Serve file with download headers
match NamedFile::builder(&file_path)
.attached_name(&filename) // Forces download with filename
.send(req.headers(), res)
.await
{
Ok(_) => {}
Err(_) => {
res.status_code(StatusCode::NOT_FOUND);
res.render("File not found");
}
}
}
#[handler]
async fn view_pdf(req: &mut Request, res: &mut Response) {
// Serve PDF for viewing in browser (not download)
match NamedFile::builder("documents/report.pdf")
.content_type("application/pdf")
.send(req.headers(), res)
.await
{
Ok(_) => {}
Err(_) => {
res.status_code(StatusCode::NOT_FOUND);
}
}
}rust
use salvo::prelude::*;
use salvo::fs::NamedFile;
#[handler]
async fn download_file(req: &mut Request, res: &mut Response) {
let filename: String = req.param("filename").unwrap();
let file_path = format!("downloads/{}", filename);
// 发送带有下载头的文件
match NamedFile::builder(&file_path)
.attached_name(&filename) // 强制以指定文件名下载
.send(req.headers(), res)
.await
{
Ok(_) => {}
Err(_) => {
res.status_code(StatusCode::NOT_FOUND);
res.render("文件不存在");
}
}
}
#[handler]
async fn view_pdf(req: &mut Request, res: &mut Response) {
// 在浏览器中预览PDF(不触发下载)
match NamedFile::builder("documents/report.pdf")
.content_type("application/pdf")
.send(req.headers(), res)
.await
{
Ok(_) => {}
Err(_) => {
res.status_code(StatusCode::NOT_FOUND);
}
}
}Directory Listing
目录列表
rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
StaticDir::new(["files"])
.auto_list(true) // Enable directory listing
.include_dot_files(false) // Hide hidden files
.defaults("index.html") // Show index.html if exists
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[tokio::main]
async fn main() {
let router = Router::with_path("{*path}").get(
StaticDir::new(["files"])
.auto_list(true) // 启用目录列表
.include_dot_files(false) // 隐藏隐藏文件
.defaults("index.html") // 如果存在则显示index.html
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Conditional Static Serving
条件式静态文件服务
rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[handler]
async fn check_auth(
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
// Check if user is authenticated for protected files
let is_authenticated = depot
.session_mut()
.and_then(|s| s.get::<bool>("logged_in"))
.unwrap_or(false);
if !is_authenticated {
res.status_code(StatusCode::UNAUTHORIZED);
res.render("Please login to access files");
ctrl.skip_rest();
}
}
#[tokio::main]
async fn main() {
let router = Router::new()
// Public static files
.push(
Router::with_path("public/{*path}").get(
StaticDir::new(["static/public"])
)
)
// Protected static files
.push(
Router::with_path("private/{*path}")
.hoop(check_auth)
.get(StaticDir::new(["static/private"]))
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use salvo::prelude::*;
use salvo::serve_static::StaticDir;
#[handler]
async fn check_auth(
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
// 检查用户是否有权访问受保护的文件
let is_authenticated = depot
.session_mut()
.and_then(|s| s.get::<bool>("logged_in"))
.unwrap_or(false);
if !is_authenticated {
res.status_code(StatusCode::UNAUTHORIZED);
res.render("请登录后访问文件");
ctrl.skip_rest();
}
}
#[tokio::main]
async fn main() {
let router = Router::new()
// 公开静态文件
.push(
Router::with_path("public/{*path}").get(
StaticDir::new(["static/public"])
)
)
// 受保护的静态文件
.push(
Router::with_path("private/{*path}")
.hoop(check_auth)
.get(StaticDir::new(["static/private"]))
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Multiple Fallback Directories
多备选目录
rust
use salvo::serve_static::StaticDir;
// Try directories in order
let static_handler = StaticDir::new([
"static/overrides", // Custom overrides first
"static/default", // Default files second
"node_modules", // npm packages last
])
.defaults("index.html");rust
use salvo::serve_static::StaticDir;
// 按顺序尝试目录
let static_handler = StaticDir::new([
"static/overrides", // 优先使用自定义覆盖文件
"static/default", // 其次是默认文件
"node_modules", // 最后是npm包
])
.defaults("index.html");Embedded Assets with Custom Handling
自定义处理嵌入式资源
rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
#[derive(RustEmbed)]
#[folder = "static"]
struct Assets;
#[handler]
async fn custom_static(req: &mut Request, res: &mut Response) {
let path = req.param::<String>("path").unwrap_or_default();
match Assets::get(&path) {
Some(content) => {
// Determine content type
let content_type = mime_guess::from_path(&path)
.first_or_octet_stream()
.to_string();
res.headers_mut()
.insert("Content-Type", content_type.parse().unwrap());
// Add caching for production
if path.contains(".") { // Has extension = asset
res.headers_mut()
.insert("Cache-Control", "max-age=31536000".parse().unwrap());
}
res.write_body(content.data.to_vec()).ok();
}
None => {
// SPA fallback
if let Some(index) = Assets::get("index.html") {
res.headers_mut()
.insert("Content-Type", "text/html".parse().unwrap());
res.write_body(index.data.to_vec()).ok();
} else {
res.status_code(StatusCode::NOT_FOUND);
}
}
}
}rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
#[derive(RustEmbed)]
#[folder = "static"]
struct Assets;
#[handler]
async fn custom_static(req: &mut Request, res: &mut Response) {
let path = req.param::<String>("path").unwrap_or_default();
match Assets::get(&path) {
Some(content) => {
// 确定内容类型
let content_type = mime_guess::from_path(&path)
.first_or_octet_stream()
.to_string();
res.headers_mut()
.insert("Content-Type", content_type.parse().unwrap());
// 为生产环境添加缓存
if path.contains(".") { // 带扩展名的文件属于资源
res.headers_mut()
.insert("Cache-Control", "max-age=31536000".parse().unwrap());
}
res.write_body(content.data.to_vec()).ok();
}
None => {
// SPA 回退
if let Some(index) = Assets::get("index.html") {
res.headers_mut()
.insert("Content-Type", "text/html".parse().unwrap());
res.write_body(index.data.to_vec()).ok();
} else {
res.status_code(StatusCode::NOT_FOUND);
}
}
}
}Complete Production Example
完整生产环境示例
rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::{StaticDir, static_embed};
use salvo::compression::Compression;
#[derive(RustEmbed)]
#[folder = "dist"]
struct Assets;
#[handler]
async fn api_handler() -> &'static str {
"API Response"
}
#[tokio::main]
async fn main() {
// Compression for all responses
let compression = Compression::new()
.enable_gzip(flate2::Compression::default())
.enable_brotli(11);
let router = Router::new()
.hoop(compression)
// API routes
.push(
Router::with_path("api")
.push(Router::with_path("data").get(api_handler))
)
// Uploads (not embedded)
.push(
Router::with_path("uploads/{*path}").get(
StaticDir::new(["uploads"])
.cache_control("max-age=3600")
)
)
// Embedded static files with SPA support
.push(
Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html")
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}rust
use rust_embed::RustEmbed;
use salvo::prelude::*;
use salvo::serve_static::{StaticDir, static_embed};
use salvo::compression::Compression;
#[derive(RustEmbed)]
#[folder = "dist"]
struct Assets;
#[handler]
async fn api_handler() -> &'static str {
"API Response"
}
#[tokio::main]
async fn main() {
// 为所有响应启用压缩
let compression = Compression::new()
.enable_gzip(flate2::Compression::default())
.enable_brotli(11);
let router = Router::new()
.hoop(compression)
// API 路由
.push(
Router::with_path("api")
.push(Router::with_path("data").get(api_handler))
)
// 上传文件(不嵌入)
.push(
Router::with_path("uploads/{*path}").get(
StaticDir::new(["uploads"])
.cache_control("max-age=3600")
)
)
// 带SPA支持的嵌入式静态文件
.push(
Router::with_path("{*path}").get(
static_embed::<Assets>()
.fallback("index.html")
)
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Best Practices
最佳实践
- Use embedded files for deployment: Single binary is easier to deploy
- Set cache headers: Long cache for hashed assets, short for dynamic content
- Enable compression: Serve gzip/brotli compressed files
- SPA fallback: Return index.html for client-side routing
- Separate API from static: Use distinct paths for API and static content
- Security: Don't expose sensitive files, check paths
- Directory listing: Disable in production unless intentional
- Multiple directories: Use fallback order for themes/overrides
- 部署时使用嵌入式文件:单二进制文件更易于部署
- 设置缓存头:带哈希的资源使用长缓存,动态内容使用短缓存
- 启用压缩:提供gzip/brotli压缩后的文件
- SPA回退:为客户端路由返回index.html
- 分离API与静态文件:为API和静态文件使用不同的路径
- 安全性:不要暴露敏感文件,检查路径合法性
- 目录列表:生产环境中除非必要,否则禁用
- 多目录配置:使用回退顺序实现主题/覆盖功能