Skip to main content

alun_fs/
plugin.rs

1//! 文件存储插件
2//!
3//! 通过 `BackendRegistry` 管理多个 `StorageBackend`,
4//! 根据请求上下文路由到对应后端。
5//!
6//! 参考 alun_task::TaskPlugin 的设计模式。
7
8use std::sync::Arc;
9
10use alun_core::plugin::Plugin;
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use tracing::info;
15
16use super::backend::StorageBackend;
17use super::registry::BackendRegistry;
18use super::types::FsPluginConfig;
19
20/// 文件存储操作结果类型
21pub type StoreResult<T> = Result<T, String>;
22
23/// 文件元数据
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FileMeta {
26    /// UUID v4 文件唯一标识
27    pub file_id: String,
28    /// 原始文件名(含扩展名)
29    pub original_name: String,
30    /// 存储相对路径
31    pub stored_path: String,
32    /// 文件大小(字节)
33    pub size: u64,
34    /// MIME 类型(如 `image/png`)
35    pub content_type: String,
36    /// 创建时间
37    pub created_at: DateTime<Utc>,
38}
39
40/// 多后端文件存储插件
41///
42/// 通过 `BackendRegistry` 管理多个 `StorageBackend` 实例。
43/// 根据 `backend_type`(从请求上下文或 bucket 配置推断)路由到对应后端。
44/// 同时实现 `alun_core::Plugin` trait,支持通过 `PluginManager` 管理生命周期。
45pub struct FsPlugin {
46    /// 后端注册中心
47    registry: BackendRegistry,
48    /// 全局配置
49    config: FsPluginConfig,
50}
51
52impl FsPlugin {
53    /// 创建本地文件存储插件(向后兼容的快捷构造方法)
54    ///
55    /// # 参数
56    /// - `root_dir` - 本地文件存储根目录
57    pub fn new_local(root_dir: &str) -> Self {
58        debug_assert!(!root_dir.is_empty(), "root_dir 不得为空");
59
60        let local_fs = super::local::LocalFs::new(root_dir);
61        let mut registry = BackendRegistry::new();
62        registry.register(
63            local_fs,
64            super::types::BackendConfig {
65                backend_type: "local".into(),
66                root_path: root_dir.to_string(),
67                ..Default::default()
68            },
69        );
70        registry.with_default("local");
71
72        Self {
73            registry,
74            config: FsPluginConfig::default(),
75        }
76    }
77
78    /// 创建文件存储插件
79    ///
80    /// # 参数
81    /// - `config` - 全局运行时配置(建议从 config.toml 的 `[fs]` section 读取)
82    /// - `registry` - 已注册后端的注册中心
83    pub fn new(config: FsPluginConfig, registry: BackendRegistry) -> Self {
84        Self { config, registry }
85    }
86
87    /// 获取注册中心引用
88    pub fn registry(&self) -> &BackendRegistry {
89        &self.registry
90    }
91
92    /// 获取全局配置引用
93    pub fn config(&self) -> &FsPluginConfig {
94        &self.config
95    }
96
97    /// 按指定后端类型写入文件
98    ///
99    /// # 参数
100    /// - `backend_type` - 目标后端类型。None 时使用默认后端
101    /// - `original_name` - 原始文件名(含扩展名)
102    /// - `data` - 文件字节内容
103    ///
104    /// # 返回
105    /// - `Ok(FileMeta)` - 写入成功,返回元数据
106    /// - `Err(String)` - 写入失败
107    pub async fn write_to(
108        &self,
109        backend_type: Option<&str>,
110        original_name: &str,
111        data: &[u8],
112    ) -> StoreResult<FileMeta> {
113        let backend = self.resolve_backend(backend_type)?;
114        backend.write(original_name, data).await
115    }
116
117    /// 写入文件(使用默认后端,向后兼容)
118    ///
119    /// # 参数
120    /// - `filename` - 原始文件名(含扩展名)
121    /// - `data` - 文件字节内容
122    pub async fn write(&self, filename: &str, data: &[u8]) -> StoreResult<FileMeta> {
123        self.write_to(None, filename, data).await
124    }
125
126    /// 读取文件内容
127    ///
128    /// # 参数
129    /// - `stored_path` - 存储相对路径
130    pub async fn read(&self, stored_path: &str) -> StoreResult<Vec<u8>> {
131        let backend = self.resolve_backend(None)?;
132        backend.read(stored_path).await
133    }
134
135    /// 删除文件(不存在不报错)
136    ///
137    /// # 参数
138    /// - `stored_path` - 存储相对路径
139    pub async fn delete(&self, stored_path: &str) -> StoreResult<()> {
140        let backend = self.resolve_backend(None)?;
141        backend.delete(stored_path).await
142    }
143
144    /// 检查文件是否存在
145    ///
146    /// # 参数
147    /// - `stored_path` - 存储相对路径
148    pub async fn exists(&self, stored_path: &str) -> bool {
149        match self.resolve_backend(None) {
150            Ok(b) => b.exists(stored_path).await,
151            Err(_) => false,
152        }
153    }
154
155    /// 对所有已注册后端执行健康检查
156    ///
157    /// # 返回
158    /// `Vec<(backend_type, Result)>` 列表
159    pub async fn health_check(&self) -> Vec<(String, StoreResult<()>)> {
160        self.registry.health_check_all().await
161    }
162
163    /// 解析目标后端:按 backend_type 查找,回退到默认
164    fn resolve_backend(&self, backend_type: Option<&str>) -> StoreResult<&Arc<dyn StorageBackend>> {
165        if let Some(bt) = backend_type {
166            if let Some(b) = self.registry.get(bt) {
167                return Ok(b);
168            }
169        }
170        self.registry
171            .default_backend()
172            .ok_or_else(|| "未配置默认存储后端".to_string())
173    }
174}
175
176#[async_trait]
177impl Plugin for FsPlugin {
178    fn name(&self) -> &str {
179        "fs"
180    }
181
182    async fn start(&self) -> alun_core::Result<()> {
183        info!(
184            "FsPlugin 启动,已注册后端: {:?}, 默认: {:?}",
185            self.registry.backend_types(),
186            self.registry.default_backend().map(|b| b.backend_type())
187        );
188
189        let results = self.registry.health_check_all().await;
190        for (bt, result) in &results {
191            match result {
192                Ok(_) => info!("后端 {} 健康检查通过", bt),
193                Err(e) => tracing::warn!("后端 {} 健康检查失败: {}", e, bt),
194            }
195        }
196        Ok(())
197    }
198
199    async fn stop(&self) -> alun_core::Result<()> {
200        info!("FsPlugin 停止");
201        Ok(())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use std::fs;
209
210    #[tokio::test]
211    async fn test_new_local_create_and_read() {
212        let dir = std::env::temp_dir().join("alun_fs_plugin_test");
213        let _ = fs::create_dir_all(&dir);
214
215        let plugin = FsPlugin::new_local(dir.to_str().unwrap());
216        let meta = plugin.write("hello.txt", b"hello plugin").await.unwrap();
217        assert_eq!(meta.original_name, "hello.txt");
218
219        let data = plugin.read(&meta.stored_path).await.unwrap();
220        assert_eq!(data, b"hello plugin");
221
222        plugin.delete(&meta.stored_path).await.unwrap();
223
224        let _ = fs::remove_dir_all(&dir);
225    }
226
227    #[tokio::test]
228    async fn test_new_local_delete_nonexistent_shouldbe_ok() {
229        let dir = std::env::temp_dir().join("alun_fs_plugin_test2");
230        let _ = fs::create_dir_all(&dir);
231
232        let plugin = FsPlugin::new_local(dir.to_str().unwrap());
233        assert!(plugin.delete("nonexistent/path.dat").await.is_ok());
234
235        let _ = fs::remove_dir_all(&dir);
236    }
237
238    #[test]
239    fn test_plugin_name() {
240        let dir = std::env::temp_dir().join("alun_fs_plugin_test3");
241        let _ = fs::create_dir_all(&dir);
242        let plugin = FsPlugin::new_local(dir.to_str().unwrap());
243        assert_eq!(plugin.name(), "fs");
244        let _ = fs::remove_dir_all(&dir);
245    }
246
247    #[test]
248    #[should_panic(expected = "root_dir")]
249    fn test_new_local_empty_root_panics() {
250        FsPlugin::new_local("");
251    }
252}