Skip to main content

cool_plugin/upload/
local.rs

1//! 本地文件上传实现
2//!
3//! 对应 TypeScript 版本的 `plugin/hooks/upload/index.ts`
4
5use crate::plugin::PluginInfo;
6use crate::upload::{
7    Mode, ModeType, PathValidator, UploadContext, UploadError, UploadHook, UploadResult,
8};
9use async_trait::async_trait;
10use chrono::Local;
11use std::fs;
12use std::path::{Path, PathBuf};
13use uuid::Uuid;
14
15/// 本地文件上传钩子
16///
17/// 对应 TypeScript 版本的 `CoolPlugin`
18pub struct LocalUploadHook {
19    /// 插件信息
20    plugin_info: PluginInfo,
21    /// 上传基础路径
22    base_path: PathBuf,
23    /// 域名前缀
24    domain: String,
25}
26
27impl LocalUploadHook {
28    /// 创建本地上传钩子
29    pub fn new(plugin_info: PluginInfo, base_path: impl Into<PathBuf>, domain: String) -> Self {
30        Self {
31            plugin_info,
32            base_path: base_path.into(),
33            domain,
34        }
35    }
36
37    /// 获取日期目录(格式:YYYYMMDD)
38    fn get_date_dir(&self) -> String {
39        Local::now().format("%Y%m%d").to_string()
40    }
41
42    /// 确保目录存在
43    fn ensure_dir(&self, dir_path: &Path) -> UploadResult<()> {
44        if !dir_path.exists() {
45            fs::create_dir_all(dir_path)?;
46        }
47        Ok(())
48    }
49}
50
51#[async_trait]
52impl UploadHook for LocalUploadHook {
53    fn plugin_info(&self) -> &PluginInfo {
54        &self.plugin_info
55    }
56
57    async fn get_mode(&self) -> UploadResult<Mode> {
58        Ok(Mode {
59            mode: ModeType::Local,
60            r#type: "local".to_string(),
61        })
62    }
63
64    async fn down_and_upload(&self, url: &str, file_name: Option<&str>) -> UploadResult<String> {
65        // 下载文件
66        let file_data = if url.starts_with("http://") || url.starts_with("https://") {
67            // HTTP/HTTPS URL,需要下载
68            // 注意:这里需要 reqwest,但为了避免依赖,暂时返回错误
69            // 实际使用时可以通过依赖注入获取 HTTP 客户端
70            return Err(UploadError::Http(
71                "HTTP 下载功能需要 reqwest 依赖".to_string(),
72            ));
73        } else {
74            // 本地文件路径
75            fs::read(url)?
76        };
77
78        let date_dir = self.get_date_dir();
79        let base_path = &self.base_path;
80
81        // 从 URL 或文件名获取扩展名
82        let extension = if let Some(name) = file_name {
83            Path::new(name)
84                .extension()
85                .and_then(|ext| ext.to_str())
86                .unwrap_or("")
87        } else {
88            Path::new(url)
89                .extension()
90                .and_then(|ext| ext.to_str())
91                .unwrap_or("")
92        };
93
94        // 验证文件名安全性
95        let safe_file_name = if let Some(name) = file_name {
96            let sanitized = PathValidator::sanitize_path(name)?;
97            // 只取文件名部分,去除可能的子目录
98            Path::new(&sanitized)
99                .file_name()
100                .and_then(|n| n.to_str())
101                .unwrap_or(&sanitized)
102                .to_string()
103        } else {
104            format!("{}.{}", Uuid::new_v4(), extension)
105        };
106
107        // 创建目录
108        let dir_path = base_path.join(&date_dir);
109        self.ensure_dir(&dir_path)?;
110
111        // 写入文件
112        let target_path = dir_path.join(&safe_file_name);
113        PathValidator::validate_target_path(&target_path, base_path)?;
114        fs::write(&target_path, file_data)?;
115
116        Ok(format!(
117            "{}/upload/{}/{}",
118            self.domain, date_dir, safe_file_name
119        ))
120    }
121
122    async fn upload_with_key(&self, file_path: &Path, key: &str) -> UploadResult<String> {
123        let date_dir = self.get_date_dir();
124        let base_path = &self.base_path;
125
126        // 验证 key 安全性
127        let safe_key = PathValidator::sanitize_path(key)?;
128
129        // 读取源文件
130        let data = fs::read(file_path)?;
131
132        // 构建目标路径
133        let target_path = base_path.join(&date_dir).join(&safe_key);
134        let dir_path = target_path.parent().unwrap_or(base_path);
135
136        // 验证最终路径
137        PathValidator::validate_target_path(&target_path, base_path)?;
138
139        // 确保目录存在
140        self.ensure_dir(dir_path)?;
141
142        // 写入文件
143        fs::write(&target_path, data)?;
144
145        Ok(format!("{}/upload/{}/{}", self.domain, date_dir, safe_key))
146    }
147
148    async fn upload(&self, ctx: &UploadContext) -> UploadResult<String> {
149        if ctx.file_data.is_empty() {
150            return Err(UploadError::EmptyFile);
151        }
152
153        let date_dir = self.get_date_dir();
154        let base_path = &self.base_path;
155
156        // 验证 key 安全性(如果提供)
157        let safe_key = if let Some(key) = &ctx.key {
158            Some(PathValidator::sanitize_path(key)?)
159        } else {
160            None
161        };
162
163        // 获取文件扩展名
164        let extension = Path::new(&ctx.filename)
165            .extension()
166            .and_then(|ext| ext.to_str())
167            .unwrap_or("");
168
169        // 生成最终文件名
170        let final_name = if let Some(key) = safe_key {
171            key
172        } else {
173            format!("{}.{}", Uuid::new_v4(), extension)
174        };
175
176        let name = format!("{}/{}", date_dir, final_name);
177        let target = base_path.join(&name);
178
179        // 验证最终路径
180        PathValidator::validate_target_path(&target, base_path)?;
181
182        // 创建目录
183        let dir_path = base_path.join(&date_dir);
184        self.ensure_dir(&dir_path)?;
185
186        // 写入文件
187        fs::write(&target, &ctx.file_data)?;
188
189        Ok(format!("{}/upload/{}", self.domain, name))
190    }
191}