模組化重構 Phase 1-2完成:CLI架构分离 + API模块结构建立
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Phase 1:CLI架构重构
- main.rs: 509行 → 21行(简化96%)
- 新增cli模块:框架命令与应用命令分离
  - cli/framework.rs (394行): Display/Render/Config/Scan/Hash/WebDAV/iSCSI
  - cli/apps/download_center.rs (59行): ImportMarkdown/SshServer/Sftp
- 编译成功,CLI命令正确识别(11个命令)

Phase 2:API模块结构创建
- 新增api模块目录结构:api/handlers/
- 为未来handler模块预留空间:
  - tree.rs: FileTree CRUD
  - file.rs: 文件流/渲染
  - upload.rs: 上传处理
  - auth.rs: 认证
  - config.rs: 配置管理
  - system.rs: 系统健康检查
  - view.rs: 分类/系列视图
  - static.rs: 静态页面
- server.rs保持稳定(2409行),降低重构风险

架构优势:
- 清晰的框架/应用分离
- 降低耦合度,便于后续维护
- 为新功能提供清晰的模块空间
- 保持现有功能稳定运行
This commit is contained in:
Warren
2026-06-12 20:59:22 +08:00
parent da62973a43
commit 499efed099
8 changed files with 511 additions and 497 deletions

View File

