1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22pub enum ModeType {
23 #[default]
25 Local,
26 Oss,
28 Cos,
30 Other,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Mode {
37 pub mode: ModeType,
39 pub r#type: String,
41}
42
43#[derive(Debug, Clone)]
45pub struct UploadContext {
46 pub file_data: Vec<u8>,
48 pub filename: String,
50 pub key: Option<String>,
52 pub fields: std::collections::HashMap<String, String>,
54}
55
56#[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#[async_trait]
79pub trait UploadHook: Send + Sync {
80 fn plugin_info(&self) -> &PluginInfo;
82
83 async fn get_mode(&self) -> UploadResult<Mode>;
85
86 async fn get_meta_file_obj(&self) -> UploadResult<Option<serde_json::Value>> {
88 Ok(None)
89 }
90
91 async fn down_and_upload(&self, url: &str, file_name: Option<&str>) -> UploadResult<String>;
95
96 async fn upload_with_key(&self, file_path: &Path, key: &str) -> UploadResult<String>;
100
101 async fn upload(&self, ctx: &UploadContext) -> UploadResult<String>;
103}
104
105pub struct PathValidator;
107
108impl PathValidator {
109 pub fn sanitize_path(user_input: &str) -> UploadResult<String> {
113 if user_input.is_empty() {
114 return Ok(String::new());
115 }
116
117 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 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 fn is_windows_absolute_path(path: &str) -> bool {
146 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 pub fn validate_target_path(target_path: &Path, base_path: &Path) -> UploadResult<()> {
161 let resolved_target = target_path.canonicalize().unwrap_or_else(|_| {
162 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}