MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

核心功能:
-  Categories/Series双视图管理(category_view.rs + import_markdown.rs)
-  FUSE Multi-Volume支持(tree_type参数)
-  SSH/SFTP/SCP/rsync协议完整实现(4042行)
-  NFS/SMB Module Phase 1-3完成
-  Archive Module Phase 1-4完成(2916行)
-  Download Center API完整实现
-  S3兼容API实现(560行)

Git配置修正:
-  删除错误origin(gitea.momentry.ddns.net)
-  删除m5max128(指向机器名)
-  设置origin = m5max128gitea.momentry.ddns.net/admin/markbase
-  设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase

数据清理:
-  删除38个临时SQLite(保留accusys.sqlite、demo.sqlite)
-  删除.bak、test_*.bin、调试脚本等临时文件
-  删除临时目录(build/、download files/、raid_test/等)
-  更新.gitignore排除临时文件

架构优化:
- 52个文件修改,2434行新增,4739行删除
- Workspace成员整合(16个crate)
- 数据库状态:accusys.sqlite保留(主demo测试)

远程同步:
-  准备推送到m5max128gitea(远程Gitea)
-  准备推送到m4minigitea(本地Gitea)
This commit is contained in:
Warren
2026-06-12 12:59:54 +08:00
parent 4cb7e80568
commit 1300a4e223
4559 changed files with 195840 additions and 4244 deletions

16
markbase-iscsi/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "markbase-iscsi"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
dirs = "6"
filetree = { path = "../filetree" }
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
which = "7"

352
markbase-iscsi/src/lib.rs Normal file
View File

