MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
核心功能: - ✅ 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:
16
markbase-iscsi/Cargo.toml
Normal file
16
markbase-iscsi/Cargo.toml
Normal 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
352
markbase-iscsi/src/lib.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user