use std::sync::Arc;
use alun_core::plugin::Plugin;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::info;
use super::backend::StorageBackend;
use super::registry::BackendRegistry;
use super::types::FsPluginConfig;
pub type StoreResult<T> = Result<T, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMeta {
pub file_id: String,
pub original_name: String,
pub stored_path: String,
pub size: u64,
pub content_type: String,
pub created_at: DateTime<Utc>,
}
pub struct FsPlugin {
registry: BackendRegistry,
config: FsPluginConfig,
}
impl FsPlugin {
pub fn new_local(root_dir: &str) -> Self {
debug_assert!(!root_dir.is_empty(), "root_dir 不得为空");
let local_fs = super::local::LocalFs::new(root_dir);
let mut registry = BackendRegistry::new();
registry.register(
local_fs,
super::types::BackendConfig {
backend_type: "local".into(),
root_path: root_dir.to_string(),
..Default::default()
},
);
registry.with_default("local");
Self {
registry,
config: FsPluginConfig::default(),
}
}
pub fn new(config: FsPluginConfig, registry: BackendRegistry) -> Self {
Self { config, registry }
}
pub fn registry(&self) -> &BackendRegistry {
&self.registry
}
pub fn config(&self) -> &FsPluginConfig {
&self.config
}
pub async fn write_to(
&self,
backend_type: Option<&str>,
original_name: &str,
data: &[u8],
) -> StoreResult<FileMeta> {
let backend = self.resolve_backend(backend_type)?;
backend.write(original_name, data).await
}
pub async fn write(&self, filename: &str, data: &[u8]) -> StoreResult<FileMeta> {
self.write_to(None, filename, data).await
}
pub async fn read(&self, stored_path: &str) -> StoreResult<Vec<u8>> {
let backend = self.resolve_backend(None)?;
backend.read(stored_path).await
}
pub async fn delete(&self, stored_path: &str) -> StoreResult<()> {
let backend = self.resolve_backend(None)?;
backend.delete(stored_path).await
}
pub async fn exists(&self, stored_path: &str) -> bool {
match self.resolve_backend(None) {
Ok(b) => b.exists(stored_path).await,
Err(_) => false,
}
}
pub async fn health_check(&self) -> Vec<(String, StoreResult<()>)> {
self.registry.health_check_all().await
}
fn resolve_backend(&self, backend_type: Option<&str>) -> StoreResult<&Arc<dyn StorageBackend>> {
if let Some(bt) = backend_type {
if let Some(b) = self.registry.get(bt) {
return Ok(b);
}
}
self.registry
.default_backend()
.ok_or_else(|| "未配置默认存储后端".to_string())
}
}
#[async_trait]
impl Plugin for FsPlugin {
fn name(&self) -> &str {
"fs"
}
async fn start(&self) -> alun_core::Result<()> {
info!(
"FsPlugin 启动,已注册后端: {:?}, 默认: {:?}",
self.registry.backend_types(),
self.registry.default_backend().map(|b| b.backend_type())
);
let results = self.registry.health_check_all().await;
for (bt, result) in &results {
match result {
Ok(_) => info!("后端 {} 健康检查通过", bt),
Err(e) => tracing::warn!("后端 {} 健康检查失败: {}", e, bt),
}
}
Ok(())
}
async fn stop(&self) -> alun_core::Result<()> {
info!("FsPlugin 停止");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[tokio::test]
async fn test_new_local_create_and_read() {
let dir = std::env::temp_dir().join("alun_fs_plugin_test");
let _ = fs::create_dir_all(&dir);
let plugin = FsPlugin::new_local(dir.to_str().unwrap());
let meta = plugin.write("hello.txt", b"hello plugin").await.unwrap();
assert_eq!(meta.original_name, "hello.txt");
let data = plugin.read(&meta.stored_path).await.unwrap();
assert_eq!(data, b"hello plugin");
plugin.delete(&meta.stored_path).await.unwrap();
let _ = fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_new_local_delete_nonexistent_shouldbe_ok() {
let dir = std::env::temp_dir().join("alun_fs_plugin_test2");
let _ = fs::create_dir_all(&dir);
let plugin = FsPlugin::new_local(dir.to_str().unwrap());
assert!(plugin.delete("nonexistent/path.dat").await.is_ok());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_plugin_name() {
let dir = std::env::temp_dir().join("alun_fs_plugin_test3");
let _ = fs::create_dir_all(&dir);
let plugin = FsPlugin::new_local(dir.to_str().unwrap());
assert_eq!(plugin.name(), "fs");
let _ = fs::remove_dir_all(&dir);
}
#[test]
#[should_panic(expected = "root_dir")]
fn test_new_local_empty_root_panics() {
FsPlugin::new_local("");
}
}