1use 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
20pub type StoreResult<T> = Result<T, String>;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FileMeta {
26 pub file_id: String,
28 pub original_name: String,
30 pub stored_path: String,
32 pub size: u64,
34 pub content_type: String,
36 pub created_at: DateTime<Utc>,
38}
39
40pub struct FsPlugin {
46 registry: BackendRegistry,
48 config: FsPluginConfig,
50}
51
52impl FsPlugin {
53 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 pub fn new(config: FsPluginConfig, registry: BackendRegistry) -> Self {
84 Self { config, registry }
85 }
86
87 pub fn registry(&self) -> &BackendRegistry {
89 &self.registry
90 }
91
92 pub fn config(&self) -> &FsPluginConfig {
94 &self.config
95 }
96
97 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 pub async fn write(&self, filename: &str, data: &[u8]) -> StoreResult<FileMeta> {
123 self.write_to(None, filename, data).await
124 }
125
126 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 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 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 pub async fn health_check(&self) -> Vec<(String, StoreResult<()>)> {
160 self.registry.health_check_all().await
161 }
162
163 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}