Skip to main content

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