Files
stash/src/main.rs
T

166 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::path::PathBuf;
use clap::Parser;
use colored::*;
use stash::{
Config,
config::{default_data_dir, display_path, resolve_db_path, validate_db_location},
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,
/// 数据目录(默认是与二进制同级的 data/,用于存放 stash.db)
#[arg(long, value_name = "DIR")]
data: Option<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. 解析数据目录:默认跟随二进制(双击启动时 CWD 不可控),可用 --data 覆盖
let data_dir = match cli.data {
Some(dir) => dir,
None => default_data_dir(),
};
// 先校验再建目录:配置错了就不该在磁盘上留下任何痕迹
let db_path = resolve_db_path(&data_dir);
if let Err(error) = validate_db_location(&abs_root, &db_path) {
eprintln!("{} {}", "[ERROR]".red().bold(), error);
std::process::exit(1);
}
std::fs::create_dir_all(&data_dir)?;
let data_dir = data_dir.canonicalize().unwrap_or(data_dir);
let db_path = resolve_db_path(&data_dir);
// 4. 构造配置并启动
let config = Config {
root: abs_root.clone(),
data_dir: data_dir.clone(),
db_path: db_path.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);
}
};
// 5. 打印启动成功 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!(" ➜ 托管路径: {}", display_path(&abs_root).white());
println!(" ➜ 内容库: {}", display_path(&db_path).white());
println!(" ➜ 前端资源: {}", "已内嵌 (stash-web/dist)".white());
println!(" ➜ 接口列表: {}", format!("http://{}/api/projects", handle.local_addr).dimmed());
println!("{}", "--------------------------------------------------".bright_blue());
println!(" 按 {} 停止服务", "Ctrl+C".yellow().bold());
println!();
// 6. 跨平台捕获退出信号
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 => {},
}
}