核心功能: - ✅ 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)
405 lines
13 KiB
Rust
405 lines
13 KiB
Rust
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct S3Config {
|
|
#[serde(default)]
|
|
pub s3: S3Section,
|
|
#[serde(default)]
|
|
pub keys: KeysSection,
|
|
#[serde(default)]
|
|
pub buckets: BucketsSection,
|
|
#[serde(default)]
|
|
pub permissions: PermissionsSection,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct S3Section {
|
|
#[serde(default = "default_enabled")]
|
|
pub enabled: bool,
|
|
#[serde(default = "default_endpoint")]
|
|
pub endpoint: String,
|
|
#[serde(default = "default_region")]
|
|
pub region: String,
|
|
#[serde(default = "default_service")]
|
|
pub service: String,
|
|
#[serde(default = "default_require_auth")]
|
|
pub require_auth: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct KeysSection {
|
|
#[serde(default = "default_access_key")]
|
|
pub default_access_key: String,
|
|
#[serde(default = "default_secret_key")]
|
|
pub default_secret_key: String,
|
|
#[serde(default = "default_keys_db_path")]
|
|
pub keys_db_path: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BucketsSection {
|
|
#[serde(default)]
|
|
pub mappings: std::collections::HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionsSection {
|
|
#[serde(default = "default_permissions")]
|
|
pub default_permissions: Vec<String>,
|
|
#[serde(default = "admin_permissions")]
|
|
pub admin_permissions: Vec<String>,
|
|
}
|
|
|
|
fn default_enabled() -> bool {
|
|
true
|
|
}
|
|
fn default_endpoint() -> String {
|
|
"http://localhost:11438/s3".to_string()
|
|
}
|
|
fn default_region() -> String {
|
|
"us-east-1".to_string()
|
|
}
|
|
fn default_service() -> String {
|
|
"s3".to_string()
|
|
}
|
|
fn default_require_auth() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_access_key() -> String {
|
|
"markbase_access_key_001".to_string()
|
|
}
|
|
fn default_secret_key() -> String {
|
|
"markbase_secret_key_xyz123".to_string()
|
|
}
|
|
fn default_keys_db_path() -> String {
|
|
"data/s3_keys.json".to_string()
|
|
}
|
|
|
|
fn default_permissions() -> Vec<String> {
|
|
vec![
|
|
"GetObject".to_string(),
|
|
"ListBucket".to_string(),
|
|
"HeadObject".to_string(),
|
|
]
|
|
}
|
|
fn admin_permissions() -> Vec<String> {
|
|
vec![
|
|
"GetObject".to_string(),
|
|
"PutObject".to_string(),
|
|
"DeleteObject".to_string(),
|
|
"ListBucket".to_string(),
|
|
"HeadObject".to_string(),
|
|
]
|
|
}
|
|
|
|
impl Default for S3Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
s3: S3Section::default(),
|
|
keys: KeysSection::default(),
|
|
buckets: BucketsSection::default(),
|
|
permissions: PermissionsSection::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for S3Section {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: default_enabled(),
|
|
endpoint: default_endpoint(),
|
|
region: default_region(),
|
|
service: default_service(),
|
|
require_auth: default_require_auth(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for KeysSection {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_access_key: default_access_key(),
|
|
default_secret_key: default_secret_key(),
|
|
keys_db_path: default_keys_db_path(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for BucketsSection {
|
|
fn default() -> Self {
|
|
Self {
|
|
mappings: std::collections::HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for PermissionsSection {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_permissions: default_permissions(),
|
|
admin_permissions: admin_permissions(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl S3Config {
|
|
pub fn load(path: &str) -> Result<Self> {
|
|
let config_path = PathBuf::from(path);
|
|
|
|
if !config_path.exists() {
|
|
log::warn!("S3 config file not found: {}, using defaults", path);
|
|
return Ok(Self::default());
|
|
}
|
|
|
|
let content = fs::read_to_string(&config_path)
|
|
.with_context(|| format!("Failed to read S3 config: {}", path))?;
|
|
|
|
let config: S3Config = toml::from_str(&content)
|
|
.with_context(|| format!("Failed to parse S3 config: {}", path))?;
|
|
|
|
log::info!("S3 config loaded from: {}", path);
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn load_default() -> Result<Self> {
|
|
Self::load("config/s3.toml")
|
|
}
|
|
|
|
pub fn save(&self, path: &str) -> Result<()> {
|
|
let config_path = PathBuf::from(path);
|
|
|
|
// Create backup before saving
|
|
if config_path.exists() {
|
|
let backup_path = config_path.with_extension("toml.bak");
|
|
std::fs::copy(&config_path, &backup_path)
|
|
.with_context(|| format!("Failed to create backup: {}", backup_path.display()))?;
|
|
log::info!("S3 config backup created: {}", backup_path.display());
|
|
}
|
|
|
|
let content = toml::to_string_pretty(self)
|
|
.with_context(|| "Failed to serialize S3 config")?;
|
|
|
|
std::fs::write(&config_path, content)
|
|
.with_context(|| format!("Failed to write S3 config: {}", path))?;
|
|
|
|
log::info!("S3 config saved to: {}", path);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn merge_env(&mut self) {
|
|
if let Ok(require_auth) = std::env::var("MB_S3_REQUIRE_AUTH") {
|
|
self.s3.require_auth = require_auth == "true" || require_auth == "1";
|
|
}
|
|
|
|
if let Ok(endpoint) = std::env::var("MB_S3_ENDPOINT") {
|
|
self.s3.endpoint = endpoint;
|
|
}
|
|
|
|
if let Ok(region) = std::env::var("MB_S3_REGION") {
|
|
self.s3.region = region;
|
|
}
|
|
|
|
if let Ok(access_key) = std::env::var("MB_S3_ACCESS_KEY") {
|
|
self.keys.default_access_key = access_key;
|
|
}
|
|
|
|
if let Ok(secret_key) = std::env::var("MB_S3_SECRET_KEY") {
|
|
self.keys.default_secret_key = secret_key;
|
|
}
|
|
}
|
|
|
|
pub fn validate(&self) -> Result<()> {
|
|
if self.s3.endpoint.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 endpoint cannot be empty"));
|
|
}
|
|
|
|
// Validate endpoint format (should start with http:// or https://)
|
|
if !self.s3.endpoint.starts_with("http://") && !self.s3.endpoint.starts_with("https://") {
|
|
return Err(anyhow::anyhow!(
|
|
"S3 endpoint must start with http:// or https://. Current: {}",
|
|
self.s3.endpoint
|
|
));
|
|
}
|
|
|
|
if self.s3.region.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 region cannot be empty"));
|
|
}
|
|
|
|
if self.s3.service.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 service cannot be empty"));
|
|
}
|
|
|
|
if self.keys.default_access_key.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 access key cannot be empty"));
|
|
}
|
|
|
|
if self.keys.default_secret_key.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 secret key cannot be empty"));
|
|
}
|
|
|
|
if self.keys.keys_db_path.is_empty() {
|
|
return Err(anyhow::anyhow!("S3 keys_db_path cannot be empty"));
|
|
}
|
|
|
|
if self.permissions.default_permissions.is_empty() {
|
|
return Err(anyhow::anyhow!("default_permissions cannot be empty"));
|
|
}
|
|
|
|
if self.permissions.admin_permissions.is_empty() {
|
|
return Err(anyhow::anyhow!("admin_permissions cannot be empty"));
|
|
}
|
|
|
|
// Validate permission format
|
|
let valid_permissions = [
|
|
"GetObject", "PutObject", "DeleteObject", "ListBucket",
|
|
"HeadObject", "ListAllMyBuckets", "CreateBucket", "DeleteBucket"
|
|
];
|
|
|
|
for perm in &self.permissions.default_permissions {
|
|
if !valid_permissions.contains(&perm.as_str()) {
|
|
return Err(anyhow::anyhow!(
|
|
"Invalid permission: {}. Must be one of: {}",
|
|
perm,
|
|
valid_permissions.join(", ")
|
|
));
|
|
}
|
|
}
|
|
|
|
for perm in &self.permissions.admin_permissions {
|
|
if !valid_permissions.contains(&perm.as_str()) {
|
|
return Err(anyhow::anyhow!(
|
|
"Invalid admin permission: {}. Must be one of: {}",
|
|
perm,
|
|
valid_permissions.join(", ")
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get(&self, key: &str) -> Option<String> {
|
|
match key {
|
|
"s3.enabled" => Some(self.s3.enabled.to_string()),
|
|
"s3.endpoint" => Some(self.s3.endpoint.clone()),
|
|
"s3.region" => Some(self.s3.region.clone()),
|
|
"s3.service" => Some(self.s3.service.clone()),
|
|
"s3.require_auth" => Some(self.s3.require_auth.to_string()),
|
|
|
|
"keys.default_access_key" => Some(self.keys.default_access_key.clone()),
|
|
"keys.default_secret_key" => Some(self.keys.default_secret_key.clone()),
|
|
"keys.keys_db_path" => Some(self.keys.keys_db_path.clone()),
|
|
|
|
"permissions.default_permissions" => {
|
|
Some(serde_json::to_string(&self.permissions.default_permissions).unwrap_or_default())
|
|
}
|
|
"permissions.admin_permissions" => {
|
|
Some(serde_json::to_string(&self.permissions.admin_permissions).unwrap_or_default())
|
|
}
|
|
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
|
|
match key {
|
|
"s3.enabled" => self.s3.enabled = value.parse()?,
|
|
"s3.endpoint" => self.s3.endpoint = value.to_string(),
|
|
"s3.region" => self.s3.region = value.to_string(),
|
|
"s3.service" => self.s3.service = value.to_string(),
|
|
"s3.require_auth" => self.s3.require_auth = value.parse()?,
|
|
|
|
"keys.default_access_key" => self.keys.default_access_key = value.to_string(),
|
|
"keys.default_secret_key" => self.keys.default_secret_key = value.to_string(),
|
|
"keys.keys_db_path" => self.keys.keys_db_path = value.to_string(),
|
|
|
|
"permissions.default_permissions" => {
|
|
self.permissions.default_permissions = serde_json::from_str(value)
|
|
.with_context(|| "Failed to parse permissions array")?;
|
|
}
|
|
"permissions.admin_permissions" => {
|
|
self.permissions.admin_permissions = serde_json::from_str(value)
|
|
.with_context(|| "Failed to parse admin permissions array")?;
|
|
}
|
|
|
|
_ => return Err(anyhow::anyhow!("Invalid S3 config key: {}", key)),
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = S3Config::default();
|
|
|
|
assert_eq!(config.s3.enabled, true);
|
|
assert_eq!(config.s3.require_auth, false);
|
|
assert_eq!(config.s3.endpoint, "http://localhost:11438/s3");
|
|
assert_eq!(config.s3.region, "us-east-1");
|
|
|
|
assert_eq!(config.keys.default_access_key, "markbase_access_key_001");
|
|
assert_eq!(config.keys.default_secret_key, "markbase_secret_key_xyz123");
|
|
|
|
assert_eq!(config.permissions.default_permissions.len(), 3);
|
|
assert_eq!(config.permissions.admin_permissions.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_load_missing_config() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config_path = temp_dir.path().join("missing.toml");
|
|
|
|
let config = S3Config::load(&config_path.to_string_lossy()).unwrap();
|
|
|
|
assert_eq!(config.s3.enabled, true);
|
|
assert_eq!(config.s3.require_auth, false);
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_env() {
|
|
std::env::set_var("MB_S3_REQUIRE_AUTH", "true");
|
|
std::env::set_var("MB_S3_ENDPOINT", "http://custom.endpoint");
|
|
|
|
let mut config = S3Config::default();
|
|
config.merge_env();
|
|
|
|
assert_eq!(config.s3.require_auth, true);
|
|
assert_eq!(config.s3.endpoint, "http://custom.endpoint");
|
|
|
|
std::env::remove_var("MB_S3_REQUIRE_AUTH");
|
|
std::env::remove_var("MB_S3_ENDPOINT");
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate() {
|
|
let config = S3Config::default();
|
|
assert!(config.validate().is_ok());
|
|
|
|
let mut invalid_config = S3Config::default();
|
|
invalid_config.s3.endpoint = "".to_string();
|
|
assert!(invalid_config.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_set() {
|
|
let mut config = S3Config::default();
|
|
|
|
assert_eq!(config.get("s3.enabled"), Some("true".to_string()));
|
|
assert_eq!(config.get("s3.endpoint"), Some("http://localhost:11438/s3".to_string()));
|
|
|
|
config.set("s3.require_auth", "true").unwrap();
|
|
assert_eq!(config.s3.require_auth, true);
|
|
|
|
config.set("s3.endpoint", "http://new.endpoint").unwrap();
|
|
assert_eq!(config.s3.endpoint, "http://new.endpoint");
|
|
}
|
|
} |