Skip to main content

cool_plugin/
upload.rs

1//! 插件上传钩子
2//!
3//! 对应 TypeScript 版本的 `plugin/hooks/upload/`
4//!
5//! 提供文件上传功能,包括:
6//! - 文件上传
7//! - 下载并上传
8//! - 指定 key 上传
9
10mod local;
11
12pub use local::LocalUploadHook;
13
14use crate::plugin::PluginInfo;
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use std::path::Path;
18use thiserror::Error;
19
20/// 上传模式类型
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22pub enum ModeType {
23    /// 本地存储
24    #[default]
25    Local,
26    /// OSS 存储
27    Oss,
28    /// COS 存储
29    Cos,
30    /// 其他
31    Other,
32}
33
34/// 上传模式
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Mode {
37    /// 模式
38    pub mode: ModeType,
39    /// 类型
40    pub r#type: String,
41}
42
43/// 上传上下文
44#[derive(Debug, Clone)]
45pub struct UploadContext {
46    /// 文件数据
47    pub file_data: Vec<u8>,
48    /// 文件名
49    pub filename: String,
50    /// 可选的 key(路径)
51    pub key: Option<String>,
52    /// 其他字段
53    pub fields: std::collections::HashMap<String, String>,
54}
55
56/// 上传错误
57#[derive(Error, Debug)]
58pub enum UploadError {
59    #[error("非法的文件路径")]
60    InvalidPath,
61    #[error("文件路径超出允许范围")]
62    PathOutOfRange,
63    #[error("上传文件为空")]
64    EmptyFile,
65    #[error("上传失败: {0}")]
66    UploadFailed(String),
67    #[error("IO 错误: {0}")]
68    Io(#[from] std::io::Error),
69    #[error("HTTP 错误: {0}")]
70    Http(String),
71}
72
73pub type UploadResult<T> = Result<T, UploadError>;
74
75/// 上传钩子 trait
76///
77/// 对应 TypeScript 版本的 `BaseUpload`
78#[async_trait]
79pub trait UploadHook: Send + Sync {
80    /// 获取插件信息
81    fn plugin_info(&self) -> &PluginInfo;
82
83    /// 获得上传模式
84    async fn get_mode(&self) -> UploadResult<Mode>;
85
86    /// 获得原始操作对象(可选实现)
87    async fn get_meta_file_obj(&self) -> UploadResult<Option<serde_json::Value>> {
88        Ok(None)
89    }
90
91    /// 下载并上传
92    ///
93    /// 从 URL 下载文件并上传到存储服务
94    async fn down_and_upload(&self, url: &str, file_name: Option<&str>) -> UploadResult<String>;
95
96    /// 指定 Key(路径)上传,本地文件上传到存储服务
97    ///
98    /// 路径一致会覆盖源文件
99    async fn upload_with_key(&self, file_path: &Path, key: &str) -> UploadResult<String>;
100
101    /// 上传文件
102    async fn upload(&self, ctx: &UploadContext) -> UploadResult<String>;
103}
104
105/// 路径安全验证工具
106pub struct PathValidator;
107
108impl PathValidator {
109    /// 验证路径安全性,防止路径遍历攻击
110    ///
111    /// 对应 TypeScript 版本的 `sanitizePath`
112    pub fn sanitize_path(user_input: &str) -> UploadResult<String> {
113        if user_input.is_empty() {
114            return Ok(String::new());
115        }
116
117        // 检查是否包含路径遍历字符
118        if user_input.contains("..")
119            || user_input.contains("./")
120            || user_input.contains(".\\")
121            || user_input.contains('\\')
122            || user_input.contains("//")
123            || user_input.contains('\0')
124            || user_input.starts_with('/')
125            || Self::is_windows_absolute_path(user_input)
126        {
127            return Err(UploadError::InvalidPath);
128        }
129
130        // 规范化路径后再次检查
131        let normalized = Path::new(user_input)
132            .components()
133            .map(|c| c.as_os_str().to_string_lossy().to_string())
134            .collect::<Vec<_>>()
135            .join("/");
136
137        if normalized.contains("..") || normalized.starts_with('/') {
138            return Err(UploadError::InvalidPath);
139        }
140
141        Ok(normalized)
142    }
143
144    /// 检查是否是 Windows 绝对路径
145    fn is_windows_absolute_path(path: &str) -> bool {
146        // 检查 C:, D: 等格式
147        if path.len() >= 2 {
148            let first_char = path.chars().next().unwrap();
149            let second_char = path.chars().nth(1).unwrap();
150            if first_char.is_ascii_alphabetic() && second_char == ':' {
151                return true;
152            }
153        }
154        false
155    }
156
157    /// 验证最终路径是否在允许的目录内
158    ///
159    /// 对应 TypeScript 版本的 `validateTargetPath`
160    pub fn validate_target_path(target_path: &Path, base_path: &Path) -> UploadResult<()> {
161        let resolved_target = target_path.canonicalize().unwrap_or_else(|_| {
162            // 如果路径不存在,使用父目录来解析
163            target_path
164                .parent()
165                .unwrap_or(target_path)
166                .canonicalize()
167                .unwrap_or_else(|_| target_path.to_path_buf())
168        });
169
170        let resolved_base = base_path
171            .canonicalize()
172            .unwrap_or_else(|_| base_path.to_path_buf());
173
174        if !resolved_target.starts_with(&resolved_base) {
175            return Err(UploadError::PathOutOfRange);
176        }
177
178        Ok(())
179    }
180}