1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum StorageEngineKind {
99 Local,
101 Aliyun,
103 Qcloud,
105 Qiniu,
107 S3,
109}
110
111impl StorageEngineKind {
112 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 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#[derive(Clone, Default)]
160pub struct EngineConfig {
161 pub bucket: String,
163 pub region: String,
165 pub endpoint: String,
167 pub domain: String,
169 pub access_key_id: String,
171 pub access_key_secret: String,
173 pub secret_id: String,
175 pub secret_key: String,
177 pub access_key: String,
179 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 pub fn new() -> Self {
203 Self::default()
204 }
205
206 pub fn with_bucket(mut self, bucket: impl Into<String>) -> Self {
208 self.bucket = bucket.into();
209 self
210 }
211
212 pub fn with_region(mut self, region: impl Into<String>) -> Self {
214 self.region = region.into();
215 self
216 }
217
218 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
220 self.endpoint = endpoint.into();
221 self
222 }
223
224 pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
226 self.domain = domain.into();
227 self
228 }
229
230 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 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 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 pub fn with_secret_id(mut self, key: impl Into<String>) -> Self {
250 self.secret_id = key.into();
251 self
252 }
253
254 pub fn with_secret_key(mut self, key: impl Into<String>) -> Self {
256 self.secret_key = key.into();
257 self
258 }
259
260 pub fn with_access_key(mut self, key: impl Into<String>) -> Self {
262 self.access_key = key.into();
263 self
264 }
265}
266
267#[derive(Debug, Clone)]
289pub struct UploadFileInfo {
290 pub name: String,
292 pub size: u64,
294 pub extension: String,
296 pub tmp_name: PathBuf,
298 pub error: i32,
300 pub is_internal: bool,
302}
303
304impl UploadFileInfo {
305 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 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
358pub 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 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 let rand_num: u32 = rand::thread_rng().gen_range(0..=9999);
390 let rand_padded = format!("{:04}", rand_num);
391
392 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
404pub 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#[async_trait]
433pub trait StorageEngine: Send + Sync {
434 async fn upload(&mut self) -> Result<Option<String>, UploadError>;
441
442 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError>;
449
450 fn file_name(&self) -> Option<&str>;
452
453 fn file_info(&self) -> Option<&UploadFileInfo>;
455
456 fn error(&self) -> Option<&str>;
458
459 async fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError>;
464
465 async fn set_upload_file_by_real(
469 &mut self,
470 file_path: &Path,
471 extension: &str,
472 ) -> Result<(), UploadError>;
473
474 fn is_internal(&self) -> bool;
476
477 fn real_path(&self) -> Option<&Path> {
483 self.file_info().map(|info| info.tmp_name.as_path())
484 }
485}
486
487pub struct LocalStorageEngine {
498 config: EngineConfig,
500 file_info: Option<UploadFileInfo>,
502 file_name: Option<String>,
504 error: Option<String>,
506 is_internal: bool,
508 upload_source_path: Option<PathBuf>,
510}
511
512impl LocalStorageEngine {
513 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 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 #[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 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 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 #[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 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 if self.is_internal {
631 self.upload_by_internal().await?;
633 Ok(None)
634 } else {
635 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 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 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 return Ok(true);
668 }
669 match tokio::fs::remove_file(&file_path).await {
671 Ok(_) => Ok(true),
672 Err(e) => {
673 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 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 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
727pub struct AliyunStorageEngine {
739 config: EngineConfig,
741 file_info: Option<UploadFileInfo>,
743 file_name: Option<String>,
745 error: Option<String>,
747 is_internal: bool,
749}
750
751impl AliyunStorageEngine {
752 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 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 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 Ok(Some(file_name))
798 }
799 Err(e) => {
800 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 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
861pub struct QcloudStorageEngine {
873 config: EngineConfig,
875 file_info: Option<UploadFileInfo>,
877 file_name: Option<String>,
879 error: Option<String>,
881 is_internal: bool,
883}
884
885impl QcloudStorageEngine {
886 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 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 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
990pub struct QiniuStorageEngine {
1002 config: EngineConfig,
1004 file_info: Option<UploadFileInfo>,
1006 file_name: Option<String>,
1008 error: Option<String>,
1010 is_internal: bool,
1012}
1013
1014impl QiniuStorageEngine {
1015 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 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 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
1116pub struct S3StorageEngine {
1124 config: EngineConfig,
1126 file_info: Option<UploadFileInfo>,
1128 file_name: Option<String>,
1130 error: Option<String>,
1132 is_internal: bool,
1134}
1135
1136impl S3StorageEngine {
1137 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 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
1237pub enum StorageDriver {
1248 Local(LocalStorageEngine),
1250 Aliyun(AliyunStorageEngine),
1252 Qcloud(QcloudStorageEngine),
1254 Qiniu(QiniuStorageEngine),
1256 S3(S3StorageEngine),
1258}
1259
1260impl StorageDriver {
1261 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 #[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 #[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 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 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 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 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 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 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 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#[cfg(test)]
1391mod tests {
1392 use super::*;
1393 use std::io::Write;
1394
1395 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 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 #[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 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 #[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 #[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 assert!(name.starts_with("storage/"), "name = {}", name);
1546 assert!(name.ends_with(".jpg"), "name = {}", name);
1547 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 let path = Path::new("/tmp/test.png");
1559 let name1 = build_save_name(path, "png");
1560 let name2 = build_save_name(path, "png");
1561 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 let last = last.rsplit_once('.').map(|(l, _)| l).unwrap_or(last);
1572 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 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 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 assert_eq!(parts[1].len(), 8);
1608 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
1609 }
1610
1611 #[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 #[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 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 let result = engine.upload().await;
1706 assert!(result.is_ok(), "upload should succeed");
1707 assert!(result.unwrap().is_none());
1709
1710 assert!(!path.exists(), "original file should be renamed away");
1712 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 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 assert!(path.exists(), "source file should be preserved");
1743
1744 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 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 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 #[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 #[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 #[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 #[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 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 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 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 #[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 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 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 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 #[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 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 assert_eq!(parts[1].len(), 8);
2031 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
2032 assert_eq!(parts[2], "r5_17_test.png");
2034 }
2035
2036 #[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 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 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 #[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 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 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 #[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 let result = engine.delete("nonexistent_file.txt").await;
2119 assert!(result.is_ok());
2120 assert!(result.unwrap(), "delete nonexistent should return true");
2121
2122 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 #[tokio::test]
2138 async fn test_r5_21_internal_upload_failure_sets_error() {
2139 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 let path = create_temp_file("will_fail.txt", b"will fail");
2150 let base = create_temp_base_path();
2151 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 std::fs::remove_file(&path).unwrap();
2162
2163 let result = engine.upload().await;
2164 assert!(result.is_err(), "upload should fail when source missing");
2166 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 #[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 #[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}