1use std::path::{Path, PathBuf};
2use tokio::fs;
3use tokio::io::{AsyncReadExt, AsyncWriteExt};
4use tracing::{info, error};
5use uuid::Uuid;
6use chrono::Utc;
7
8use super::plugin::{FileMeta, StoreResult};
9
10pub struct LocalFs {
14 root_dir: PathBuf,
16}
17
18impl LocalFs {
19 pub fn new(root_dir: &str) -> Self {
23 let path = PathBuf::from(root_dir);
24 let path = if path.is_absolute() {
25 path
26 } else {
27 std::env::current_dir()
28 .unwrap_or_else(|_| PathBuf::from("."))
29 .join(path)
30 };
31
32 info!("LocalFs 初始化, root={}", path.display());
33 Self { root_dir: path }
34 }
35
36 pub fn root_dir(&self) -> &Path { &self.root_dir }
38
39 pub async fn write(&self, relative_path: &str, data: &[u8]) -> StoreResult<FileMeta> {
41 let full_path = self.resolve(relative_path);
42 self.ensure_parent(&full_path).await?;
43
44 let mut file = fs::File::create(&full_path).await.map_err(|e| {
45 error!("文件创建失败 {}: {}", full_path.display(), e);
46 format!("文件创建失败: {}", e)
47 })?;
48
49 file.write_all(data).await.map_err(|e| {
50 error!("文件写入失败 {}: {}", full_path.display(), e);
51 format!("文件写入失败: {}", e)
52 })?;
53
54 let metadata = fs::metadata(&full_path).await.map_err(|e| {
55 format!("获取元数据失败: {}", e)
56 })?;
57
58 let meta = FileMeta {
59 file_id: Uuid::new_v4().to_string(),
60 original_name: relative_path.to_string(),
61 stored_path: relative_path.to_string(),
62 size: metadata.len(),
63 content_type: mime_guess(relative_path),
64 created_at: Utc::now(),
65 };
66
67 info!("文件存储成功: {} ({} bytes)", relative_path, meta.size);
68 Ok(meta)
69 }
70
71 pub async fn write_with_name(&self, original_name: &str, data: &[u8]) -> StoreResult<FileMeta> {
75 let ext = Path::new(original_name)
76 .extension()
77 .and_then(|e| e.to_str())
78 .unwrap_or("bin");
79
80 let file_id = Uuid::new_v4().to_string();
81 let stored_name = format!("{}.{}", file_id, ext);
82 let date_path = Utc::now().format("%Y/%m/%d").to_string();
83 let relative_path = format!("{}/{}", date_path, stored_name);
84
85 let full_path = self.resolve(&relative_path);
86 self.ensure_parent(&full_path).await?;
87
88 let mut file = fs::File::create(&full_path).await.map_err(|e| {
89 format!("文件创建失败: {}", e)
90 })?;
91
92 file.write_all(data).await.map_err(|e| {
93 format!("文件写入失败: {}", e)
94 })?;
95
96 let meta = FileMeta {
97 file_id,
98 original_name: original_name.to_string(),
99 stored_path: relative_path,
100 size: data.len() as u64,
101 content_type: mime_guess(original_name),
102 created_at: Utc::now(),
103 };
104
105 info!("文件存储成功: {} -> {} ({} bytes)", original_name, meta.stored_path, meta.size);
106 Ok(meta)
107 }
108
109 pub async fn read(&self, relative_path: &str) -> StoreResult<Vec<u8>> {
113 let full_path = self.resolve(relative_path);
114
115 if !full_path.exists() {
116 return Err(format!("文件不存在: {}", relative_path));
117 }
118
119 let mut file = fs::File::open(&full_path).await.map_err(|e| {
120 format!("文件打开失败: {}", e)
121 })?;
122
123 let mut data = Vec::new();
124 file.read_to_end(&mut data).await.map_err(|e| {
125 format!("文件读取失败: {}", e)
126 })?;
127
128 Ok(data)
129 }
130
131 pub async fn delete(&self, relative_path: &str) -> StoreResult<()> {
133 let full_path = self.resolve(relative_path);
134
135 if !full_path.exists() {
136 return Ok(());
137 }
138
139 fs::remove_file(&full_path).await.map_err(|e| {
140 format!("文件删除失败: {}", e)
141 })?;
142
143 info!("文件已删除: {}", relative_path);
144 Ok(())
145 }
146
147 pub async fn exists(&self, relative_path: &str) -> bool {
149 self.resolve(relative_path).exists()
150 }
151
152 fn resolve(&self, relative_path: &str) -> PathBuf {
153 let path = Path::new(relative_path);
154 let path = if path.is_absolute() {
155 path.strip_prefix("/").unwrap_or(path).to_path_buf()
156 } else {
157 path.to_path_buf()
158 };
159 self.root_dir.join(path)
160 }
161
162 async fn ensure_parent(&self, full_path: &Path) -> StoreResult<()> {
163 if let Some(parent) = full_path.parent() {
164 if !parent.exists() {
165 fs::create_dir_all(parent).await.map_err(|e| {
166 format!("目录创建失败: {}", e)
167 })?;
168 }
169 }
170 Ok(())
171 }
172}
173
174fn mime_guess(filename: &str) -> String {
175 let ext = Path::new(filename)
176 .extension()
177 .and_then(|e| e.to_str())
178 .unwrap_or("bin");
179
180 match ext.to_lowercase().as_str() {
181 "jpg" | "jpeg" => "image/jpeg",
182 "png" => "image/png",
183 "gif" => "image/gif",
184 "webp" => "image/webp",
185 "svg" => "image/svg+xml",
186 "pdf" => "application/pdf",
187 "json" => "application/json",
188 "xml" => "application/xml",
189 "html" | "htm" => "text/html",
190 "css" => "text/css",
191 "js" => "application/javascript",
192 "txt" => "text/plain",
193 "csv" => "text/csv",
194 "zip" => "application/zip",
195 "mp4" => "video/mp4",
196 "mp3" => "audio/mpeg",
197 "doc" => "application/msword",
198 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
199 "xls" => "application/vnd.ms-excel",
200 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
201 _ => "application/octet-stream",
202 }.into()
203}