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