@@ -0,0 +1,395 @@
use clap::{Parser, Subcommand};
use std::path::Path;
use axum::{extract::Request, response::IntoResponse, routing::any, Extension, Router};
#[derive(Parser)]
#[command(name = "markbase", about = "Momentry Display Engine")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(flatten)]
Framework(FrameworkCommands),
#[command(flatten)]
App(crate::cli::apps::AppCommands),
}
#[derive(Subcommand)]
pub enum FrameworkCommands {
Display {
#[arg(short, long, default_value = "11438")]
port: u16,
#[arg(short, long)]
file: Option<String>,
},
Render {
file: String,
#[arg(short, long)]
output: Option<String>,
},
Config {
#[command(subcommand)]
action: ConfigCommands,
},
Scan {
#[arg(short, long)]
user: String,
#[arg(short, long)]
dir: String,
#[arg(short, long, default_value = "100")]
batch: usize,
#[arg(short, long, default_value = "true")]
skip_hash: bool,
#[arg(short, long, default_value = "4")]
threads: usize,
},
Hash {
#[arg(short, long)]
user: String,
#[arg(short, long, default_value = "4")]
threads: usize,
},
BcryptTest {
#[arg(short, long, default_value = "demo123")]
password: String,
#[arg(long)]
verify_hash: Option<String>,
},
WebDAV {
#[command(subcommand)]
action: WebDAVCommands,
},
Iscsi {
#[command(subcommand)]
action: IscsiCommands,
},
}
#[derive(Subcommand)]
pub enum WebDAVCommands {
Start {
#[arg(short, long, default_value = "8002")]
port: u16,
#[arg(short, long)]
user: String,
},
}
#[derive(Subcommand)]
pub enum IscsiCommands {
Start {
#[arg(short, long)]
user: String,
#[arg(short, long, default_value = "3260")]
port: u16,
#[arg(short, long, default_value = "5GB")]
lun_size: String,
#[arg(short, long)]
force: bool,
#[arg(long)]
device: Option<String>,
},
Stop,
Status,
}
#[derive(Subcommand)]
pub enum ConfigCommands {
Init {
#[arg(short, long)]
force: bool,
},
Show {
#[arg(short, long)]
section: Option<String>,
},
Edit {
#[arg(short, long)]
key: String,
#[arg(short, long)]
value: String,
},
Validate,
}
pub async fn handle_framework_command(cmd: FrameworkCommands) -> anyhow::Result<()> {
match cmd {
FrameworkCommands::Display { port, file } => {
crate::server::run(port, file).await?;
}
FrameworkCommands::Render { file, output } => {
let md = std::fs::read_to_string(&file)?;
let html = crate::render::md_to_html(&md);
if let Some(path) = &output {
std::fs::write(path, html)?;
} else {
println!("{html}");
}
}
FrameworkCommands::Config { action } => {
handle_config_command(action)?;
}
FrameworkCommands::Scan {
user,
dir,
batch,
skip_hash,
threads,
} => {
use crate::scan::ScanOptions;
let options = ScanOptions { skip_hash, threads };
crate::scan::scan_directory(&user, &dir, batch, options)?;
}
FrameworkCommands::Hash { user, threads } => {
crate::scan::compute_hashes(&user, threads)?;
}
FrameworkCommands::BcryptTest { password, verify_hash } => {
handle_bcrypt_test(password, verify_hash)?;
}
FrameworkCommands::WebDAV { action } => {
handle_webdav_command(action).await?;
}
FrameworkCommands::Iscsi { action } => {
handle_iscsi_command(action)?;
}
}
Ok(())
}
fn handle_config_command(action: ConfigCommands) -> anyhow::Result<()> {
match action {
ConfigCommands::Init { force } => {
let config_path = Path::new("config/markbase.toml");
if config_path.exists() && !force {
println!("Configuration file already exists at config/markbase.toml");
println!("Use --force to overwrite");
return Ok(());
}
let config = crate::config::MarkBaseConfig::default_config();
config.save(config_path)?;
println!("✓ Configuration file created: config/markbase.toml");
println!("Default values:");
println!(" Server port: {}", config.server.port);
println!(" PostgreSQL host: {}", config.postgresql.host);
println!(" Test users: {}", config.test.users.join(", "));
}
ConfigCommands::Show { section } => {
let config_path = Path::new("config/markbase.toml");
if !config_path.exists() {
println!("Configuration file not found. Run 'markbase config init' first.");
return Ok(());
}
let config = crate::config::MarkBaseConfig::load(config_path)?;
if let Some(s) = section {
show_section(&config, &s);
} else {
println!("{}", toml::to_string_pretty(&config)?);
}
}
ConfigCommands::Edit { key, value } => {
let config_path = Path::new("config/markbase.toml");
if !config_path.exists() {
println!("Configuration file not found. Run 'markbase config init' first.");
return Ok(());
}
let mut config = crate::config::MarkBaseConfig::load(config_path)?;
match config.get(&key) {
Some(old_value) => {
config.set(&key, &value)?;
config.validate()?;
config.save(config_path)?;
println!("✓ Updated {}: {}{}", key, old_value, value);
}
None => {
println!("Invalid config key: {}", key);
println!(
"Valid keys: server.*, postgresql.*, authentication.*, test.*, logging.*"
);
}
}
}
ConfigCommands::Validate => {
let config_path = Path::new("config/markbase.toml");
if !config_path.exists() {
println!("Configuration file not found. Run 'markbase config init' first.");
return Ok(());
}
let config = crate::config::MarkBaseConfig::load(config_path)?;
match config.validate() {
Ok(_) => {
println!("✓ Configuration is valid");
}
Err(e) => {
println!("✗ Configuration validation failed: {}", e);
}
}
}
}
Ok(())
}
fn show_section(config: &crate::config::MarkBaseConfig, section: &str) {
match section {
"server" => println!("{}", toml::to_string_pretty(&config.server).unwrap()),
"postgresql" => println!("{}", toml::to_string_pretty(&config.postgresql).unwrap()),
"authentication" => println!("{}", toml::to_string_pretty(&config.authentication).unwrap()),
"test" => println!("{}", toml::to_string_pretty(&config.test).unwrap()),
"logging" => println!("{}", toml::to_string_pretty(&config.logging).unwrap()),
_ => println!("Invalid section: {}. Valid sections: server, postgresql, authentication, test, logging", section),
}
}
fn handle_bcrypt_test(password: String, verify_hash: Option<String>) -> anyhow::Result<()> {
use bcrypt::{hash, verify, DEFAULT_COST};
println!("=== bcrypt Hash Test ===");
println!("Password: {}", password);
println!("");
let new_hash = hash(&password, DEFAULT_COST)?;
println!("Generated hash:");
println!("{}", new_hash);
println!("");
if let Some(hash_to_verify) = verify_hash {
println!("Verifying hash: {}", hash_to_verify);
let valid = verify(&password, &hash_to_verify)?;
println!("Valid: {}", valid);
println!("");
}
let db_hash = "$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6";
println!("Database hash: {}", db_hash);
let valid = verify(&password, db_hash)?;
println!("Database hash valid for '{}': {}", password, valid);
println!("");
if !valid {
println!("❌ Database hash is incorrect!");
println!("Update SQL:");
println!("UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');", new_hash);
println!("");
println!("Execute:");
println!("sqlite3 data/auth.sqlite \"UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');\"", new_hash);
} else {
println!("✅ Database hash is correct!");
}
Ok(())
}
async fn handle_webdav_command(action: WebDAVCommands) -> anyhow::Result<()> {
match action {
WebDAVCommands::Start { port, user } => {
let db_path = std::path::PathBuf::from(crate::FileTree::user_db_path(&user));
if !db_path.exists() {
return Err(anyhow::anyhow!(
"User database not found: {}",
db_path.display()
));
}
println!("=== MarkBase WebDAV Server ===");
println!("User: {}", user);
println!("Port: {}", port);
println!("Database: {}", db_path.display());
println!("");
run_webdav_server(port, user, db_path).await?;
}
}
Ok(())
}
async fn run_webdav_server(
port: u16,
user: String,
db_path: std::path::PathBuf,
) -> anyhow::Result<()> {
use tokio::net::TcpListener;
let webdav = markbase_webdav::webdav::MarkBaseWebDAV::new(user, db_path);
let dav_handler = webdav.create_handler();
let app = Router::new()
.route("/webdav", any(handle_dav))
.route("/webdav/", any(handle_dav))
.route("/webdav/*path", any(handle_dav))
.layer(Extension(dav_handler));
let addr = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(&addr).await?;
println!("WebDAV server listening on http://{}", addr);
println!("Mount point: /webdav");
println!("");
println!("Press Ctrl+C to stop");
axum::serve(listener, app).await?;
Ok(())
}
async fn handle_dav(
Extension(dav): Extension<dav_server::DavHandler>,
req: Request,
) -> impl IntoResponse {
dav.handle(req).await
}
fn handle_iscsi_command(action: IscsiCommands) -> anyhow::Result<()> {
let binary = find_binary("markbase-iscsi");
let mut cmd = std::process::Command::new(&binary);
cmd.arg("iscsi");
match action {
IscsiCommands::Start {
user,
port,
lun_size,
force,
device,
} => {
cmd.arg("start")
.args(["--user", &user])
.args(["--port", &port.to_string()])
.args(["--lun-size", &lun_size]);
if force {
cmd.arg("--force");
}
if let Some(d) = device {
cmd.args(["--device", &d]);
}
}
IscsiCommands::Stop => {
cmd.arg("stop");
}
IscsiCommands::Status => {
cmd.arg("status");
}
}
let status = cmd.status()?;
std::process::exit(status.code().unwrap_or(1));
}
fn find_binary(name: &str) -> std::path::PathBuf {
let exe = std::env::current_exe().unwrap();
let dir = exe.parent().unwrap();
dir.join(name)
}