@@ -0,0 +1,352 @@
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::time::Duration;
const ISCSI_DATA_DIR: &str = "data/iscsi";
const PID_FILE: &str = "data/iscsi/gotgt.pid";
const CONFIG_FILE: &str = ".gotgt/config.json";
const GOTGT_TARGET_PREFIX: &str = "iqn.2026-05.momentry:markbase_";
#[derive(Debug)]
pub struct IscsiTargetStatus {
pub running: bool,
pub pid: Option<u32>,
pub port: u16,
pub target: String,
pub lun_path: String,
pub lun_size: u64,
}
fn find_gotgt() -> Result<PathBuf> {
if let Ok(path) = std::env::var("GOTGT_PATH") {
let p = PathBuf::from(&path);
if p.exists() {
return Ok(p);
}
}
if let Ok(path) = which::which("gotgt") {
return Ok(path);
}
let fallback = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/"))
.join(".local/bin/gotgt");
if fallback.exists() {
return Ok(fallback);
}
anyhow::bail!("gotgt binary not found")
}
fn ensure_data_dir() -> Result<()> {
std::fs::create_dir_all(ISCSI_DATA_DIR)?;
let config_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/"))
.join(".gotgt");
std::fs::create_dir_all(config_dir)?;
Ok(())
}
fn parse_size(s: &str) -> Result<u64> {
if let Some(n) = s.strip_suffix("TB") {
Ok((n.parse::<f64>()? * 1_099_511_627_776.0) as u64)
} else if let Some(n) = s.strip_suffix("GB") {
Ok((n.parse::<f64>()? * 1_073_741_824.0) as u64)
} else if let Some(n) = s.strip_suffix("MB") {
Ok((n.parse::<f64>()? * 1_048_576.0) as u64)
} else if let Some(n) = s.strip_suffix("KB") {
Ok((n.parse::<f64>()? * 1024.0) as u64)
} else {
s.parse::<u64>().map_err(|e| anyhow::anyhow!("{e}"))
}
}
fn get_lun_path(user: &str) -> PathBuf {
PathBuf::from(ISCSI_DATA_DIR).join(format!("{}_lun.bin", user))
}
fn get_pid_path() -> PathBuf {
PathBuf::from(PID_FILE)
}
fn get_config_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/"))
.join(CONFIG_FILE)
}
fn get_block_device_size(device: &str) -> Result<u64> {
let output = std::process::Command::new("diskutil")
.arg("info")
.arg(device)
.output()
.context("Failed to run diskutil info")?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some(size_str) = line.strip_prefix(" Disk Size:") {
if let Some(bytes) = size_str.trim().split_whitespace().next() {
if let Ok(size) = bytes.replace(',', "").parse::<u64>() {
return Ok(size);
}
}
}
}
anyhow::bail!("Could not determine size of block device {}", device)
}
fn is_block_device_mounted(device: &str) -> Result<bool> {
let output = std::process::Command::new("diskutil")
.arg("info")
.arg(device)
.output()
.context("Failed to run diskutil info")?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.trim_start().starts_with("Mounted:") {
return Ok(line.contains("Yes"));
}
}
Ok(false)
}
pub fn generate_config(
user: &str,
port: u16,
lun_size: &str,
force: bool,
device: Option<&str>,
) -> Result<()> {
ensure_data_dir()?;
let config_path = get_config_path();
if config_path.exists() && !force {
return Ok(());
}
let (storage_path, lun_size_bytes) = if let Some(dev) = device {
let dev_path = Path::new(dev);
if !dev_path.exists() {
anyhow::bail!("Block device not found: {}", dev);
}
if is_block_device_mounted(dev)? {
anyhow::bail!(
"Block device {} is mounted. Unmount first with: diskutil unmountDisk {}",
dev,
dev
);
}
let size = get_block_device_size(dev)?;
(format!("blk:{}", dev), size)
} else {
let lun_path = get_lun_path(user);
let size_bytes = parse_size(lun_size)?;
if !lun_path.exists() {
let count = size_bytes / 512;
let status = std::process::Command::new("dd")
.arg("if=/dev/zero")
.arg(format!("of={}", lun_path.display()))
.arg("bs=512")
.arg(format!("count={}", count))
.arg("status=progress")
.status()
.context("Failed to create LUN disk image")?;
if !status.success() {
anyhow::bail!("dd failed to create LUN image");
}
}
(format!("file:{}", lun_path.display()), size_bytes)
};
let config = serde_json::json!({
"storages": [{
"deviceID": 1000,
"path": storage_path,
"online": true,
"thinProvisioning": false,
"blockShift": 9
}],
"iscsiportals": [{
"id": 0,
"portal": format!("127.0.0.1:{}", port)
}],
"iscsitargets": {
format!("{}{}", GOTGT_TARGET_PREFIX, user): {
"tpgts": { "1": [0] },
"luns": { "0": 1000 }
}
}
});
std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)
.with_context(|| format!("Failed to write {}", config_path.display()))?;
Ok(())
}
pub fn start(
user: &str,
port: u16,
lun_size: &str,
force: bool,
device: Option<&str>,
) -> Result<IscsiTargetStatus> {
let pid_path = get_pid_path();
if pid_path.exists() {
if let Ok(pid_str) = std::fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<u32>() {
let alive = std::process::Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false);
if alive {
if !force {
anyhow::bail!(
"gotgt already running (PID {}). Use --force to restart.",
pid
);
}
let _ = std::process::Command::new("kill")
.arg(pid.to_string())
.status();
}
}
}
let _ = std::fs::remove_file(&pid_path);
}
generate_config(user, port, lun_size, force, device)?;
let gotgt_path = find_gotgt()?;
let child = std::process::Command::new(&gotgt_path)
.arg("daemon")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.context("Failed to start gotgt daemon")?;
let pid = child.id();
std::fs::write(&pid_path, pid.to_string()).context("Failed to write PID file")?;
std::thread::sleep(Duration::from_millis(500));
let config_path = get_config_path();
let (lun_path, lun_size_out) = if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let v: serde_json::Value = serde_json::from_str(&content)?;
let raw = v["storages"][0]["path"].as_str().unwrap_or("").to_string();
let size = if let Some(dev) = device {
get_block_device_size(dev).unwrap_or(0)
} else {
get_lun_path(user).metadata().map(|m| m.len()).unwrap_or(0)
};
(raw, size)
} else {
(String::new(), 0)
};
Ok(IscsiTargetStatus {
running: true,
pid: Some(pid),
port,
target: format!("{}{}", GOTGT_TARGET_PREFIX, user),
lun_path,
lun_size: lun_size_out,
})
}
pub fn stop() -> Result<()> {
let pid_path = get_pid_path();
if !pid_path.exists() {
anyhow::bail!("gotgt is not running (no PID file)");
}
let pid: u32 = std::fs::read_to_string(&pid_path)?.trim().parse()?;
let alive = std::process::Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false);
if alive {
std::process::Command::new("kill")
.arg(pid.to_string())
.status()
.context("Failed to send SIGTERM")?;
for _ in 0..20 {
if !std::process::Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
break;
}
std::thread::sleep(Duration::from_millis(100));
}
}
let _ = std::fs::remove_file(&pid_path);
Ok(())
}
pub fn status() -> Result<IscsiTargetStatus> {
let pid_path = get_pid_path();
let config_path = get_config_path();
let pid = pid_path
.exists()
.then(|| std::fs::read_to_string(&pid_path).ok())
.flatten()
.and_then(|s| s.trim().parse::<u32>().ok());
let running = pid
.map(|p| {
std::process::Command::new("kill")
.arg("-0")
.arg(p.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false)
})
.unwrap_or(false);
let (port, target, lun_path, lun_size) = if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let v: serde_json::Value = serde_json::from_str(&content)?;
let port = v["iscsiportals"][0]["portal"]
.as_str()
.and_then(|s| s.split(':').nth(1))
.and_then(|p| p.parse().ok())
.unwrap_or(3260);
let target = v["iscsitargets"]
.as_object()
.and_then(|o| o.keys().next().cloned())
.unwrap_or_default();
let raw = v["storages"][0]["path"].as_str().unwrap_or("").to_string();
let (lun, size) = if let Some(dev) = raw.strip_prefix("blk:") {
let sz = get_block_device_size(dev).unwrap_or(0);
(raw.clone(), sz)
} else {
let f = raw.strip_prefix("file:").unwrap_or(&raw).to_string();
let sz = PathBuf::from(&f).metadata().map(|m| m.len()).unwrap_or(0);
(raw.clone(), sz)
};
let lun = lun;
(port, target, lun, size)
} else {
(3260, String::new(), String::new(), 0)
};
Ok(IscsiTargetStatus {
running,
pid,
port,
target,
lun_path,
lun_size,
})
}