mod local;
pub use local::LocalUploadHook;
use crate::plugin::PluginInfo;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ModeType {
#[default]
Local,
Oss,
Cos,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Mode {
pub mode: ModeType,
pub r#type: String,
}
#[derive(Debug, Clone)]
pub struct UploadContext {
pub file_data: Vec<u8>,
pub filename: String,
pub key: Option<String>,
pub fields: std::collections::HashMap<String, String>,
}
#[derive(Error, Debug)]
pub enum UploadError {
#[error("非法的文件路径")]
InvalidPath,
#[error("文件路径超出允许范围")]
PathOutOfRange,
#[error("上传文件为空")]
EmptyFile,
#[error("上传失败: {0}")]
UploadFailed(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("HTTP 错误: {0}")]
Http(String),
}
pub type UploadResult<T> = Result<T, UploadError>;
#[async_trait]
pub trait UploadHook: Send + Sync {
fn plugin_info(&self) -> &PluginInfo;
async fn get_mode(&self) -> UploadResult<Mode>;
async fn get_meta_file_obj(&self) -> UploadResult<Option<serde_json::Value>> {
Ok(None)
}
async fn down_and_upload(&self, url: &str, file_name: Option<&str>) -> UploadResult<String>;
async fn upload_with_key(&self, file_path: &Path, key: &str) -> UploadResult<String>;
async fn upload(&self, ctx: &UploadContext) -> UploadResult<String>;
}
pub struct PathValidator;
impl PathValidator {
pub fn sanitize_path(user_input: &str) -> UploadResult<String> {
if user_input.is_empty() {
return Ok(String::new());
}
if user_input.contains("..")
|| user_input.contains("./")
|| user_input.contains(".\\")
|| user_input.contains('\\')
|| user_input.contains("//")
|| user_input.contains('\0')
|| user_input.starts_with('/')
|| Self::is_windows_absolute_path(user_input)
{
return Err(UploadError::InvalidPath);
}
let normalized = Path::new(user_input)
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/");
if normalized.contains("..") || normalized.starts_with('/') {
return Err(UploadError::InvalidPath);
}
Ok(normalized)
}
fn is_windows_absolute_path(path: &str) -> bool {
if path.len() >= 2 {
let first_char = path.chars().next().unwrap();
let second_char = path.chars().nth(1).unwrap();
if first_char.is_ascii_alphabetic() && second_char == ':' {
return true;
}
}
false
}
pub fn validate_target_path(target_path: &Path, base_path: &Path) -> UploadResult<()> {
let resolved_target = target_path.canonicalize().unwrap_or_else(|_| {
target_path
.parent()
.unwrap_or(target_path)
.canonicalize()
.unwrap_or_else(|_| target_path.to_path_buf())
});
let resolved_base = base_path
.canonicalize()
.unwrap_or_else(|_| base_path.to_path_buf());
if !resolved_target.starts_with(&resolved_base) {
return Err(UploadError::PathOutOfRange);
}
Ok(())
}
}