salvo-static-files

📁 salvo-rs/salvo-skills 📅 3 days ago
1
总安装量
1
周安装量
#41111
全站排名
安装命令
npx skills add https://github.com/salvo-rs/salvo-skills --skill salvo-static-files

Agent 安装分布

amp 1
opencode 1
cursor 1
kimi-cli 1
codex 1
github-copilot 1

Skill 文档

Salvo Static File Serving

This skill helps serve static files in Salvo applications, including directories, single files, and embedded assets.

Setup

[dependencies]
salvo = { version = "0.89.0", features = ["serve-static"] }

# For embedded files
rust-embed = "8"

Serving a Directory

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;
}

StaticDir Options

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");

Serving a Single File

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:

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;
}

Combined API and Static Files

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;
}

SPA (Single Page Application) Support

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;
}

Serving Different Asset Types

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;
}

File Downloads

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);
        }
    }
}

Directory Listing

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;
}

Conditional Static Serving

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;
}

Multiple Fallback Directories

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");

Embedded Assets with Custom Handling

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);
            }
        }
    }
}

Complete Production Example

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;
}

Best Practices

  1. Use embedded files for deployment: Single binary is easier to deploy
  2. Set cache headers: Long cache for hashed assets, short for dynamic content
  3. Enable compression: Serve gzip/brotli compressed files
  4. SPA fallback: Return index.html for client-side routing
  5. Separate API from static: Use distinct paths for API and static content
  6. Security: Don’t expose sensitive files, check paths
  7. Directory listing: Disable in production unless intentional
  8. Multiple directories: Use fallback order for themes/overrides