refactor: 合并为单 crate,前端改为内嵌 React 控制台

合并 stash-cli / stash-core 为根目录单 crate(lib + bin),新增 React + Vite + TS 控制台替换 Askama 模板。
build.rs 用 walkdir 把 stash-web/dist 同步到 $OUT_DIR/web,由 include_dir! 编译期内嵌,产出单文件二进制。
路由改为 / 与 /assets/** 走内嵌前端,其余交给 ServeDir,缺失路径返回内置 404。同步更新依赖、.gitignore 与 README。
This commit is contained in:
2026-10-21 17:59:08 +08:00
parent 92c510e959
commit e522a98f6f
48 changed files with 5113 additions and 273 deletions
+138
View File
@@ -0,0 +1,138 @@
use std::path::PathBuf;
use clap::Parser;
use colored::*;
use stash::{Config, start};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser, Debug)]
#[command(
name = "stash",
author = "stash developers",
version,
about = "🚀 本地静态页面托管与多项目管理工具",
long_about = "支持本地静态页面按目录隔离托管,支持前端直接调用同源接口,提供清爽的项目导航列表。"
)]
struct Cli {
/// 静态页面托管根目录(支持相对路径和绝对路径)
#[arg(short, long, default_value = "./public", value_name = "DIR")]
root: PathBuf,
/// 监听端口
#[arg(short, long, default_value_t = 8080, value_name = "PORT")]
port: u16,
/// 绑定监听地址(默认 127.0.0.1,如需局域网访问可传 0.0.0.0)
#[arg(long, default_value = "127.0.0.1", value_name = "HOST")]
host: String,
/// 关闭根目录项目导航列表功能(仅托管静态文件)
#[arg(long, default_value_t = false)]
no_listing: bool,
/// 关闭 CORS 跨域标头注入
#[arg(long, default_value_t = false)]
no_cors: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. 初始化 tracing 日志
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
// 2. 解析命令行输入
let cli = Cli::parse();
// 检查目录是否存在,不存在则友好提示并自动创建默认 public
if !cli.root.exists() {
if cli.root == PathBuf::from("./public") {
println!(
"{} 默认托管目录 {} 不存在,正在自动创建...",
"[INFO]".cyan(),
"./public".yellow()
);
std::fs::create_dir_all(&cli.root)?;
} else {
eprintln!(
"{} 指定的托管目录不存在: {}",
"[ERROR]".red().bold(),
cli.root.display().to_string().yellow()
);
std::process::exit(1);
}
}
let abs_root = cli.root.canonicalize().unwrap_or_else(|_| cli.root.clone());
// 3. 构造 stash-core 配置并启动
let config = Config {
root: abs_root.clone(),
port: cli.port,
host: cli.host,
directory_listing: !cli.no_listing,
enable_cors: !cli.no_cors,
};
let handle = match start(config).await {
Ok(h) => h,
Err(err) => {
eprintln!("{} 启动失败: {}", "[FATAL]".red().bold(), err);
std::process::exit(1);
}
};
// 4. 打印启动成功 Banner
println!();
println!("{}", "==================================================".bright_blue());
println!(" ✨ {} {}", "Stash Server".green().bold(), format!("v{}", env!("CARGO_PKG_VERSION")).dimmed());
println!("{}", "==================================================".bright_blue());
println!(" ➜ 本地访问: {}", format!("http://{}", handle.local_addr).cyan().bold().underline());
println!(" ➜ 托管路径: {}", abs_root.display().to_string().white());
println!(" ➜ 前端资源: {}", "已内嵌 (stash-web/dist)".white());
println!(" ➜ 接口列表: {}", format!("http://{}/api/projects", handle.local_addr).dimmed());
println!("{}", "--------------------------------------------------".bright_blue());
println!(" 按 {} 停止服务", "Ctrl+C".yellow().bold());
println!();
// 5. 跨平台捕获退出信号
wait_for_shutdown_signal().await;
println!("\n{} 正在停止服务并释放端口...", "[SYS]".yellow());
handle.stop();
// 留出时间让 tokio 优雅关闭 socket 句柄
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
println!("{} 服务已安全退出。\n", "[DONE]".green().bold());
Ok(())
}
/// 监听停机信号(支持 Windows Ctrl+C 以及 Linux/macOS SIGINT/SIGTERM)
async fn wait_for_shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("无法注册 Ctrl+C 信号处理器");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("无法注册 SIGTERM 信号处理器")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}