Skip to main content

sz_rust_core/upload/
storage.rs

1//! 存储对接模块 — 对齐 PHP `storage\Driver` + `storage\engine\*`
2//!
3//! 本模块实现 5 种存储驱动(本地/阿里云/腾讯云/七牛/S3),
4//! 复用 `sz-orm-storage` crate 的底层实现,对齐 PHP `app\common\library\storage` 命名空间。
5//!
6//! ## PHP 对齐
7//!
8//! ### 核心类映射
9//!
10//! | PHP 类 | Rust 结构 | 说明 |
11//! |---------|-----------|------|
12//! | `storage\Driver` | [`StorageDriver`] | 存储驱动(工厂 + 代理) |
13//! | `storage\engine\Server`(abstract) | [`StorageEngine`] trait | 存储引擎抽象 |
14//! | `storage\engine\Local` | [`LocalStorageEngine`] | 本地存储引擎 |
15//! | `storage\engine\Aliyun` | [`AliyunStorageEngine`] | 阿里云 OSS 引擎 |
16//! | `storage\engine\Qcloud` | [`QcloudStorageEngine`] | 腾讯云 COS 引擎 |
17//! | `storage\engine\Qiniu` | [`QiniuStorageEngine`] | 七牛云 Kodo 引擎 |
18//! | (无 PHP 对应) | [`S3StorageEngine`] | AWS S3 兼容引擎 |
19//!
20//! ### 核心方法映射
21//!
22//! | PHP 方法 | Rust 方法 | 说明 |
23//! |---------|-----------|------|
24//! | `Driver::getEngineClass($storage)` | [`StorageDriver::new`] | 工厂方法 |
25//! | `Server::setUploadFile($name)` | [`StorageEngine::set_upload_file`] | 设置上传文件 |
26//! | `Server::setUploadFileByReal($filePath, $extension)` | [`StorageEngine::set_upload_file_by_real`] | 内部上传 |
27//! | `Server::upload()` | [`StorageEngine::upload`] | 执行上传 |
28//! | `Server::delete($fileName)` | [`StorageEngine::delete`] | 删除文件 |
29//! | `Server::getFileName()` | [`StorageEngine::file_name`] | 获取文件名 |
30//! | `Server::getFileInfo()` | [`StorageEngine::file_info`] | 获取文件信息 |
31//! | `Server::getError()` | [`StorageEngine::error`] | 获取错误信息 |
32//! | `Server::getRealPath()` | [`StorageEngine::real_path`] | 获取真实路径 |
33//! | `Server::buildSaveName()`(private) | [`build_save_name`] | 生成保存文件名 |
34//!
35//! ## PHP 行为对齐(R5 硬约束)
36//!
37//! - **R5-16**:`buildSaveName` = `storage/{Ymd}/{YmdHis}{md5(realPath)[0..5]}{rand(0..9999) padded 4}.{ext}`
38//!   (对齐 PHP `Server.php` 第 103-111 行)
39//! - **R5-17**:`setUploadFileByReal` 的 `fileName` = `storage/{Ymd}/{basename}`(不用 YmdHis+md5+rand)
40//!   (对齐 PHP `Server.php` 第 45-59 行)
41//! - **R5-18**:`getRealPath` 区分 `isInternal`(内部用 `fileInfo.tmp_name`,外部用 `UploadedFile.path`)
42//!   (对齐 PHP `Server.php` 第 84-91 行)
43//! - **R5-19**:`Local::upload` 区分 `isInternal`(internal 用 `rename`,external 用 `putFile`)
44//!   (对齐 PHP `Local.php` 第 20-23 行)
45//! - **R5-20**:`Local::delete` Elvis 短路 `!file_exists($filePath) ?: unlink($filePath)`
46//!   (对齐 PHP `Local.php` 第 59-64 行)
47//! - **R5-21**:`Local::uploadByInternal` 失败设置 `error='upload write error'` 返回 `false`
48//!   (对齐 PHP `Local.php` 第 43-54 行)
49//! - **R5-22**:云存储 `upload` 成功返回 `true`(PHP 行为),Rust 端返回 `Ok(Some(save_name))`
50//!   (对齐 PHP `Qcloud.php`/`Aliyun.php`/`Qiniu.php`)
51//! - **R5-23**:`Local::uploadByExternal` 返回 `saveName`(`Filesystem::disk('public')->putFile` 返回路径)
52//!   (对齐 PHP `Local.php` 第 28-38 行)
53//!
54//! ## PHP 源码参考
55//!
56//! - `e:\vue\test\鲜视达\server\app\common\library\storage\Driver.php`(118 行)
57//! - `e:\vue\test\鲜视达\server\app\common\library\storage\engine\Server.php`(112 行)
58//! - `e:\vue\test\鲜视达\server\app\common\library\storage\engine\Local.php`(73 行)
59//! - `e:\vue\test\鲜视达\server\app\common\library\storage\engine\Qcloud.php`(84 行)
60//! - `e:\vue\test\鲜视达\server\app\common\library\storage\engine\Aliyun.php`(76 行)
61//! - `e:\vue\test\鲜视达\server\app\common\library\storage\engine\Qiniu.php`(76 行)
62
63use std::path::{Path, PathBuf};
64
65use async_trait::async_trait;
66use chrono::Local;
67use md5::{Digest, Md5};
68use rand::Rng;
69use sz_orm_storage::Storage;
70
71use super::{UploadError, UploadedFile};
72
73// ============================================================================
74// 存储引擎类型
75// ============================================================================
76
77/// 存储引擎类型 — 对齐 PHP `storage\Driver::getEngineClass($storage)` 的 `$storage` 参数
78///
79/// PHP 第 108-116 行:
80/// ```php
81/// private function getEngineClass($storage = 'qcloud') {
82///     $engineName = is_null($storage) ? $this->config['default'] : $storage;
83///     $classSpace = __NAMESPACE__ . '\\engine\\' . ucfirst($engineName);
84///     if (!class_exists($classSpace)) {
85///         throw new Exception('未找到存储引擎类: ' . $engineName);
86///     }
87///     return new $classSpace($this->config['engine'][$engineName]);
88/// }
89/// ```
90///
91/// Rust 端支持 5 种引擎(PHP 端 4 种 + S3):
92/// - `Local` → [`LocalStorageEngine`](对齐 PHP `engine\Local`)
93/// - `Aliyun` → [`AliyunStorageEngine`](对齐 PHP `engine\Aliyun`)
94/// - `Qcloud` → [`QcloudStorageEngine`](对齐 PHP `engine\Qcloud`)
95/// - `Qiniu` → [`QiniuStorageEngine`](对齐 PHP `engine\Qiniu`)
96/// - `S3` → [`S3StorageEngine`](Rust 扩展,无 PHP 对应)
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum StorageEngineKind {
99    /// 本地存储(对齐 PHP `engine\Local`)
100    Local,
101    /// 阿里云 OSS(对齐 PHP `engine\Aliyun`)
102    Aliyun,
103    /// 腾讯云 COS(对齐 PHP `engine\Qcloud`)
104    Qcloud,
105    /// 七牛云 Kodo(对齐 PHP `engine\Qiniu`)
106    Qiniu,
107    /// AWS S3 兼容(Rust 扩展)
108    S3,
109}
110
111impl StorageEngineKind {
112    /// 从字符串解析引擎类型 — 对齐 PHP `ucfirst($engineName)` + `class_exists` 检查
113    ///
114    /// PHP 行为:大小写不敏感(`ucfirst` 仅首字母大写),不匹配抛异常。
115    /// Rust 端:大小写不敏感,不匹配返回 `None`。
116    pub fn parse(s: &str) -> Option<Self> {
117        match s.to_lowercase().as_str() {
118            "local" => Some(Self::Local),
119            "aliyun" => Some(Self::Aliyun),
120            "qcloud" => Some(Self::Qcloud),
121            "qiniu" => Some(Self::Qiniu),
122            "s3" => Some(Self::S3),
123            _ => None,
124        }
125    }
126
127    /// 获取引擎类型字符串
128    pub fn as_str(self) -> &'static str {
129        match self {
130            Self::Local => "local",
131            Self::Aliyun => "aliyun",
132            Self::Qcloud => "qcloud",
133            Self::Qiniu => "qiniu",
134            Self::S3 => "s3",
135        }
136    }
137}
138
139// ============================================================================
140// 引擎配置
141// ============================================================================
142
143/// 引擎配置 — 对齐 PHP `config['engine'][$engineName]` 配置数组
144///
145/// PHP 配置结构(`Setting.php` 第 201-228 行):
146/// ```php
147/// 'storage' => [
148///     'default' => 'local',
149///     'engine' => [
150///         'local' => [],
151///         'qiniu' => ['bucket' => '', 'access_key' => '', 'secret_key' => '', 'domain' => 'http://'],
152///         'aliyun' => ['bucket' => '', 'access_key_id' => '', 'access_key_secret' => '', 'domain' => 'http://'],
153///         'qcloud' => ['bucket' => '', 'region' => '', 'secret_id' => '', 'secret_key' => '', 'domain' => 'http://'],
154///     ],
155/// ],
156/// ```
157///
158/// Rust 端统一为一个结构体,不同引擎使用不同字段。
159#[derive(Debug, Clone, Default)]
160pub struct EngineConfig {
161    /// 存储桶名称(对齐 PHP `bucket`)
162    pub bucket: String,
163    /// 地域(对齐 PHP `region`,仅 Qcloud 使用)
164    pub region: String,
165    /// 端点(对齐 PHP `domain`/`endpoint`,Aliyun 使用)
166    pub endpoint: String,
167    /// 自定义域名(对齐 PHP `domain`)
168    pub domain: String,
169    /// 阿里云 AccessKey ID(对齐 PHP `access_key_id`)
170    pub access_key_id: String,
171    /// 阿里云 AccessKey Secret(对齐 PHP `access_key_secret`)
172    pub access_key_secret: String,
173    /// 腾讯云 SecretId(对齐 PHP `secret_id`)
174    pub secret_id: String,
175    /// 腾讯云/七牛 SecretKey(对齐 PHP `secret_key`)
176    pub secret_key: String,
177    /// 七牛 AccessKey(对齐 PHP `access_key`)
178    pub access_key: String,
179    /// 本地存储基础路径(对齐 PHP `WEB_PATH`)
180    pub base_path: String,
181}
182
183impl EngineConfig {
184    /// 创建空配置
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    /// 设置 bucket
190    pub fn with_bucket(mut self, bucket: impl Into<String>) -> Self {
191        self.bucket = bucket.into();
192        self
193    }
194
195    /// 设置 region
196    pub fn with_region(mut self, region: impl Into<String>) -> Self {
197        self.region = region.into();
198        self
199    }
200
201    /// 设置 endpoint
202    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
203        self.endpoint = endpoint.into();
204        self
205    }
206
207    /// 设置 domain
208    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
209        self.domain = domain.into();
210        self
211    }
212
213    /// 设置 base_path
214    pub fn with_base_path(mut self, base_path: impl Into<String>) -> Self {
215        self.base_path = base_path.into();
216        self
217    }
218
219    /// 设置 access_key_id
220    pub fn with_access_key_id(mut self, key: impl Into<String>) -> Self {
221        self.access_key_id = key.into();
222        self
223    }
224
225    /// 设置 access_key_secret
226    pub fn with_access_key_secret(mut self, key: impl Into<String>) -> Self {
227        self.access_key_secret = key.into();
228        self
229    }
230
231    /// 设置 secret_id
232    pub fn with_secret_id(mut self, key: impl Into<String>) -> Self {
233        self.secret_id = key.into();
234        self
235    }
236
237    /// 设置 secret_key
238    pub fn with_secret_key(mut self, key: impl Into<String>) -> Self {
239        self.secret_key = key.into();
240        self
241    }
242
243    /// 设置 access_key(七牛)
244    pub fn with_access_key(mut self, key: impl Into<String>) -> Self {
245        self.access_key = key.into();
246        self
247    }
248}
249
250// ============================================================================
251// 上传文件信息
252// ============================================================================
253
254/// 上传文件信息 — 对齐 PHP `Server::setUploadFileByReal` 中的 `$fileInfo` 数组
255///
256/// PHP 第 45-59 行:
257/// ```php
258/// public function setUploadFileByReal($filePath, $extension) {
259///     $this->isInternal = true;
260///     $this->fileInfo = [
261///         'name' => basename($filePath),
262///         'size' => filesize($filePath),
263///         'extension' => $extension,
264///         'tmp_name' => $filePath,
265///         'error' => 0,
266///         'isInternal' => $this->isInternal
267///     ];
268///     $this->fileName = 'storage/'.date('Ymd') ."/". $this->fileInfo['name'];
269/// }
270/// ```
271#[derive(Debug, Clone)]
272pub struct UploadFileInfo {
273    /// 文件名(对齐 PHP `name` = `basename($filePath)`)
274    pub name: String,
275    /// 文件大小(字节,对齐 PHP `size` = `filesize($filePath)`)
276    pub size: u64,
277    /// 扩展名(对齐 PHP `extension`)
278    pub extension: String,
279    /// 临时文件路径(对齐 PHP `tmp_name`)
280    pub tmp_name: PathBuf,
281    /// 错误码(对齐 PHP `error`,0 表示无错误)
282    pub error: i32,
283    /// 是否内部上传(对齐 PHP `isInternal`)
284    pub is_internal: bool,
285}
286
287impl UploadFileInfo {
288    /// 从 `UploadedFile` 创建文件信息 — 对齐 PHP `Server::setUploadFile` 的隐式行为
289    ///
290    /// PHP `setUploadFile` 直接将 `Request::file($name)` 赋给 `$this->file`,
291    /// 然后调用 `buildSaveName()` 生成文件名。
292    /// Rust 端将 `UploadedFile` 转换为 `UploadFileInfo`。
293    pub fn from_uploaded_file(file: &UploadedFile) -> Result<Self, UploadError> {
294        let path = file.as_file().path();
295        let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
296        Ok(Self {
297            name: file.original_name().to_string(),
298            size,
299            extension: file.original_extension(),
300            tmp_name: path.to_path_buf(),
301            error: file.error_code() as i32,
302            is_internal: false,
303        })
304    }
305
306    /// 从真实路径创建文件信息 — 对齐 PHP `Server::setUploadFileByReal`
307    ///
308    /// PHP 行为:`isInternal = true`,`name = basename($filePath)`,`size = filesize($filePath)`。
309    pub fn from_real_path<P: AsRef<Path>>(path: P, extension: &str) -> Result<Self, UploadError> {
310        let path = path.as_ref();
311        if !path.exists() {
312            return Err(UploadError::FileNotFound(
313                path.to_string_lossy().to_string(),
314            ));
315        }
316        let name = path
317            .file_name()
318            .map(|n| n.to_string_lossy().to_string())
319            .unwrap_or_default();
320        let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
321        Ok(Self {
322            name,
323            size,
324            extension: extension.to_string(),
325            tmp_name: path.to_path_buf(),
326            error: 0,
327            is_internal: true,
328        })
329    }
330}
331
332// ============================================================================
333// 保存文件名生成
334// ============================================================================
335
336/// 生成保存文件名 — 对齐 PHP `Server::buildSaveName` 第 103-111 行
337///
338/// PHP 行为:
339/// ```php
340/// private function buildSaveName() {
341///     $realPath = $this->file->getPathname();
342///     $ext = $this->file->getOriginalExtension();
343///     return 'storage/'.date('Ymd') ."/".date('YmdHis') . substr(md5($realPath), 0, 5)
344///         . str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT) . ".{$ext}";
345/// }
346/// ```
347///
348/// Rust 端格式:`storage/{Ymd}/{YmdHis}{md5(realPath)[0..5]}{rand(0..9999) padded 4}.{ext}`
349///
350/// 注意:PHP `rand(0, 9999)` 是伪随机数,Rust 端使用 `rand::thread_rng().gen_range(0..=9999)`。
351pub fn build_save_name(real_path: &Path, extension: &str) -> String {
352    let now = Local::now();
353    let ymd = now.format("%Y%m%d").to_string();
354    let ymd_his = now.format("%Y%m%d%H%M%S").to_string();
355
356    // 对齐 PHP `substr(md5($realPath), 0, 5)`
357    let mut md5 = Md5::new();
358    md5.update(real_path.to_string_lossy().as_bytes());
359    let md5_hex = hex::encode(md5.finalize());
360    let md5_prefix = &md5_hex[..5];
361
362    // 对齐 PHP `str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT)`
363    let rand_num: u32 = rand::thread_rng().gen_range(0..=9999);
364    let rand_padded = format!("{:04}", rand_num);
365
366    // 对齐 PHP `".{$ext}"` — 扩展名为空时不加点
367    // 注意:PHP 格式为 `storage/{Ymd}/{YmdHis}{md5[5]}{rand[4]}.{ext}`(YmdHis 后无 `/`)
368    if extension.is_empty() {
369        format!("storage/{}/{}{}{}", ymd, ymd_his, md5_prefix, rand_padded)
370    } else {
371        format!(
372            "storage/{}/{}{}{}.{}",
373            ymd, ymd_his, md5_prefix, rand_padded, extension
374        )
375    }
376}
377
378/// 生成内部上传的保存文件名 — 对齐 PHP `Server::setUploadFileByReal` 第 58 行
379///
380/// PHP 行为:
381/// ```php
382/// $this->fileName = 'storage/'.date('Ymd') ."/". $this->fileInfo['name'];
383/// ```
384///
385/// 注意:与 `buildSaveName` 不同,内部上传使用 `basename` 而非 `YmdHis+md5+rand`。
386pub fn build_internal_save_name(file_path: &Path) -> String {
387    let now = Local::now();
388    let ymd = now.format("%Y%m%d").to_string();
389    let basename = file_path
390        .file_name()
391        .map(|n| n.to_string_lossy().to_string())
392        .unwrap_or_default();
393    format!("storage/{}/{}", ymd, basename)
394}
395
396// ============================================================================
397// StorageEngine trait
398// ============================================================================
399
400/// 存储引擎抽象 — 对齐 PHP `storage\engine\Server`(abstract)
401///
402/// PHP 抽象方法:`upload()` / `delete($fileName)` / `getFileName()`
403/// PHP 具体方法:`setUploadFile` / `setUploadFileByReal` / `getFileInfo` / `getRealPath` / `getError` / `buildSaveName`
404///
405/// Rust 端使用 `async_trait` 因为 `upload`/`delete` 涉及 IO 操作。
406#[async_trait]
407pub trait StorageEngine: Send + Sync {
408    /// 执行文件上传 — 对齐 PHP `Server::upload()`(abstract)
409    ///
410    /// 返回值:
411    /// - `Ok(Some(save_name))`:Local 外部上传返回保存路径
412    /// - `Ok(None)`:Local 内部上传或云存储上传成功(PHP 返回 `true`)
413    /// - `Err(_)`:上传失败
414    async fn upload(&mut self) -> Result<Option<String>, UploadError>;
415
416    /// 执行文件删除 — 对齐 PHP `Server::delete($fileName)`(abstract)
417    ///
418    /// 返回值:
419    /// - `Ok(true)`:删除成功或文件不存在(对齐 PHP Elvis 短路)
420    /// - `Ok(false)`:删除失败(PHP 设置 `error` 返回 `false`)
421    /// - `Err(_)`:系统错误
422    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError>;
423
424    /// 获取文件名 — 对齐 PHP `Server::getFileName()`
425    fn file_name(&self) -> Option<&str>;
426
427    /// 获取文件信息 — 对齐 PHP `Server::getFileInfo()`
428    fn file_info(&self) -> Option<&UploadFileInfo>;
429
430    /// 获取错误信息 — 对齐 PHP `Server::getError()`
431    fn error(&self) -> Option<&str>;
432
433    /// 设置上传文件 — 对齐 PHP `Server::setUploadFile($name)`
434    ///
435    /// PHP 行为:从 `Request::file($name)` 获取文件,调用 `buildSaveName()` 生成文件名。
436    /// Rust 端:从 `UploadedFile` 设置,调用 `build_save_name()` 生成文件名。
437    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError>;
438
439    /// 设置内部上传文件 — 对齐 PHP `Server::setUploadFileByReal($filePath, $extension)`
440    ///
441    /// PHP 行为:设置 `isInternal = true`,构造 `fileInfo`,文件名为 `storage/{Ymd}/{basename}`。
442    fn set_upload_file_by_real(
443        &mut self,
444        file_path: &Path,
445        extension: &str,
446    ) -> Result<(), UploadError>;
447
448    /// 是否内部上传
449    fn is_internal(&self) -> bool;
450
451    /// 获取真实路径 — 对齐 PHP `Server::getRealPath()` 第 84-91 行
452    ///
453    /// PHP 行为:
454    /// - `isInternal == true` → 返回 `$this->fileInfo['tmp_name']`
455    /// - `isInternal == false` → 返回 `request()->file('iFile')->getRealPath()`
456    fn real_path(&self) -> Option<&Path> {
457        self.file_info().map(|info| info.tmp_name.as_path())
458    }
459}
460
461// ============================================================================
462// LocalStorageEngine
463// ============================================================================
464
465/// 本地存储引擎 — 对齐 PHP `storage\engine\Local`
466///
467/// PHP 第 20-23 行:`upload` 区分 `isInternal`
468/// PHP 第 28-38 行:`uploadByExternal` 用 `Filesystem::disk('public')->putFile`
469/// PHP 第 43-54 行:`uploadByInternal` 用 `rename`
470/// PHP 第 59-64 行:`delete` Elvis 短路 `!file_exists($filePath) ?: unlink($filePath)`
471pub struct LocalStorageEngine {
472    /// 引擎配置
473    config: EngineConfig,
474    /// 文件信息
475    file_info: Option<UploadFileInfo>,
476    /// 保存文件名
477    file_name: Option<String>,
478    /// 错误信息
479    error: Option<String>,
480    /// 是否内部上传
481    is_internal: bool,
482    /// 外部上传时的源文件路径(用于 copy)
483    upload_source_path: Option<PathBuf>,
484}
485
486impl LocalStorageEngine {
487    /// 创建本地存储引擎实例
488    pub fn new(config: EngineConfig) -> Self {
489        Self {
490            config,
491            file_info: None,
492            file_name: None,
493            error: None,
494            is_internal: false,
495            upload_source_path: None,
496        }
497    }
498
499    /// 获取上传目录 — 对齐 PHP `WEB_PATH . 'uploads'`
500    ///
501    /// PHP 行为:`$uplodDir = WEB_PATH . 'uploads';`
502    /// Rust 端:`config.base_path` + `uploads`
503    pub fn upload_dir(&self) -> PathBuf {
504        if self.config.base_path.is_empty() {
505            PathBuf::from("uploads")
506        } else {
507            PathBuf::from(&self.config.base_path).join("uploads")
508        }
509    }
510
511    /// 内部上传 — 对齐 PHP `Local::uploadByInternal` 第 43-54 行
512    ///
513    /// PHP 行为:
514    /// ```php
515    /// private function uploadByInternal() {
516    ///     $uplodDir = WEB_PATH . 'uploads';
517    ///     $realPath = $this->getRealPath();
518    ///     if (!rename($realPath, "{$uplodDir}/$this->fileName")) {
519    ///         $this->error = 'upload write error';
520    ///         return false;
521    ///     }
522    ///     return true;
523    /// }
524    /// ```
525    #[tracing::instrument(skip(self))]
526    async fn upload_by_internal(&mut self) -> Result<bool, UploadError> {
527        let target = self.upload_dir().join(
528            self.file_name
529                .as_ref()
530                .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
531                .as_str(),
532        );
533        let real_path = self
534            .file_info
535            .as_ref()
536            .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
537            .tmp_name
538            .clone();
539
540        // 对齐 PHP `rename($realPath, "{$uplodDir}/$this->fileName")`
541        if let Some(parent) = target.parent() {
542            tokio::fs::create_dir_all(parent).await?;
543        }
544        match tokio::fs::rename(&real_path, &target).await {
545            Ok(_) => Ok(true),
546            Err(e) => {
547                // 对齐 PHP `$this->error = 'upload write error'; return false;`
548                self.error = Some("upload write error".to_string());
549                Err(UploadError::MoveFailed {
550                    from: real_path.to_string_lossy().to_string(),
551                    to: target.to_string_lossy().to_string(),
552                    error: e.to_string(),
553                })
554            }
555        }
556    }
557
558    /// 外部上传 — 对齐 PHP `Local::uploadByExternal` 第 28-38 行
559    ///
560    /// PHP 行为:
561    /// ```php
562    /// private function uploadByExternal() {
563    ///     $saveName = '';
564    ///     try {
565    ///         $saveName = Filesystem::disk('public')->putFile('', $this->file);
566    ///     } catch (\Exception $e) {
567    ///         log_write('文件上传异常:'.$e->getMessage());
568    ///     }
569    ///     return $saveName;
570    /// }
571    /// ```
572    ///
573    /// Rust 端:使用 `tokio::fs::copy` 模拟 `putFile`,返回保存路径。
574    #[tracing::instrument(skip(self))]
575    async fn upload_by_external(&mut self) -> Result<String, UploadError> {
576        let file_name = self
577            .file_name
578            .as_ref()
579            .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
580            .clone();
581        let source = self
582            .upload_source_path
583            .as_ref()
584            .or_else(|| self.file_info.as_ref().map(|info| &info.tmp_name))
585            .cloned()
586            .ok_or_else(|| UploadError::UploadFailed("source path not set".to_string()))?;
587
588        let target = self.upload_dir().join(&file_name);
589        if let Some(parent) = target.parent() {
590            tokio::fs::create_dir_all(parent).await?;
591        }
592        // 对齐 PHP `Filesystem::disk('public')->putFile('', $this->file)`
593        // Rust 端用 copy(保留原文件,对齐 think Filesystem public disk 的 putFile 行为)
594        tokio::fs::copy(&source, &target).await?;
595        Ok(file_name)
596    }
597}
598
599#[async_trait]
600impl StorageEngine for LocalStorageEngine {
601    #[tracing::instrument(skip(self))]
602    async fn upload(&mut self) -> Result<Option<String>, UploadError> {
603        // 对齐 PHP 第 20-23 行:`return $this->isInternal ? $this->uploadByInternal() : $this->uploadByExternal();`
604        if self.is_internal {
605            // 内部上传返回 true/false(PHP),Rust 端用 Ok(None) 表示成功
606            self.upload_by_internal().await?;
607            Ok(None)
608        } else {
609            // 外部上传返回 saveName
610            let save_name = self.upload_by_external().await?;
611            Ok(Some(save_name))
612        }
613    }
614
615    #[tracing::instrument(skip(self))]
616    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
617        // 对齐 PHP 第 59-64 行:
618        // `$filePath = WEB_PATH . "uploads/{$fileName}";`
619        // `return !file_exists($filePath) ?: unlink($filePath);`
620
621        // 安全:防止路径遍历攻击(PHP 原始代码未做此检查)
622        // 拒绝包含 `..` 或绝对路径的 file_name
623        if file_name.contains("..") || std::path::Path::new(file_name).is_absolute() {
624            return Err(UploadError::InvalidFileName(file_name.to_string()));
625        }
626
627        let file_path = self.upload_dir().join(file_name);
628
629        // 安全:验证最终路径仍在 upload_dir 内(canonicalize 后比较)
630        if let (Ok(upload_canon), Ok(file_canon)) = (
631            std::fs::canonicalize(self.upload_dir()),
632            std::fs::canonicalize(&file_path),
633        ) {
634            if !file_canon.starts_with(&upload_canon) {
635                return Err(UploadError::InvalidFileName(file_name.to_string()));
636            }
637        }
638
639        if !file_path.exists() {
640            // 对齐 PHP `!file_exists($filePath)` 为 true 时短路返回 true
641            return Ok(true);
642        }
643        // 对齐 PHP `unlink($filePath)`
644        match tokio::fs::remove_file(&file_path).await {
645            Ok(_) => Ok(true),
646            Err(e) => {
647                // PHP `unlink` 失败返回 false(不抛异常),Rust 端记录错误返回 false
648                self.error = Some(e.to_string());
649                Ok(false)
650            }
651        }
652    }
653
654    fn file_name(&self) -> Option<&str> {
655        self.file_name.as_deref()
656    }
657
658    fn file_info(&self) -> Option<&UploadFileInfo> {
659        self.file_info.as_ref()
660    }
661
662    fn error(&self) -> Option<&str> {
663        self.error.as_deref()
664    }
665
666    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
667        // 对齐 PHP `Server::setUploadFile` 第 32-40 行:
668        // `$this->file = Request::file($name);`
669        // `$this->fileName = $this->buildSaveName();`
670        let info = UploadFileInfo::from_uploaded_file(file)?;
671        let save_name = build_save_name(&info.tmp_name, &info.extension);
672        self.upload_source_path = Some(info.tmp_name.clone());
673        self.file_info = Some(info);
674        self.file_name = Some(save_name);
675        self.is_internal = false;
676        Ok(())
677    }
678
679    fn set_upload_file_by_real(
680        &mut self,
681        file_path: &Path,
682        extension: &str,
683    ) -> Result<(), UploadError> {
684        // 对齐 PHP `Server::setUploadFileByReal` 第 45-59 行:
685        // `$this->isInternal = true;`
686        // `$this->fileInfo = [...]`
687        // `$this->fileName = 'storage/'.date('Ymd') ."/". $this->fileInfo['name'];`
688        let info = UploadFileInfo::from_real_path(file_path, extension)?;
689        let save_name = build_internal_save_name(file_path);
690        self.file_info = Some(info);
691        self.file_name = Some(save_name);
692        self.is_internal = true;
693        Ok(())
694    }
695
696    fn is_internal(&self) -> bool {
697        self.is_internal
698    }
699}
700
701// ============================================================================
702// AliyunStorageEngine
703// ============================================================================
704
705/// 阿里云 OSS 存储引擎 — 对齐 PHP `storage\engine\Aliyun`
706///
707/// PHP 行为:
708/// - `upload`:`OssClient::uploadFile($bucket, $fileName, $realPath)`,成功返回 `true`,失败设置 `error` 返回 `false`
709/// - `delete`:`OssClient::deleteObject($bucket, $fileName)`,成功返回 `true`,失败设置 `error` 返回 `false`
710///
711/// Rust 端复用 `sz-orm-storage::AliyunOssStorage`。
712pub struct AliyunStorageEngine {
713    /// 引擎配置
714    config: EngineConfig,
715    /// 文件信息
716    file_info: Option<UploadFileInfo>,
717    /// 保存文件名
718    file_name: Option<String>,
719    /// 错误信息
720    error: Option<String>,
721    /// 是否内部上传
722    is_internal: bool,
723}
724
725impl AliyunStorageEngine {
726    /// 创建阿里云存储引擎实例
727    pub fn new(config: EngineConfig) -> Self {
728        Self {
729            config,
730            file_info: None,
731            file_name: None,
732            error: None,
733            is_internal: false,
734        }
735    }
736
737    /// 创建底层存储
738    fn create_storage(&self) -> sz_orm_storage::AliyunOssStorage {
739        sz_orm_storage::AliyunOssStorage::new(
740            self.config.bucket.clone(),
741            self.config.endpoint.clone(),
742        )
743    }
744}
745
746#[async_trait]
747impl StorageEngine for AliyunStorageEngine {
748    #[tracing::instrument(skip(self))]
749    async fn upload(&mut self) -> Result<Option<String>, UploadError> {
750        // 对齐 PHP `Aliyun::upload` 第 27-46 行
751        let file_name = self
752            .file_name
753            .as_ref()
754            .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
755            .clone();
756        let info = self
757            .file_info
758            .as_ref()
759            .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
760            .clone();
761
762        let storage = self.create_storage();
763        let data = tokio::fs::read(&info.tmp_name).await?;
764        let content_type = mime_guess::from_path(&info.tmp_name)
765            .first_or_octet_stream()
766            .to_string();
767
768        match storage.put(&file_name, &data, &content_type).await {
769            Ok(_) => {
770                // 对齐 PHP `return true;`
771                Ok(Some(file_name))
772            }
773            Err(e) => {
774                // 对齐 PHP `$this->error = $e->getMessage(); return false;`
775                let msg = e.to_string();
776                self.error = Some(msg.clone());
777                Err(UploadError::UploadFailed(msg))
778            }
779        }
780    }
781
782    #[tracing::instrument(skip(self))]
783    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
784        // 对齐 PHP `Aliyun::delete` 第 51-66 行
785        let storage = self.create_storage();
786        match storage.delete(file_name).await {
787            Ok(_) => Ok(true),
788            Err(e) => {
789                let msg = e.to_string();
790                self.error = Some(msg);
791                Ok(false)
792            }
793        }
794    }
795
796    fn file_name(&self) -> Option<&str> {
797        self.file_name.as_deref()
798    }
799
800    fn file_info(&self) -> Option<&UploadFileInfo> {
801        self.file_info.as_ref()
802    }
803
804    fn error(&self) -> Option<&str> {
805        self.error.as_deref()
806    }
807
808    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
809        let info = UploadFileInfo::from_uploaded_file(file)?;
810        let save_name = build_save_name(&info.tmp_name, &info.extension);
811        self.file_info = Some(info);
812        self.file_name = Some(save_name);
813        self.is_internal = false;
814        Ok(())
815    }
816
817    fn set_upload_file_by_real(
818        &mut self,
819        file_path: &Path,
820        extension: &str,
821    ) -> Result<(), UploadError> {
822        let info = UploadFileInfo::from_real_path(file_path, extension)?;
823        let save_name = build_internal_save_name(file_path);
824        self.file_info = Some(info);
825        self.file_name = Some(save_name);
826        self.is_internal = true;
827        Ok(())
828    }
829
830    fn is_internal(&self) -> bool {
831        self.is_internal
832    }
833}
834
835// ============================================================================
836// QcloudStorageEngine
837// ============================================================================
838
839/// 腾讯云 COS 存储引擎 — 对齐 PHP `storage\engine\Qcloud`
840///
841/// PHP 行为:
842/// - `upload`:`cosClient->putObject(['Bucket', 'Key', 'Body' => fopen(...)])`,成功返回 `true`
843/// - `delete`:`cosClient->deleteObject(['Bucket', 'Key'])`,成功返回 `true`
844///
845/// Rust 端复用 `sz-orm-storage::TencentCosStorage`。
846pub struct QcloudStorageEngine {
847    /// 引擎配置
848    config: EngineConfig,
849    /// 文件信息
850    file_info: Option<UploadFileInfo>,
851    /// 保存文件名
852    file_name: Option<String>,
853    /// 错误信息
854    error: Option<String>,
855    /// 是否内部上传
856    is_internal: bool,
857}
858
859impl QcloudStorageEngine {
860    /// 创建腾讯云存储引擎实例
861    pub fn new(config: EngineConfig) -> Self {
862        Self {
863            config,
864            file_info: None,
865            file_name: None,
866            error: None,
867            is_internal: false,
868        }
869    }
870
871    /// 创建底层存储
872    fn create_storage(&self) -> sz_orm_storage::TencentCosStorage {
873        sz_orm_storage::TencentCosStorage::new(
874            self.config.bucket.clone(),
875            self.config.region.clone(),
876        )
877    }
878}
879
880#[async_trait]
881impl StorageEngine for QcloudStorageEngine {
882    #[tracing::instrument(skip(self))]
883    async fn upload(&mut self) -> Result<Option<String>, UploadError> {
884        // 对齐 PHP `Qcloud::upload` 第 44-59 行
885        let file_name = self
886            .file_name
887            .as_ref()
888            .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
889            .clone();
890        let info = self
891            .file_info
892            .as_ref()
893            .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
894            .clone();
895
896        let storage = self.create_storage();
897        let data = tokio::fs::read(&info.tmp_name).await?;
898        let content_type = mime_guess::from_path(&info.tmp_name)
899            .first_or_octet_stream()
900            .to_string();
901
902        match storage.put(&file_name, &data, &content_type).await {
903            Ok(_) => Ok(Some(file_name)),
904            Err(e) => {
905                let msg = e.to_string();
906                self.error = Some(msg.clone());
907                Err(UploadError::UploadFailed(msg))
908            }
909        }
910    }
911
912    #[tracing::instrument(skip(self))]
913    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
914        let storage = self.create_storage();
915        match storage.delete(file_name).await {
916            Ok(_) => Ok(true),
917            Err(e) => {
918                let msg = e.to_string();
919                self.error = Some(msg);
920                Ok(false)
921            }
922        }
923    }
924
925    fn file_name(&self) -> Option<&str> {
926        self.file_name.as_deref()
927    }
928
929    fn file_info(&self) -> Option<&UploadFileInfo> {
930        self.file_info.as_ref()
931    }
932
933    fn error(&self) -> Option<&str> {
934        self.error.as_deref()
935    }
936
937    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
938        let info = UploadFileInfo::from_uploaded_file(file)?;
939        let save_name = build_save_name(&info.tmp_name, &info.extension);
940        self.file_info = Some(info);
941        self.file_name = Some(save_name);
942        self.is_internal = false;
943        Ok(())
944    }
945
946    fn set_upload_file_by_real(
947        &mut self,
948        file_path: &Path,
949        extension: &str,
950    ) -> Result<(), UploadError> {
951        let info = UploadFileInfo::from_real_path(file_path, extension)?;
952        let save_name = build_internal_save_name(file_path);
953        self.file_info = Some(info);
954        self.file_name = Some(save_name);
955        self.is_internal = true;
956        Ok(())
957    }
958
959    fn is_internal(&self) -> bool {
960        self.is_internal
961    }
962}
963
964// ============================================================================
965// QiniuStorageEngine
966// ============================================================================
967
968/// 七牛云 Kodo 存储引擎 — 对齐 PHP `storage\engine\Qiniu`
969///
970/// PHP 行为:
971/// - `upload`:`UploadManager::putFile($token, $fileName, $realPath)`,成功返回 `true`
972/// - `delete`:`BucketManager::delete($bucket, $fileName)`,成功返回 `true`
973///
974/// Rust 端复用 `sz-orm-storage::QiniuKodoStorage`。
975pub struct QiniuStorageEngine {
976    /// 引擎配置
977    config: EngineConfig,
978    /// 文件信息
979    file_info: Option<UploadFileInfo>,
980    /// 保存文件名
981    file_name: Option<String>,
982    /// 错误信息
983    error: Option<String>,
984    /// 是否内部上传
985    is_internal: bool,
986}
987
988impl QiniuStorageEngine {
989    /// 创建七牛存储引擎实例
990    pub fn new(config: EngineConfig) -> Self {
991        Self {
992            config,
993            file_info: None,
994            file_name: None,
995            error: None,
996            is_internal: false,
997        }
998    }
999
1000    /// 创建底层存储
1001    fn create_storage(&self) -> sz_orm_storage::QiniuKodoStorage {
1002        sz_orm_storage::QiniuKodoStorage::new(self.config.bucket.clone())
1003    }
1004}
1005
1006#[async_trait]
1007impl StorageEngine for QiniuStorageEngine {
1008    #[tracing::instrument(skip(self))]
1009    async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1010        // 对齐 PHP `Qiniu::upload` 第 28-50 行
1011        let file_name = self
1012            .file_name
1013            .as_ref()
1014            .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1015            .clone();
1016        let info = self
1017            .file_info
1018            .as_ref()
1019            .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1020            .clone();
1021
1022        let storage = self.create_storage();
1023        let data = tokio::fs::read(&info.tmp_name).await?;
1024        let content_type = mime_guess::from_path(&info.tmp_name)
1025            .first_or_octet_stream()
1026            .to_string();
1027
1028        match storage.put(&file_name, &data, &content_type).await {
1029            Ok(_) => Ok(Some(file_name)),
1030            Err(e) => {
1031                let msg = e.to_string();
1032                self.error = Some(msg.clone());
1033                Err(UploadError::UploadFailed(msg))
1034            }
1035        }
1036    }
1037
1038    #[tracing::instrument(skip(self))]
1039    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1040        let storage = self.create_storage();
1041        match storage.delete(file_name).await {
1042            Ok(_) => Ok(true),
1043            Err(e) => {
1044                let msg = e.to_string();
1045                self.error = Some(msg);
1046                Ok(false)
1047            }
1048        }
1049    }
1050
1051    fn file_name(&self) -> Option<&str> {
1052        self.file_name.as_deref()
1053    }
1054
1055    fn file_info(&self) -> Option<&UploadFileInfo> {
1056        self.file_info.as_ref()
1057    }
1058
1059    fn error(&self) -> Option<&str> {
1060        self.error.as_deref()
1061    }
1062
1063    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1064        let info = UploadFileInfo::from_uploaded_file(file)?;
1065        let save_name = build_save_name(&info.tmp_name, &info.extension);
1066        self.file_info = Some(info);
1067        self.file_name = Some(save_name);
1068        self.is_internal = false;
1069        Ok(())
1070    }
1071
1072    fn set_upload_file_by_real(
1073        &mut self,
1074        file_path: &Path,
1075        extension: &str,
1076    ) -> Result<(), UploadError> {
1077        let info = UploadFileInfo::from_real_path(file_path, extension)?;
1078        let save_name = build_internal_save_name(file_path);
1079        self.file_info = Some(info);
1080        self.file_name = Some(save_name);
1081        self.is_internal = true;
1082        Ok(())
1083    }
1084
1085    fn is_internal(&self) -> bool {
1086        self.is_internal
1087    }
1088}
1089
1090// ============================================================================
1091// S3StorageEngine
1092// ============================================================================
1093
1094/// AWS S3 兼容存储引擎 — Rust 扩展(无 PHP 对应)
1095///
1096/// 复用 `sz-orm-storage::S3Storage`。
1097pub struct S3StorageEngine {
1098    /// 引擎配置
1099    config: EngineConfig,
1100    /// 文件信息
1101    file_info: Option<UploadFileInfo>,
1102    /// 保存文件名
1103    file_name: Option<String>,
1104    /// 错误信息
1105    error: Option<String>,
1106    /// 是否内部上传
1107    is_internal: bool,
1108}
1109
1110impl S3StorageEngine {
1111    /// 创建 S3 存储引擎实例
1112    pub fn new(config: EngineConfig) -> Self {
1113        Self {
1114            config,
1115            file_info: None,
1116            file_name: None,
1117            error: None,
1118            is_internal: false,
1119        }
1120    }
1121
1122    /// 创建底层存储
1123    fn create_storage(&self) -> sz_orm_storage::S3Storage {
1124        sz_orm_storage::S3Storage::new(self.config.bucket.clone(), self.config.region.clone())
1125    }
1126}
1127
1128#[async_trait]
1129impl StorageEngine for S3StorageEngine {
1130    #[tracing::instrument(skip(self))]
1131    async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1132        let file_name = self
1133            .file_name
1134            .as_ref()
1135            .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1136            .clone();
1137        let info = self
1138            .file_info
1139            .as_ref()
1140            .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1141            .clone();
1142
1143        let storage = self.create_storage();
1144        let data = tokio::fs::read(&info.tmp_name).await?;
1145        let content_type = mime_guess::from_path(&info.tmp_name)
1146            .first_or_octet_stream()
1147            .to_string();
1148
1149        match storage.put(&file_name, &data, &content_type).await {
1150            Ok(_) => Ok(Some(file_name)),
1151            Err(e) => {
1152                let msg = e.to_string();
1153                self.error = Some(msg.clone());
1154                Err(UploadError::UploadFailed(msg))
1155            }
1156        }
1157    }
1158
1159    #[tracing::instrument(skip(self))]
1160    async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1161        let storage = self.create_storage();
1162        match storage.delete(file_name).await {
1163            Ok(_) => Ok(true),
1164            Err(e) => {
1165                let msg = e.to_string();
1166                self.error = Some(msg);
1167                Ok(false)
1168            }
1169        }
1170    }
1171
1172    fn file_name(&self) -> Option<&str> {
1173        self.file_name.as_deref()
1174    }
1175
1176    fn file_info(&self) -> Option<&UploadFileInfo> {
1177        self.file_info.as_ref()
1178    }
1179
1180    fn error(&self) -> Option<&str> {
1181        self.error.as_deref()
1182    }
1183
1184    fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1185        let info = UploadFileInfo::from_uploaded_file(file)?;
1186        let save_name = build_save_name(&info.tmp_name, &info.extension);
1187        self.file_info = Some(info);
1188        self.file_name = Some(save_name);
1189        self.is_internal = false;
1190        Ok(())
1191    }
1192
1193    fn set_upload_file_by_real(
1194        &mut self,
1195        file_path: &Path,
1196        extension: &str,
1197    ) -> Result<(), UploadError> {
1198        let info = UploadFileInfo::from_real_path(file_path, extension)?;
1199        let save_name = build_internal_save_name(file_path);
1200        self.file_info = Some(info);
1201        self.file_name = Some(save_name);
1202        self.is_internal = true;
1203        Ok(())
1204    }
1205
1206    fn is_internal(&self) -> bool {
1207        self.is_internal
1208    }
1209}
1210
1211// ============================================================================
1212// StorageDriver — 工厂 + 代理
1213// ============================================================================
1214
1215/// 存储驱动 — 对齐 PHP `storage\Driver`
1216///
1217/// PHP 第 12-23 行:构造方法实例化引擎
1218/// PHP 第 108-116 行:`getEngineClass` 工厂方法
1219///
1220/// Rust 端使用枚举分发,避免动态分发开销。
1221pub enum StorageDriver {
1222    /// 本地存储引擎
1223    Local(LocalStorageEngine),
1224    /// 阿里云 OSS 引擎
1225    Aliyun(AliyunStorageEngine),
1226    /// 腾讯云 COS 引擎
1227    Qcloud(QcloudStorageEngine),
1228    /// 七牛云 Kodo 引擎
1229    Qiniu(QiniuStorageEngine),
1230    /// AWS S3 兼容引擎
1231    S3(S3StorageEngine),
1232}
1233
1234impl StorageDriver {
1235    /// 创建存储驱动 — 对齐 PHP `Driver::__construct($config, $storage = 'qcloud')`
1236    ///
1237    /// PHP 第 18-23 行:
1238    /// ```php
1239    /// public function __construct($config, $storage = 'qcloud') {
1240    ///     $this->config = $config;
1241    ///     $this->engine = $this->getEngineClass($storage);
1242    /// }
1243    /// ```
1244    pub fn new(kind: StorageEngineKind, config: EngineConfig) -> Self {
1245        match kind {
1246            StorageEngineKind::Local => Self::Local(LocalStorageEngine::new(config)),
1247            StorageEngineKind::Aliyun => Self::Aliyun(AliyunStorageEngine::new(config)),
1248            StorageEngineKind::Qcloud => Self::Qcloud(QcloudStorageEngine::new(config)),
1249            StorageEngineKind::Qiniu => Self::Qiniu(QiniuStorageEngine::new(config)),
1250            StorageEngineKind::S3 => Self::S3(S3StorageEngine::new(config)),
1251        }
1252    }
1253
1254    /// 执行上传 — 对齐 PHP `Driver::upload()`
1255    #[tracing::instrument(skip(self))]
1256    pub async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1257        match self {
1258            Self::Local(e) => e.upload().await,
1259            Self::Aliyun(e) => e.upload().await,
1260            Self::Qcloud(e) => e.upload().await,
1261            Self::Qiniu(e) => e.upload().await,
1262            Self::S3(e) => e.upload().await,
1263        }
1264    }
1265
1266    /// 执行删除 — 对齐 PHP `Driver::delete($fileName)`
1267    #[tracing::instrument(skip(self))]
1268    pub async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1269        match self {
1270            Self::Local(e) => e.delete(file_name).await,
1271            Self::Aliyun(e) => e.delete(file_name).await,
1272            Self::Qcloud(e) => e.delete(file_name).await,
1273            Self::Qiniu(e) => e.delete(file_name).await,
1274            Self::S3(e) => e.delete(file_name).await,
1275        }
1276    }
1277
1278    /// 获取文件名 — 对齐 PHP `Driver::getFileName()`
1279    pub fn file_name(&self) -> Option<&str> {
1280        match self {
1281            Self::Local(e) => e.file_name(),
1282            Self::Aliyun(e) => e.file_name(),
1283            Self::Qcloud(e) => e.file_name(),
1284            Self::Qiniu(e) => e.file_name(),
1285            Self::S3(e) => e.file_name(),
1286        }
1287    }
1288
1289    /// 获取文件信息 — 对齐 PHP `Driver::getFileInfo()`
1290    pub fn file_info(&self) -> Option<&UploadFileInfo> {
1291        match self {
1292            Self::Local(e) => e.file_info(),
1293            Self::Aliyun(e) => e.file_info(),
1294            Self::Qcloud(e) => e.file_info(),
1295            Self::Qiniu(e) => e.file_info(),
1296            Self::S3(e) => e.file_info(),
1297        }
1298    }
1299
1300    /// 获取错误信息 — 对齐 PHP `Driver::getError()`
1301    pub fn error(&self) -> Option<&str> {
1302        match self {
1303            Self::Local(e) => e.error(),
1304            Self::Aliyun(e) => e.error(),
1305            Self::Qcloud(e) => e.error(),
1306            Self::Qiniu(e) => e.error(),
1307            Self::S3(e) => e.error(),
1308        }
1309    }
1310
1311    /// 设置上传文件 — 对齐 PHP `Driver::setUploadFile($name)`
1312    pub fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1313        match self {
1314            Self::Local(e) => e.set_upload_file(file),
1315            Self::Aliyun(e) => e.set_upload_file(file),
1316            Self::Qcloud(e) => e.set_upload_file(file),
1317            Self::Qiniu(e) => e.set_upload_file(file),
1318            Self::S3(e) => e.set_upload_file(file),
1319        }
1320    }
1321
1322    /// 设置内部上传文件 — 对齐 PHP `Driver::setUploadFileByReal($filePath, $extension)`
1323    pub fn set_upload_file_by_real(
1324        &mut self,
1325        file_path: &Path,
1326        extension: &str,
1327    ) -> Result<(), UploadError> {
1328        match self {
1329            Self::Local(e) => e.set_upload_file_by_real(file_path, extension),
1330            Self::Aliyun(e) => e.set_upload_file_by_real(file_path, extension),
1331            Self::Qcloud(e) => e.set_upload_file_by_real(file_path, extension),
1332            Self::Qiniu(e) => e.set_upload_file_by_real(file_path, extension),
1333            Self::S3(e) => e.set_upload_file_by_real(file_path, extension),
1334        }
1335    }
1336
1337    /// 是否内部上传
1338    pub fn is_internal(&self) -> bool {
1339        match self {
1340            Self::Local(e) => e.is_internal(),
1341            Self::Aliyun(e) => e.is_internal(),
1342            Self::Qcloud(e) => e.is_internal(),
1343            Self::Qiniu(e) => e.is_internal(),
1344            Self::S3(e) => e.is_internal(),
1345        }
1346    }
1347
1348    /// 获取引擎类型
1349    pub fn kind(&self) -> StorageEngineKind {
1350        match self {
1351            Self::Local(_) => StorageEngineKind::Local,
1352            Self::Aliyun(_) => StorageEngineKind::Aliyun,
1353            Self::Qcloud(_) => StorageEngineKind::Qcloud,
1354            Self::Qiniu(_) => StorageEngineKind::Qiniu,
1355            Self::S3(_) => StorageEngineKind::S3,
1356        }
1357    }
1358}
1359
1360// ============================================================================
1361// 测试
1362// ============================================================================
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::*;
1367    use std::io::Write;
1368
1369    // ---------------------------------------------------------------------
1370    // 辅助函数
1371    // ---------------------------------------------------------------------
1372
1373    /// 创建临时文件
1374    fn create_temp_file(name: &str, content: &[u8]) -> std::path::PathBuf {
1375        let dir = std::env::temp_dir().join(format!(
1376            "sz_rust_storage_test_{}",
1377            std::time::SystemTime::now()
1378                .duration_since(std::time::UNIX_EPOCH)
1379                .unwrap_or_default()
1380                .as_nanos()
1381        ));
1382        std::fs::create_dir_all(&dir).unwrap();
1383        let path = dir.join(name);
1384        let mut f = std::fs::File::create(&path).unwrap();
1385        f.write_all(content).unwrap();
1386        path
1387    }
1388
1389    /// 创建临时目录作为 base_path
1390    fn create_temp_base_path() -> std::path::PathBuf {
1391        let dir = std::env::temp_dir().join(format!(
1392            "sz_rust_storage_base_{}",
1393            std::time::SystemTime::now()
1394                .duration_since(std::time::UNIX_EPOCH)
1395                .unwrap_or_default()
1396                .as_nanos()
1397        ));
1398        std::fs::create_dir_all(&dir).unwrap();
1399        dir
1400    }
1401
1402    // ---------------------------------------------------------------------
1403    // 组 1:StorageEngineKind 基础测试
1404    // ---------------------------------------------------------------------
1405
1406    #[test]
1407    fn test_storage_engine_kind_parse_local() {
1408        assert_eq!(
1409            StorageEngineKind::parse("local"),
1410            Some(StorageEngineKind::Local)
1411        );
1412    }
1413
1414    #[test]
1415    fn test_storage_engine_kind_parse_aliyun() {
1416        assert_eq!(
1417            StorageEngineKind::parse("aliyun"),
1418            Some(StorageEngineKind::Aliyun)
1419        );
1420    }
1421
1422    #[test]
1423    fn test_storage_engine_kind_parse_qcloud() {
1424        assert_eq!(
1425            StorageEngineKind::parse("qcloud"),
1426            Some(StorageEngineKind::Qcloud)
1427        );
1428    }
1429
1430    #[test]
1431    fn test_storage_engine_kind_parse_qiniu() {
1432        assert_eq!(
1433            StorageEngineKind::parse("qiniu"),
1434            Some(StorageEngineKind::Qiniu)
1435        );
1436    }
1437
1438    #[test]
1439    fn test_storage_engine_kind_parse_s3() {
1440        assert_eq!(StorageEngineKind::parse("s3"), Some(StorageEngineKind::S3));
1441    }
1442
1443    #[test]
1444    fn test_storage_engine_kind_parse_case_insensitive() {
1445        // 对齐 PHP ucfirst 行为(大小写不敏感)
1446        assert_eq!(
1447            StorageEngineKind::parse("LOCAL"),
1448            Some(StorageEngineKind::Local)
1449        );
1450        assert_eq!(
1451            StorageEngineKind::parse("Aliyun"),
1452            Some(StorageEngineKind::Aliyun)
1453        );
1454    }
1455
1456    #[test]
1457    fn test_storage_engine_kind_parse_invalid() {
1458        assert_eq!(StorageEngineKind::parse("invalid"), None);
1459        assert_eq!(StorageEngineKind::parse(""), None);
1460    }
1461
1462    #[test]
1463    fn test_storage_engine_kind_as_str() {
1464        assert_eq!(StorageEngineKind::Local.as_str(), "local");
1465        assert_eq!(StorageEngineKind::Aliyun.as_str(), "aliyun");
1466        assert_eq!(StorageEngineKind::Qcloud.as_str(), "qcloud");
1467        assert_eq!(StorageEngineKind::Qiniu.as_str(), "qiniu");
1468        assert_eq!(StorageEngineKind::S3.as_str(), "s3");
1469    }
1470
1471    // ---------------------------------------------------------------------
1472    // 组 2:EngineConfig builder 测试
1473    // ---------------------------------------------------------------------
1474
1475    #[test]
1476    fn test_engine_config_builder() {
1477        let config = EngineConfig::new()
1478            .with_bucket("my-bucket")
1479            .with_region("us-east-1")
1480            .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1481            .with_domain("https://cdn.example.com")
1482            .with_base_path("/var/www/uploads")
1483            .with_access_key_id("akid")
1484            .with_access_key_secret("aksecret")
1485            .with_secret_id("sid")
1486            .with_secret_key("sk")
1487            .with_access_key("ak");
1488        assert_eq!(config.bucket, "my-bucket");
1489        assert_eq!(config.region, "us-east-1");
1490        assert_eq!(config.endpoint, "oss-cn-hangzhou.aliyuncs.com");
1491        assert_eq!(config.domain, "https://cdn.example.com");
1492        assert_eq!(config.base_path, "/var/www/uploads");
1493        assert_eq!(config.access_key_id, "akid");
1494        assert_eq!(config.access_key_secret, "aksecret");
1495        assert_eq!(config.secret_id, "sid");
1496        assert_eq!(config.secret_key, "sk");
1497        assert_eq!(config.access_key, "ak");
1498    }
1499
1500    #[test]
1501    fn test_engine_config_default() {
1502        let config = EngineConfig::default();
1503        assert!(config.bucket.is_empty());
1504        assert!(config.region.is_empty());
1505        assert!(config.endpoint.is_empty());
1506        assert!(config.domain.is_empty());
1507        assert!(config.base_path.is_empty());
1508    }
1509
1510    // ---------------------------------------------------------------------
1511    // 组 3:build_save_name 测试
1512    // ---------------------------------------------------------------------
1513
1514    #[test]
1515    fn test_build_save_name_format() {
1516        let path = Path::new("/tmp/photo.jpg");
1517        let name = build_save_name(path, "jpg");
1518        // 格式:storage/{Ymd}/{YmdHis}{md5[0..5]}{rand padded 4}.jpg
1519        assert!(name.starts_with("storage/"), "name = {}", name);
1520        assert!(name.ends_with(".jpg"), "name = {}", name);
1521        // 验证日期部分(前 8 位是 Ymd)
1522        let parts: Vec<&str> = name.split('/').collect();
1523        assert_eq!(parts.len(), 3, "name = {}", name);
1524        let ymd_part = parts[1];
1525        assert_eq!(ymd_part.len(), 8, "Ymd should be 8 chars");
1526        assert!(ymd_part.chars().all(|c| c.is_ascii_digit()));
1527    }
1528
1529    #[test]
1530    fn test_build_save_name_md5_prefix_consistent() {
1531        // 相同路径 + 相同时间(秒级)生成的 md5 前缀应一致
1532        let path = Path::new("/tmp/test.png");
1533        let name1 = build_save_name(path, "png");
1534        let name2 = build_save_name(path, "png");
1535        // 提取 md5 前缀(去掉 storage/{Ymd}/{YmdHis} 前缀和随机部分)
1536        // 格式:storage/{Ymd}/{YmdHis}{md5[5]}{rand[4]}.ext
1537        // Ymd = 8, YmdHis = 14, md5 = 5, rand = 4
1538        let extract_md5 = |s: &str| -> String {
1539            let parts: Vec<&str> = s.split('/').collect();
1540            if parts.len() != 3 {
1541                return String::new();
1542            }
1543            let last = parts[2];
1544            // 去掉 .ext 后缀
1545            let last = last.rsplit_once('.').map(|(l, _)| l).unwrap_or(last);
1546            // 去掉前 14 位(YmdHis)
1547            if last.len() > 14 {
1548                last[14..19].to_string()
1549            } else {
1550                String::new()
1551            }
1552        };
1553        let md5_1 = extract_md5(&name1);
1554        let md5_2 = extract_md5(&name2);
1555        assert_eq!(
1556            md5_1, md5_2,
1557            "md5 prefix should be consistent for same path"
1558        );
1559        assert_eq!(md5_1.len(), 5, "md5 prefix should be 5 chars");
1560    }
1561
1562    #[test]
1563    fn test_build_save_name_empty_extension() {
1564        let path = Path::new("/tmp/noext");
1565        let name = build_save_name(path, "");
1566        // 扩展名为空时不加点
1567        assert!(!name.ends_with('.'), "name = {}", name);
1568        assert!(name.starts_with("storage/"));
1569    }
1570
1571    #[test]
1572    fn test_build_internal_save_name_format() {
1573        let path = Path::new("/tmp/photo.jpg");
1574        let name = build_internal_save_name(path);
1575        // 格式:storage/{Ymd}/{basename}
1576        assert!(name.starts_with("storage/"), "name = {}", name);
1577        assert!(name.ends_with("photo.jpg"), "name = {}", name);
1578        let parts: Vec<&str> = name.split('/').collect();
1579        assert_eq!(parts.len(), 3, "name = {}", name);
1580        // Ymd = 8 位数字
1581        assert_eq!(parts[1].len(), 8);
1582        assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
1583    }
1584
1585    // ---------------------------------------------------------------------
1586    // 组 4:UploadFileInfo 测试
1587    // ---------------------------------------------------------------------
1588
1589    #[test]
1590    fn test_upload_file_info_from_real_path() {
1591        let path = create_temp_file("test.txt", b"hello world");
1592        let info = UploadFileInfo::from_real_path(&path, "txt").unwrap();
1593        assert_eq!(info.name, "test.txt");
1594        assert_eq!(info.size, 11);
1595        assert_eq!(info.extension, "txt");
1596        assert_eq!(info.tmp_name, path);
1597        assert_eq!(info.error, 0);
1598        assert!(info.is_internal);
1599        std::fs::remove_file(&path).ok();
1600    }
1601
1602    #[test]
1603    fn test_upload_file_info_from_real_path_not_found() {
1604        let result = UploadFileInfo::from_real_path("/nonexistent/path.txt", "txt");
1605        assert!(result.is_err());
1606    }
1607
1608    // ---------------------------------------------------------------------
1609    // 组 5:LocalStorageEngine 测试
1610    // ---------------------------------------------------------------------
1611
1612    #[tokio::test]
1613    async fn test_local_storage_engine_new() {
1614        let config = EngineConfig::new().with_base_path("/tmp/test");
1615        let engine = LocalStorageEngine::new(config);
1616        assert!(!engine.is_internal());
1617        assert!(engine.file_name().is_none());
1618        assert!(engine.file_info().is_none());
1619        assert!(engine.error().is_none());
1620    }
1621
1622    #[tokio::test]
1623    async fn test_local_storage_upload_dir_default() {
1624        let config = EngineConfig::new();
1625        let engine = LocalStorageEngine::new(config);
1626        assert_eq!(engine.upload_dir(), PathBuf::from("uploads"));
1627    }
1628
1629    #[tokio::test]
1630    async fn test_local_storage_upload_dir_with_base_path() {
1631        let base = create_temp_base_path();
1632        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1633        let engine = LocalStorageEngine::new(config);
1634        let expected = base.join("uploads");
1635        assert_eq!(engine.upload_dir(), expected);
1636        std::fs::remove_dir_all(&base).ok();
1637    }
1638
1639    #[tokio::test]
1640    async fn test_local_storage_set_upload_file_by_real() {
1641        let path = create_temp_file("internal.txt", b"internal data");
1642        let base = create_temp_base_path();
1643        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1644        let mut engine = LocalStorageEngine::new(config);
1645
1646        engine
1647            .set_upload_file_by_real(&path, "txt")
1648            .expect("set_upload_file_by_real failed");
1649
1650        // 对齐 R5-17:内部上传文件名 = storage/{Ymd}/{basename}
1651        assert!(engine.is_internal());
1652        let file_name = engine.file_name().expect("file name should be set");
1653        assert!(file_name.starts_with("storage/"));
1654        assert!(file_name.ends_with("internal.txt"));
1655
1656        let info = engine.file_info().expect("file info should be set");
1657        assert_eq!(info.name, "internal.txt");
1658        assert!(info.is_internal);
1659
1660        std::fs::remove_file(&path).ok();
1661        std::fs::remove_dir_all(&base).ok();
1662    }
1663
1664    #[tokio::test]
1665    async fn test_local_storage_upload_internal_uses_rename() {
1666        let path = create_temp_file("rename_test.txt", b"rename me");
1667        let base = create_temp_base_path();
1668        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1669        let mut engine = LocalStorageEngine::new(config);
1670
1671        engine
1672            .set_upload_file_by_real(&path, "txt")
1673            .expect("set_upload_file_by_real failed");
1674        let file_name = engine.file_name().unwrap().to_string();
1675
1676        // 对齐 R5-19:内部上传用 rename
1677        let result = engine.upload().await;
1678        assert!(result.is_ok(), "upload should succeed");
1679        // 对齐 R5-22:内部上传返回 None
1680        assert!(result.unwrap().is_none());
1681
1682        // 原文件应被 rename 走(不存在)
1683        assert!(!path.exists(), "original file should be renamed away");
1684        // 目标文件应存在
1685        let target = engine.upload_dir().join(&file_name);
1686        assert!(target.exists(), "target file should exist: {:?}", target);
1687
1688        std::fs::remove_dir_all(&base).ok();
1689    }
1690
1691    #[tokio::test]
1692    async fn test_local_storage_upload_external_returns_save_name() {
1693        let path = create_temp_file("external.txt", b"external data");
1694        let base = create_temp_base_path();
1695        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1696        let mut engine = LocalStorageEngine::new(config);
1697
1698        // 对齐 R5-23:外部上传返回 saveName
1699        let file = UploadedFile::new(&path, "external.txt", Some("text/plain"), Some(0), true)
1700            .expect("UploadedFile::new failed");
1701        engine
1702            .set_upload_file(&file)
1703            .expect("set_upload_file failed");
1704
1705        let result = engine.upload().await;
1706        assert!(result.is_ok(), "upload should succeed");
1707        let save_name = result
1708            .unwrap()
1709            .expect("external upload should return save name");
1710        assert!(save_name.starts_with("storage/"));
1711
1712        // 源文件应保留(外部上传用 copy)
1713        assert!(path.exists(), "source file should be preserved");
1714
1715        // 目标文件应存在
1716        let target = engine.upload_dir().join(&save_name);
1717        assert!(target.exists(), "target file should exist: {:?}", target);
1718
1719        std::fs::remove_file(&path).ok();
1720        std::fs::remove_dir_all(&base).ok();
1721    }
1722
1723    #[tokio::test]
1724    async fn test_local_storage_delete_existing_file() {
1725        let base = create_temp_base_path();
1726        let uploads_dir = base.join("uploads").join("storage").join("20260101");
1727        std::fs::create_dir_all(&uploads_dir).unwrap();
1728        let file_path = uploads_dir.join("delete_me.txt");
1729        std::fs::write(&file_path, b"delete me").unwrap();
1730
1731        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1732        let mut engine = LocalStorageEngine::new(config);
1733
1734        // 对齐 R5-20:delete Elvis 短路
1735        let result = engine.delete("storage/20260101/delete_me.txt").await;
1736        assert!(result.is_ok());
1737        assert!(result.unwrap(), "delete should return true");
1738        assert!(!file_path.exists(), "file should be deleted");
1739
1740        std::fs::remove_dir_all(&base).ok();
1741    }
1742
1743    #[tokio::test]
1744    async fn test_local_storage_delete_nonexistent_file() {
1745        let base = create_temp_base_path();
1746        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1747        let mut engine = LocalStorageEngine::new(config);
1748
1749        // 对齐 R5-20:!file_exists($filePath) 为 true 时短路返回 true
1750        let result = engine.delete("nonexistent.txt").await;
1751        assert!(result.is_ok());
1752        assert!(result.unwrap(), "delete nonexistent should return true");
1753
1754        std::fs::remove_dir_all(&base).ok();
1755    }
1756
1757    // ---------------------------------------------------------------------
1758    // 组 6:云存储引擎构造测试
1759    // ---------------------------------------------------------------------
1760
1761    #[test]
1762    fn test_aliyun_storage_engine_new() {
1763        let config = EngineConfig::new()
1764            .with_bucket("aliyun-bucket")
1765            .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1766            .with_access_key_id("akid")
1767            .with_access_key_secret("aksecret");
1768        let engine = AliyunStorageEngine::new(config);
1769        assert!(!engine.is_internal());
1770        assert!(engine.file_name().is_none());
1771    }
1772
1773    #[test]
1774    fn test_qcloud_storage_engine_new() {
1775        let config = EngineConfig::new()
1776            .with_bucket("cos-bucket")
1777            .with_region("ap-guangzhou")
1778            .with_secret_id("sid")
1779            .with_secret_key("sk");
1780        let engine = QcloudStorageEngine::new(config);
1781        assert!(!engine.is_internal());
1782    }
1783
1784    #[test]
1785    fn test_qiniu_storage_engine_new() {
1786        let config = EngineConfig::new()
1787            .with_bucket("qiniu-bucket")
1788            .with_access_key("ak")
1789            .with_secret_key("sk");
1790        let engine = QiniuStorageEngine::new(config);
1791        assert!(!engine.is_internal());
1792    }
1793
1794    #[test]
1795    fn test_s3_storage_engine_new() {
1796        let config = EngineConfig::new()
1797            .with_bucket("s3-bucket")
1798            .with_region("us-east-1");
1799        let engine = S3StorageEngine::new(config);
1800        assert!(!engine.is_internal());
1801    }
1802
1803    // ---------------------------------------------------------------------
1804    // 组 7:云存储引擎 set_upload_file_by_real 测试
1805    // ---------------------------------------------------------------------
1806
1807    #[test]
1808    fn test_aliyun_set_upload_file_by_real() {
1809        let path = create_temp_file("aliyun.txt", b"aliyun");
1810        let config = EngineConfig::new()
1811            .with_bucket("bucket")
1812            .with_endpoint("endpoint");
1813        let mut engine = AliyunStorageEngine::new(config);
1814        engine.set_upload_file_by_real(&path, "txt").unwrap();
1815        assert!(engine.is_internal());
1816        assert!(engine.file_name().unwrap().starts_with("storage/"));
1817        std::fs::remove_file(&path).ok();
1818    }
1819
1820    #[test]
1821    fn test_qcloud_set_upload_file_by_real() {
1822        let path = create_temp_file("qcloud.txt", b"qcloud");
1823        let config = EngineConfig::new()
1824            .with_bucket("bucket")
1825            .with_region("region");
1826        let mut engine = QcloudStorageEngine::new(config);
1827        engine.set_upload_file_by_real(&path, "txt").unwrap();
1828        assert!(engine.is_internal());
1829        std::fs::remove_file(&path).ok();
1830    }
1831
1832    #[test]
1833    fn test_qiniu_set_upload_file_by_real() {
1834        let path = create_temp_file("qiniu.txt", b"qiniu");
1835        let config = EngineConfig::new().with_bucket("bucket");
1836        let mut engine = QiniuStorageEngine::new(config);
1837        engine.set_upload_file_by_real(&path, "txt").unwrap();
1838        assert!(engine.is_internal());
1839        std::fs::remove_file(&path).ok();
1840    }
1841
1842    #[test]
1843    fn test_s3_set_upload_file_by_real() {
1844        let path = create_temp_file("s3.txt", b"s3");
1845        let config = EngineConfig::new()
1846            .with_bucket("bucket")
1847            .with_region("region");
1848        let mut engine = S3StorageEngine::new(config);
1849        engine.set_upload_file_by_real(&path, "txt").unwrap();
1850        assert!(engine.is_internal());
1851        std::fs::remove_file(&path).ok();
1852    }
1853
1854    // ---------------------------------------------------------------------
1855    // 组 8:StorageDriver 工厂测试
1856    // ---------------------------------------------------------------------
1857
1858    #[test]
1859    fn test_storage_driver_new_local() {
1860        let config = EngineConfig::new();
1861        let driver = StorageDriver::new(StorageEngineKind::Local, config);
1862        assert_eq!(driver.kind(), StorageEngineKind::Local);
1863        assert!(!driver.is_internal());
1864    }
1865
1866    #[test]
1867    fn test_storage_driver_new_aliyun() {
1868        let config = EngineConfig::new();
1869        let driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1870        assert_eq!(driver.kind(), StorageEngineKind::Aliyun);
1871    }
1872
1873    #[test]
1874    fn test_storage_driver_new_qcloud() {
1875        let config = EngineConfig::new();
1876        let driver = StorageDriver::new(StorageEngineKind::Qcloud, config);
1877        assert_eq!(driver.kind(), StorageEngineKind::Qcloud);
1878    }
1879
1880    #[test]
1881    fn test_storage_driver_new_qiniu() {
1882        let config = EngineConfig::new();
1883        let driver = StorageDriver::new(StorageEngineKind::Qiniu, config);
1884        assert_eq!(driver.kind(), StorageEngineKind::Qiniu);
1885    }
1886
1887    #[test]
1888    fn test_storage_driver_new_s3() {
1889        let config = EngineConfig::new();
1890        let driver = StorageDriver::new(StorageEngineKind::S3, config);
1891        assert_eq!(driver.kind(), StorageEngineKind::S3);
1892    }
1893
1894    // ---------------------------------------------------------------------
1895    // 组 9:StorageDriver 代理方法测试
1896    // ---------------------------------------------------------------------
1897
1898    #[tokio::test]
1899    async fn test_storage_driver_local_upload_and_delete() {
1900        let path = create_temp_file("driver.txt", b"driver data");
1901        let base = create_temp_base_path();
1902        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1903        let mut driver = StorageDriver::new(StorageEngineKind::Local, config);
1904
1905        driver
1906            .set_upload_file_by_real(&path, "txt")
1907            .expect("set_upload_file_by_real failed");
1908        let file_name = driver.file_name().unwrap().to_string();
1909
1910        let result = driver.upload().await;
1911        assert!(result.is_ok());
1912        assert!(result.unwrap().is_none());
1913
1914        // 删除刚上传的文件
1915        let del_result = driver.delete(&file_name).await;
1916        assert!(del_result.is_ok());
1917        assert!(del_result.unwrap());
1918
1919        std::fs::remove_file(&path).ok();
1920        std::fs::remove_dir_all(&base).ok();
1921    }
1922
1923    #[tokio::test]
1924    async fn test_storage_driver_aliyun_upload() {
1925        let path = create_temp_file("aliyun_driver.txt", b"aliyun data");
1926        let config = EngineConfig::new()
1927            .with_bucket("test-bucket")
1928            .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
1929        let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1930
1931        driver
1932            .set_upload_file_by_real(&path, "txt")
1933            .expect("set_upload_file_by_real failed");
1934        let result = driver.upload().await;
1935        assert!(result.is_ok(), "upload should succeed");
1936        // 对齐 R5-22:云存储返回 Some(save_name)
1937        assert!(result.unwrap().is_some());
1938
1939        std::fs::remove_file(&path).ok();
1940    }
1941
1942    #[tokio::test]
1943    async fn test_storage_driver_error_after_failed_delete() {
1944        // 测试云存储删除(内存模拟,删除不存在的 key 不会报错)
1945        let config = EngineConfig::new()
1946            .with_bucket("bucket")
1947            .with_endpoint("endpoint");
1948        let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1949        let result = driver.delete("nonexistent.txt").await;
1950        assert!(result.is_ok());
1951    }
1952
1953    // ---------------------------------------------------------------------
1954    // 组 10:PHP 行为对齐 R5 测试
1955    // ---------------------------------------------------------------------
1956
1957    /// R5-16:`buildSaveName` 格式 = `storage/{Ymd}/{YmdHis}{md5(realPath)[0..5]}{rand(0..9999) padded 4}.{ext}`
1958    #[test]
1959    fn test_r5_16_build_save_name_format() {
1960        let path = Path::new("/tmp/r5_16_test.jpg");
1961        let name = build_save_name(path, "jpg");
1962
1963        // 验证格式:storage/{Ymd}/{YmdHis}{md5[5]}{rand[4]}.jpg
1964        assert!(name.starts_with("storage/"), "name = {}", name);
1965        assert!(name.ends_with(".jpg"), "name = {}", name);
1966
1967        let parts: Vec<&str> = name.split('/').collect();
1968        assert_eq!(parts.len(), 3, "should have 3 parts: storage, Ymd, rest");
1969
1970        // Ymd = 8 位数字
1971        let ymd = parts[1];
1972        assert_eq!(ymd.len(), 8, "Ymd should be 8 digits");
1973        assert!(ymd.chars().all(|c| c.is_ascii_digit()));
1974
1975        // 最后部分:YmdHis(14) + md5(5) + rand(4) + ".jpg"(4) = 27
1976        let last = parts[2];
1977        assert_eq!(
1978            last.len(),
1979            27,
1980            "last part length = {}, last = {}",
1981            last.len(),
1982            last
1983        );
1984    }
1985
1986    /// R5-17:`setUploadFileByReal` 的 `fileName` = `storage/{Ymd}/{basename}`
1987    #[test]
1988    fn test_r5_17_internal_save_name_uses_basename() {
1989        let path = Path::new("/tmp/r5_17_test.png");
1990        let name = build_internal_save_name(path);
1991
1992        // 格式:storage/{Ymd}/{basename}
1993        assert!(name.starts_with("storage/"));
1994        assert!(name.ends_with("r5_17_test.png"));
1995
1996        let parts: Vec<&str> = name.split('/').collect();
1997        assert_eq!(parts.len(), 3, "should have 3 parts");
1998        // Ymd = 8 位数字
1999        assert_eq!(parts[1].len(), 8);
2000        assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
2001        // basename 保持不变
2002        assert_eq!(parts[2], "r5_17_test.png");
2003    }
2004
2005    /// R5-18:`getRealPath` 区分 isInternal(内部用 tmp_name,外部用 UploadedFile.path)
2006    #[tokio::test]
2007    async fn test_r5_18_real_path_internal_vs_external() {
2008        let internal_path = create_temp_file("internal.txt", b"internal");
2009        let external_path = create_temp_file("external.txt", b"external");
2010        let base = create_temp_base_path();
2011        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2012
2013        // 内部上传:real_path 应为 file_info.tmp_name(即传入的 file_path)
2014        let mut engine = LocalStorageEngine::new(config.clone());
2015        engine
2016            .set_upload_file_by_real(&internal_path, "txt")
2017            .unwrap();
2018        assert!(engine.is_internal());
2019        let real_path = engine.real_path().expect("real_path should be set");
2020        assert_eq!(real_path, internal_path);
2021
2022        // 外部上传:real_path 应为 UploadedFile.path
2023        let mut engine2 = LocalStorageEngine::new(config);
2024        let file = UploadedFile::new(
2025            &external_path,
2026            "external.txt",
2027            Some("text/plain"),
2028            Some(0),
2029            true,
2030        )
2031        .expect("UploadedFile::new failed");
2032        engine2.set_upload_file(&file).unwrap();
2033        assert!(!engine2.is_internal());
2034        let real_path = engine2.real_path().expect("real_path should be set");
2035        assert_eq!(real_path, external_path);
2036
2037        std::fs::remove_file(&internal_path).ok();
2038        std::fs::remove_file(&external_path).ok();
2039        std::fs::remove_dir_all(&base).ok();
2040    }
2041
2042    /// R5-19:`Local::upload` 区分 isInternal(internal 用 rename,external 用 copy)
2043    #[tokio::test]
2044    async fn test_r5_19_upload_internal_rename_external_copy() {
2045        let internal_path = create_temp_file("r5_19_internal.txt", b"internal");
2046        let external_path = create_temp_file("r5_19_external.txt", b"external");
2047        let base = create_temp_base_path();
2048        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2049
2050        // 内部上传:用 rename,原文件不存在
2051        let mut engine = LocalStorageEngine::new(config.clone());
2052        engine
2053            .set_upload_file_by_real(&internal_path, "txt")
2054            .unwrap();
2055        engine.upload().await.expect("internal upload failed");
2056        assert!(!internal_path.exists(), "rename should move file away");
2057
2058        // 外部上传:用 copy,原文件保留
2059        let mut engine2 = LocalStorageEngine::new(config);
2060        let file = UploadedFile::new(
2061            &external_path,
2062            "r5_19_external.txt",
2063            Some("text/plain"),
2064            Some(0),
2065            true,
2066        )
2067        .expect("UploadedFile::new failed");
2068        engine2.set_upload_file(&file).unwrap();
2069        engine2.upload().await.expect("external upload failed");
2070        assert!(external_path.exists(), "copy should preserve source");
2071
2072        std::fs::remove_file(&internal_path).ok();
2073        std::fs::remove_file(&external_path).ok();
2074        std::fs::remove_dir_all(&base).ok();
2075    }
2076
2077    /// R5-20:`Local::delete` Elvis 短路 `!file_exists($filePath) ?: unlink($filePath)`
2078    #[tokio::test]
2079    async fn test_r5_20_delete_elvis_short_circuit() {
2080        let base = create_temp_base_path();
2081        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2082        let mut engine = LocalStorageEngine::new(config);
2083
2084        // 文件不存在时:!file_exists 为 true,短路返回 true(不调用 unlink)
2085        let result = engine.delete("nonexistent_file.txt").await;
2086        assert!(result.is_ok());
2087        assert!(result.unwrap(), "delete nonexistent should return true");
2088
2089        // 文件存在时:!file_exists 为 false,调用 unlink 返回结果
2090        let uploads_dir = base.join("uploads").join("storage").join("20260101");
2091        std::fs::create_dir_all(&uploads_dir).unwrap();
2092        let file_path = uploads_dir.join("exists.txt");
2093        std::fs::write(&file_path, b"exists").unwrap();
2094
2095        let result = engine.delete("storage/20260101/exists.txt").await;
2096        assert!(result.is_ok());
2097        assert!(result.unwrap(), "delete existing should return true");
2098        assert!(!file_path.exists(), "file should be deleted");
2099
2100        std::fs::remove_dir_all(&base).ok();
2101    }
2102
2103    /// R5-21:`Local::uploadByInternal` 失败设置 `error='upload write error'`
2104    #[tokio::test]
2105    async fn test_r5_21_internal_upload_failure_sets_error() {
2106        // 创建一个不存在的源文件路径,触发 rename 失败
2107        let nonexistent_path = std::env::temp_dir().join(format!(
2108            "nonexistent_{}.txt",
2109            std::time::SystemTime::now()
2110                .duration_since(std::time::UNIX_EPOCH)
2111                .unwrap_or_default()
2112                .as_nanos()
2113        ));
2114        // 注意:不创建文件,直接调用 set_upload_file_by_real 会失败
2115        // 所以我们创建文件然后立即删除,模拟 rename 失败
2116        let path = create_temp_file("will_fail.txt", b"will fail");
2117        let base = create_temp_base_path();
2118        // 设置一个只读的 base_path 使 rename 失败(Windows 上可能不生效,改为其他方式)
2119        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2120        let mut engine = LocalStorageEngine::new(config);
2121
2122        engine
2123            .set_upload_file_by_real(&path, "txt")
2124            .expect("set_upload_file_by_real failed");
2125
2126        // 删除源文件,使 rename 失败
2127        std::fs::remove_file(&path).unwrap();
2128
2129        let result = engine.upload().await;
2130        // rename 应该失败(源文件不存在)
2131        assert!(result.is_err(), "upload should fail when source missing");
2132        // 错误信息应包含 'upload write error'(对齐 PHP)
2133        let error_msg = engine.error().unwrap_or("");
2134        assert!(
2135            error_msg.contains("upload write error"),
2136            "error should contain 'upload write error', got: {}",
2137            error_msg
2138        );
2139
2140        let _ = nonexistent_path;
2141        std::fs::remove_dir_all(&base).ok();
2142    }
2143
2144    /// R5-22:云存储 `upload` 成功返回 `true`(PHP),Rust 端返回 `Ok(Some(save_name))`
2145    #[tokio::test]
2146    async fn test_r5_22_cloud_upload_returns_save_name() {
2147        let path = create_temp_file("r5_22.txt", b"cloud upload");
2148        let config = EngineConfig::new()
2149            .with_bucket("r5_22_bucket")
2150            .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
2151        let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
2152
2153        driver
2154            .set_upload_file_by_real(&path, "txt")
2155            .expect("set_upload_file_by_real failed");
2156        let file_name = driver.file_name().unwrap().to_string();
2157
2158        let result = driver.upload().await;
2159        assert!(result.is_ok(), "upload should succeed");
2160        let returned = result.unwrap().expect("cloud upload should return Some");
2161        assert_eq!(returned, file_name, "should return save name");
2162
2163        std::fs::remove_file(&path).ok();
2164    }
2165
2166    /// R5-23:`Local::uploadByExternal` 返回 `saveName`(putFile 返回路径)
2167    #[tokio::test]
2168    async fn test_r5_23_local_external_upload_returns_save_name() {
2169        let path = create_temp_file("r5_23.txt", b"external save name");
2170        let base = create_temp_base_path();
2171        let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2172        let mut engine = LocalStorageEngine::new(config);
2173
2174        let file = UploadedFile::new(&path, "r5_23.txt", Some("text/plain"), Some(0), true)
2175            .expect("UploadedFile::new failed");
2176        engine.set_upload_file(&file).unwrap();
2177        let expected_name = engine.file_name().unwrap().to_string();
2178
2179        let result = engine.upload().await;
2180        assert!(result.is_ok(), "upload should succeed");
2181        let save_name = result
2182            .unwrap()
2183            .expect("external upload should return save name");
2184        assert_eq!(save_name, expected_name, "should return save name");
2185
2186        std::fs::remove_file(&path).ok();
2187        std::fs::remove_dir_all(&base).ok();
2188    }
2189}