模組化重構 Phase 1-2完成:CLI架构分离 + API模块结构建立
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:
16
markbase-core/src/api/handlers/mod.rs
Normal file
16
markbase-core/src/api/handlers/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
// API Handlers Module
|
||||
//
|
||||
// This module provides space for future modular API handlers.
|
||||
// Current handlers are implemented in server.rs for stability.
|
||||
//
|
||||
// Future migration plan:
|
||||
// - tree.rs: FileTree CRUD operations
|
||||
// - file.rs: File streaming/rendering
|
||||
// - upload.rs: File upload handling
|
||||
// - auth.rs: Authentication handlers
|
||||
// - config.rs: Configuration management
|
||||
// - system.rs: System health/metrics
|
||||
// - view.rs: Category/Series view handlers
|
||||
// - static.rs: Static page handlers
|
||||
|
||||
pub use crate::server::AppState;
|
||||
12
markbase-core/src/api/mod.rs
Normal file
12
markbase-core/src/api/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod handlers;
|
||||
|
||||
// API Module - Future Modular Architecture
|
||||
//
|
||||
// This module provides the structure for modular API handlers.
|
||||
// Current implementation remains in server.rs for stability.
|
||||
//
|
||||
// Benefits of this architecture:
|
||||
// - Clear separation of concerns
|
||||
// - Easier maintenance for new features
|
||||
// - Gradual migration path from server.rs
|
||||
// - Independent testing per handler module
|
||||
60
markbase-core/src/cli/apps/download_center.rs
Normal file
60
markbase-core/src/cli/apps/download_center.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use clap::Subcommand;
|
||||
use rusqlite::Connection;
|
||||
use anyhow::Context;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DownloadCenterCommands {
|
||||
ImportMarkdown {
|
||||
#[arg(short, long, default_value = "accusys")]
|
||||
user: String,
|
||||
#[arg(short, long)]
|
||||
tree_type: String,
|
||||
},
|
||||
SshServer {
|
||||
#[arg(short, long, default_value = "2024")]
|
||||
port: u16,
|
||||
},
|
||||
Sftp {
|
||||
#[arg(short, long, default_value = "2023")]
|
||||
port: u16,
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn handle_download_center_command(cmd: DownloadCenterCommands) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
DownloadCenterCommands::ImportMarkdown { user, tree_type } => {
|
||||
let db_path = format!("data/users/{}.sqlite", user);
|
||||
let conn = Connection::open(&db_path)
|
||||
.with_context(|| format!("Failed to open database: {}", db_path))?;
|
||||
|
||||
println!("Importing Markdown files to {} virtual tree...", tree_type);
|
||||
|
||||
if tree_type == "categories" {
|
||||
crate::import_markdown::import_categories_to_db(&conn, &user, &tree_type)?;
|
||||
println!("Categories imported successfully!");
|
||||
} else if tree_type == "series" {
|
||||
crate::import_markdown::import_series_to_db(&conn, &user, &tree_type)?;
|
||||
println!("Series imported successfully!");
|
||||
} else {
|
||||
eprintln!("Invalid tree_type: {}. Use 'categories' or 'series'", tree_type);
|
||||
}
|
||||
}
|
||||
DownloadCenterCommands::SshServer { port } => {
|
||||
println!("=== MarkBase SSH Server (Hand-written Implementation) ===");
|
||||
println!("Port: {}", port);
|
||||
println!("Implementation: SSH-2.0-MarkBaseSSH_1.0");
|
||||
println!("Features: SSH + SFTP + SCP + rsync");
|
||||
println!("Security: ⭐⭐⭐⭐⭐ (RustCrypto authoritative libraries)");
|
||||
println!();
|
||||
|
||||
crate::ssh_server::server::run_ssh_server(Some(port))?;
|
||||
}
|
||||
DownloadCenterCommands::Sftp { port, user } => {
|
||||
println!("SFTP server command is currently disabled (old implementation)");
|
||||
println!("Use 'ssh-server' command for the new SSH+SFTP implementation");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
11
markbase-core/src/cli/apps/mod.rs
Normal file
11
markbase-core/src/cli/apps/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod download_center;
|
||||
|
||||
pub use download_center::{DownloadCenterCommands, handle_download_center_command};
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AppCommands {
|
||||
#[command(flatten)]
|
||||
DownloadCenter(DownloadCenterCommands),
|
||||
}
|
||||
395
markbase-core/src/cli/framework.rs
Normal file
395
markbase-core/src/cli/framework.rs
Normal 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)
|
||||
}
|
||||
5
markbase-core/src/cli/mod.rs
Normal file
5
markbase-core/src/cli/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod framework;
|
||||
pub mod apps;
|
||||
|
||||
pub use framework::{Cli, Commands, FrameworkCommands};
|
||||
pub use apps::{AppCommands, DownloadCenterCommands};
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod audio;
|
||||
pub mod auth;
|
||||
pub mod audit;
|
||||
pub mod cli;
|
||||
pub mod api;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod download;
|
||||
|
||||
@@ -1,509 +1,22 @@
|
||||
use axum::{extract::Request, response::IntoResponse, routing::any, Extension, Router};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "markbase", about = "Momentry Display Engine")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Start display server
|
||||
Display {
|
||||
#[arg(short, long, default_value = "11438")]
|
||||
port: u16,
|
||||
/// Optional initial markdown file
|
||||
file: Option<String>,
|
||||
},
|
||||
/// Render markdown to HTML (stdout)
|
||||
Render {
|
||||
file: String,
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Configuration management
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigCommands,
|
||||
},
|
||||
/// Scan and import files from directory
|
||||
Scan {
|
||||
/// User ID
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
/// Directory to scan
|
||||
#[arg(short, long)]
|
||||
dir: String,
|
||||
/// Batch size for database insertion
|
||||
#[arg(short, long, default_value = "100")]
|
||||
batch: usize,
|
||||
/// Skip SHA256 hash calculation (faster import)
|
||||
#[arg(short, long, default_value = "true")]
|
||||
skip_hash: bool,
|
||||
/// Number of threads for hash calculation (if skip_hash=false)
|
||||
#[arg(short, long, default_value = "4")]
|
||||
threads: usize,
|
||||
},
|
||||
/// Compute SHA256 hashes for imported files
|
||||
Hash {
|
||||
/// User ID
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
/// Number of threads for parallel hash calculation
|
||||
#[arg(short, long, default_value = "4")]
|
||||
threads: usize,
|
||||
},
|
||||
/// Start WebDAV server
|
||||
WebDAV {
|
||||
#[command(subcommand)]
|
||||
action: WebDAVCommands,
|
||||
},
|
||||
/// Manage iSCSI target (gotgt)
|
||||
Iscsi {
|
||||
#[command(subcommand)]
|
||||
action: IscsiCommands,
|
||||
},
|
||||
/// Start SFTP server
|
||||
Sftp {
|
||||
/// Port to listen on
|
||||
#[arg(short, long, default_value = "2023")]
|
||||
port: u16,
|
||||
/// User ID for database
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
},
|
||||
/// Test bcrypt password hash
|
||||
BcryptTest {
|
||||
/// Password to hash
|
||||
#[arg(short, long, default_value = "demo123")]
|
||||
password: String,
|
||||
/// Hash to verify (optional)
|
||||
#[arg(long)]
|
||||
verify_hash: Option<String>,
|
||||
},
|
||||
/// Start SSH server (hand-written implementation)
|
||||
SshServer {
|
||||
/// Port to listen on (default 2024)
|
||||
#[arg(short, long, default_value = "2024")]
|
||||
port: u16,
|
||||
},
|
||||
/// Import Markdown files to categories/series virtual trees
|
||||
ImportMarkdown {
|
||||
/// User ID (default: accusys)
|
||||
#[arg(short, long, default_value = "accusys")]
|
||||
user: String,
|
||||
/// Tree type (categories or series)
|
||||
#[arg(short, long)]
|
||||
tree_type: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum IscsiCommands {
|
||||
/// Start iSCSI target daemon
|
||||
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,
|
||||
/// Block device path (e.g., /dev/disk5). Overrides file-backed LUN.
|
||||
#[arg(long)]
|
||||
device: Option<String>,
|
||||
},
|
||||
/// Stop iSCSI target daemon
|
||||
Stop,
|
||||
/// Show iSCSI target status
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum WebDAVCommands {
|
||||
/// Start WebDAV server for user
|
||||
Start {
|
||||
/// Port to listen on
|
||||
#[arg(short, long, default_value = "8002")]
|
||||
port: u16,
|
||||
/// User ID for database
|
||||
#[arg(short, long)]
|
||||
user: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ConfigCommands {
|
||||
/// Initialize default configuration file
|
||||
Init {
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
/// Show current configuration
|
||||
Show {
|
||||
#[arg(short, long)]
|
||||
section: Option<String>,
|
||||
},
|
||||
/// Edit configuration
|
||||
Edit {
|
||||
#[arg(short, long)]
|
||||
key: String,
|
||||
#[arg(short, long)]
|
||||
value: String,
|
||||
},
|
||||
/// Validate configuration
|
||||
Validate,
|
||||
}
|
||||
use markbase_core::cli::{Cli, Commands};
|
||||
use clap::Parser;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Display { port, file } => {
|
||||
markbase_core::server::run(port, file).await?;
|
||||
Commands::Framework(cmd) => {
|
||||
markbase_core::cli::framework::handle_framework_command(cmd).await?;
|
||||
}
|
||||
Commands::Render { file, output } => {
|
||||
let md = std::fs::read_to_string(&file)?;
|
||||
let html = markbase_core::render::md_to_html(&md);
|
||||
if let Some(path) = &output {
|
||||
std::fs::write(path, html)?;
|
||||
} else {
|
||||
println!("{html}");
|
||||
}
|
||||
}
|
||||
Commands::Config { action } => {
|
||||
handle_config_command(action)?;
|
||||
}
|
||||
Commands::Scan {
|
||||
user,
|
||||
dir,
|
||||
batch,
|
||||
skip_hash,
|
||||
threads,
|
||||
} => {
|
||||
use markbase_core::scan::ScanOptions;
|
||||
let options = ScanOptions { skip_hash, threads };
|
||||
markbase_core::scan::scan_directory(&user, &dir, batch, options)?;
|
||||
}
|
||||
Commands::Hash { user, threads } => {
|
||||
markbase_core::scan::compute_hashes(&user, threads)?;
|
||||
}
|
||||
Commands::WebDAV { action } => {
|
||||
handle_webdav_command(action).await?;
|
||||
}
|
||||
Commands::Iscsi { action } => {
|
||||
handle_iscsi_command(action)?;
|
||||
}
|
||||
Commands::Sftp { port, user } => {
|
||||
println!("SFTP server command is currently disabled (old implementation)");
|
||||
println!("Use 'ssh-server' command for the new SSH+SFTP implementation");
|
||||
// handle_sftp_command(port, user).await?;
|
||||
}
|
||||
Commands::BcryptTest { password, verify_hash } => {
|
||||
handle_bcrypt_test(password, verify_hash)?;
|
||||
}
|
||||
Commands::SshServer { port } => {
|
||||
println!("=== MarkBase SSH Server (Hand-written Implementation) ===");
|
||||
println!("Port: {}", port); // port已经是u16,不是Option<u16>
|
||||
println!("Implementation: SSH-2.0-MarkBaseSSH_1.0");
|
||||
println!("Features: SSH + SFTP + SCP + rsync");
|
||||
println!("Security: ⭐⭐⭐⭐⭐ (RustCrypto authoritative libraries)");
|
||||
println!();
|
||||
|
||||
markbase_core::ssh_server::server::run_ssh_server(Some(port))?;
|
||||
}
|
||||
Commands::BcryptTest {
|
||||
password,
|
||||
verify_hash,
|
||||
} => {
|
||||
handle_bcrypt_test(password, verify_hash)?;
|
||||
}
|
||||
Commands::ImportMarkdown { user, tree_type } => {
|
||||
use rusqlite::Connection;
|
||||
use markbase_core::import_markdown;
|
||||
use anyhow::Context;
|
||||
|
||||
let db_path = format!("data/users/{}.sqlite", user);
|
||||
let conn = Connection::open(&db_path)
|
||||
.with_context(|| format!("Failed to open database: {}", db_path))?;
|
||||
|
||||
println!("Importing Markdown files to {} virtual tree...", tree_type);
|
||||
|
||||
if tree_type == "categories" {
|
||||
import_markdown::import_categories_to_db(&conn, &user, &tree_type)?;
|
||||
println!("Categories imported successfully!");
|
||||
} else if tree_type == "series" {
|
||||
import_markdown::import_series_to_db(&conn, &user, &tree_type)?;
|
||||
println!("Series imported successfully!");
|
||||
} else {
|
||||
eprintln!("Invalid tree_type: {}. Use 'categories' or 'series'", tree_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = markbase_core::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 = markbase_core::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 = markbase_core::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 = markbase_core::config::MarkBaseConfig::load(config_path)?;
|
||||
|
||||
match config.validate() {
|
||||
Ok(_) => {
|
||||
println!("✓ Configuration is valid");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("✗ Configuration validation failed: {}", e);
|
||||
Commands::App(app_cmd) => {
|
||||
match app_cmd {
|
||||
markbase_core::cli::apps::AppCommands::DownloadCenter(cmd) => {
|
||||
markbase_core::cli::apps::download_center::handle_download_center_command(cmd).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_section(config: &markbase_core::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_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));
|
||||
}
|
||||
|
||||
async fn handle_webdav_command(action: WebDAVCommands) -> anyhow::Result<()> {
|
||||
match action {
|
||||
WebDAVCommands::Start { port, user } => {
|
||||
let db_path = std::path::PathBuf::from(filetree::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 find_binary(name: &str) -> std::path::PathBuf {
|
||||
let exe = std::env::current_exe().unwrap();
|
||||
let dir = exe.parent().unwrap();
|
||||
dir.join(name)
|
||||
}
|
||||
|
||||
// async fn handle_sftp_command(port: u16, user: String) -> anyhow::Result<()> {
|
||||
// println!("=== MarkBase SFTP Server ===");
|
||||
// println!("User: {}", user);
|
||||
// println!("Port: {}", port);
|
||||
// println!("Auth DB: data/auth.sqlite");
|
||||
// println!("FileTree DB: data/users/{}.sqlite", user);
|
||||
// println!("");
|
||||
//
|
||||
// let config = markbase_core::sftp::SftpConfig::load_default()?;
|
||||
//
|
||||
// if port != config.sftp.port {
|
||||
// println!(
|
||||
// "Warning: CLI port {} overrides config port {}",
|
||||
// port, config.sftp.port
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// markbase_core::sftp::run_server(config, &user).await?;
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
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!("");
|
||||
|
||||
// Generate new hash
|
||||
let new_hash = hash(&password, DEFAULT_COST)?;
|
||||
println!("Generated hash:");
|
||||
println!("{}", new_hash);
|
||||
println!("");
|
||||
|
||||
// Verify current hash if provided
|
||||
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!("");
|
||||
}
|
||||
|
||||
// Verify database hash
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user