1use std::path::{Path, PathBuf};
64
65use async_trait::async_trait;
66use chrono::Local;
67use md5::{Digest, Md5};
68use rand::Rng;
69use sz_orm_storage::Storage;
70
71use super::{UploadError, UploadedFile};
72
73#[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(Debug, 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 EngineConfig {
184 pub fn new() -> Self {
186 Self::default()
187 }
188
189 pub fn with_bucket(mut self, bucket: impl Into<String>) -> Self {
191 self.bucket = bucket.into();
192 self
193 }
194
195 pub fn with_region(mut self, region: impl Into<String>) -> Self {
197 self.region = region.into();
198 self
199 }
200
201 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
203 self.endpoint = endpoint.into();
204 self
205 }
206
207 pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
209 self.domain = domain.into();
210 self
211 }
212
213 pub fn with_base_path(mut self, base_path: impl Into<String>) -> Self {
215 self.base_path = base_path.into();
216 self
217 }
218
219 pub fn with_access_key_id(mut self, key: impl Into<String>) -> Self {
221 self.access_key_id = key.into();
222 self
223 }
224
225 pub fn with_access_key_secret(mut self, key: impl Into<String>) -> Self {
227 self.access_key_secret = key.into();
228 self
229 }
230
231 pub fn with_secret_id(mut self, key: impl Into<String>) -> Self {
233 self.secret_id = key.into();
234 self
235 }
236
237 pub fn with_secret_key(mut self, key: impl Into<String>) -> Self {
239 self.secret_key = key.into();
240 self
241 }
242
243 pub fn with_access_key(mut self, key: impl Into<String>) -> Self {
245 self.access_key = key.into();
246 self
247 }
248}
249
250#[derive(Debug, Clone)]
272pub struct UploadFileInfo {
273 pub name: String,
275 pub size: u64,
277 pub extension: String,
279 pub tmp_name: PathBuf,
281 pub error: i32,
283 pub is_internal: bool,
285}
286
287impl UploadFileInfo {
288 pub fn from_uploaded_file(file: &UploadedFile) -> Result<Self, UploadError> {
294 let path = file.as_file().path();
295 let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
296 Ok(Self {
297 name: file.original_name().to_string(),
298 size,
299 extension: file.original_extension(),
300 tmp_name: path.to_path_buf(),
301 error: file.error_code() as i32,
302 is_internal: false,
303 })
304 }
305
306 pub fn from_real_path<P: AsRef<Path>>(path: P, extension: &str) -> Result<Self, UploadError> {
310 let path = path.as_ref();
311 if !path.exists() {
312 return Err(UploadError::FileNotFound(
313 path.to_string_lossy().to_string(),
314 ));
315 }
316 let name = path
317 .file_name()
318 .map(|n| n.to_string_lossy().to_string())
319 .unwrap_or_default();
320 let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
321 Ok(Self {
322 name,
323 size,
324 extension: extension.to_string(),
325 tmp_name: path.to_path_buf(),
326 error: 0,
327 is_internal: true,
328 })
329 }
330}
331
332pub fn build_save_name(real_path: &Path, extension: &str) -> String {
352 let now = Local::now();
353 let ymd = now.format("%Y%m%d").to_string();
354 let ymd_his = now.format("%Y%m%d%H%M%S").to_string();
355
356 let mut md5 = Md5::new();
358 md5.update(real_path.to_string_lossy().as_bytes());
359 let md5_hex = hex::encode(md5.finalize());
360 let md5_prefix = &md5_hex[..5];
361
362 let rand_num: u32 = rand::thread_rng().gen_range(0..=9999);
364 let rand_padded = format!("{:04}", rand_num);
365
366 if extension.is_empty() {
369 format!("storage/{}/{}{}{}", ymd, ymd_his, md5_prefix, rand_padded)
370 } else {
371 format!(
372 "storage/{}/{}{}{}.{}",
373 ymd, ymd_his, md5_prefix, rand_padded, extension
374 )
375 }
376}
377
378pub fn build_internal_save_name(file_path: &Path) -> String {
387 let now = Local::now();
388 let ymd = now.format("%Y%m%d").to_string();
389 let basename = file_path
390 .file_name()
391 .map(|n| n.to_string_lossy().to_string())
392 .unwrap_or_default();
393 format!("storage/{}/{}", ymd, basename)
394}
395
396#[async_trait]
407pub trait StorageEngine: Send + Sync {
408 async fn upload(&mut self) -> Result<Option<String>, UploadError>;
415
416 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError>;
423
424 fn file_name(&self) -> Option<&str>;
426
427 fn file_info(&self) -> Option<&UploadFileInfo>;
429
430 fn error(&self) -> Option<&str>;
432
433 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError>;
438
439 fn set_upload_file_by_real(
443 &mut self,
444 file_path: &Path,
445 extension: &str,
446 ) -> Result<(), UploadError>;
447
448 fn is_internal(&self) -> bool;
450
451 fn real_path(&self) -> Option<&Path> {
457 self.file_info().map(|info| info.tmp_name.as_path())
458 }
459}
460
461pub struct LocalStorageEngine {
472 config: EngineConfig,
474 file_info: Option<UploadFileInfo>,
476 file_name: Option<String>,
478 error: Option<String>,
480 is_internal: bool,
482 upload_source_path: Option<PathBuf>,
484}
485
486impl LocalStorageEngine {
487 pub fn new(config: EngineConfig) -> Self {
489 Self {
490 config,
491 file_info: None,
492 file_name: None,
493 error: None,
494 is_internal: false,
495 upload_source_path: None,
496 }
497 }
498
499 pub fn upload_dir(&self) -> PathBuf {
504 if self.config.base_path.is_empty() {
505 PathBuf::from("uploads")
506 } else {
507 PathBuf::from(&self.config.base_path).join("uploads")
508 }
509 }
510
511 #[tracing::instrument(skip(self))]
526 async fn upload_by_internal(&mut self) -> Result<bool, UploadError> {
527 let target = self.upload_dir().join(
528 self.file_name
529 .as_ref()
530 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
531 .as_str(),
532 );
533 let real_path = self
534 .file_info
535 .as_ref()
536 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
537 .tmp_name
538 .clone();
539
540 if let Some(parent) = target.parent() {
542 tokio::fs::create_dir_all(parent).await?;
543 }
544 match tokio::fs::rename(&real_path, &target).await {
545 Ok(_) => Ok(true),
546 Err(e) => {
547 self.error = Some("upload write error".to_string());
549 Err(UploadError::MoveFailed {
550 from: real_path.to_string_lossy().to_string(),
551 to: target.to_string_lossy().to_string(),
552 error: e.to_string(),
553 })
554 }
555 }
556 }
557
558 #[tracing::instrument(skip(self))]
575 async fn upload_by_external(&mut self) -> Result<String, UploadError> {
576 let file_name = self
577 .file_name
578 .as_ref()
579 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
580 .clone();
581 let source = self
582 .upload_source_path
583 .as_ref()
584 .or_else(|| self.file_info.as_ref().map(|info| &info.tmp_name))
585 .cloned()
586 .ok_or_else(|| UploadError::UploadFailed("source path not set".to_string()))?;
587
588 let target = self.upload_dir().join(&file_name);
589 if let Some(parent) = target.parent() {
590 tokio::fs::create_dir_all(parent).await?;
591 }
592 tokio::fs::copy(&source, &target).await?;
595 Ok(file_name)
596 }
597}
598
599#[async_trait]
600impl StorageEngine for LocalStorageEngine {
601 #[tracing::instrument(skip(self))]
602 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
603 if self.is_internal {
605 self.upload_by_internal().await?;
607 Ok(None)
608 } else {
609 let save_name = self.upload_by_external().await?;
611 Ok(Some(save_name))
612 }
613 }
614
615 #[tracing::instrument(skip(self))]
616 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
617 if file_name.contains("..") || std::path::Path::new(file_name).is_absolute() {
624 return Err(UploadError::InvalidFileName(file_name.to_string()));
625 }
626
627 let file_path = self.upload_dir().join(file_name);
628
629 if let (Ok(upload_canon), Ok(file_canon)) = (
631 std::fs::canonicalize(self.upload_dir()),
632 std::fs::canonicalize(&file_path),
633 ) {
634 if !file_canon.starts_with(&upload_canon) {
635 return Err(UploadError::InvalidFileName(file_name.to_string()));
636 }
637 }
638
639 if !file_path.exists() {
640 return Ok(true);
642 }
643 match tokio::fs::remove_file(&file_path).await {
645 Ok(_) => Ok(true),
646 Err(e) => {
647 self.error = Some(e.to_string());
649 Ok(false)
650 }
651 }
652 }
653
654 fn file_name(&self) -> Option<&str> {
655 self.file_name.as_deref()
656 }
657
658 fn file_info(&self) -> Option<&UploadFileInfo> {
659 self.file_info.as_ref()
660 }
661
662 fn error(&self) -> Option<&str> {
663 self.error.as_deref()
664 }
665
666 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
667 let info = UploadFileInfo::from_uploaded_file(file)?;
671 let save_name = build_save_name(&info.tmp_name, &info.extension);
672 self.upload_source_path = Some(info.tmp_name.clone());
673 self.file_info = Some(info);
674 self.file_name = Some(save_name);
675 self.is_internal = false;
676 Ok(())
677 }
678
679 fn set_upload_file_by_real(
680 &mut self,
681 file_path: &Path,
682 extension: &str,
683 ) -> Result<(), UploadError> {
684 let info = UploadFileInfo::from_real_path(file_path, extension)?;
689 let save_name = build_internal_save_name(file_path);
690 self.file_info = Some(info);
691 self.file_name = Some(save_name);
692 self.is_internal = true;
693 Ok(())
694 }
695
696 fn is_internal(&self) -> bool {
697 self.is_internal
698 }
699}
700
701pub struct AliyunStorageEngine {
713 config: EngineConfig,
715 file_info: Option<UploadFileInfo>,
717 file_name: Option<String>,
719 error: Option<String>,
721 is_internal: bool,
723}
724
725impl AliyunStorageEngine {
726 pub fn new(config: EngineConfig) -> Self {
728 Self {
729 config,
730 file_info: None,
731 file_name: None,
732 error: None,
733 is_internal: false,
734 }
735 }
736
737 fn create_storage(&self) -> sz_orm_storage::AliyunOssStorage {
739 sz_orm_storage::AliyunOssStorage::new(
740 self.config.bucket.clone(),
741 self.config.endpoint.clone(),
742 )
743 }
744}
745
746#[async_trait]
747impl StorageEngine for AliyunStorageEngine {
748 #[tracing::instrument(skip(self))]
749 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
750 let file_name = self
752 .file_name
753 .as_ref()
754 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
755 .clone();
756 let info = self
757 .file_info
758 .as_ref()
759 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
760 .clone();
761
762 let storage = self.create_storage();
763 let data = tokio::fs::read(&info.tmp_name).await?;
764 let content_type = mime_guess::from_path(&info.tmp_name)
765 .first_or_octet_stream()
766 .to_string();
767
768 match storage.put(&file_name, &data, &content_type).await {
769 Ok(_) => {
770 Ok(Some(file_name))
772 }
773 Err(e) => {
774 let msg = e.to_string();
776 self.error = Some(msg.clone());
777 Err(UploadError::UploadFailed(msg))
778 }
779 }
780 }
781
782 #[tracing::instrument(skip(self))]
783 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
784 let storage = self.create_storage();
786 match storage.delete(file_name).await {
787 Ok(_) => Ok(true),
788 Err(e) => {
789 let msg = e.to_string();
790 self.error = Some(msg);
791 Ok(false)
792 }
793 }
794 }
795
796 fn file_name(&self) -> Option<&str> {
797 self.file_name.as_deref()
798 }
799
800 fn file_info(&self) -> Option<&UploadFileInfo> {
801 self.file_info.as_ref()
802 }
803
804 fn error(&self) -> Option<&str> {
805 self.error.as_deref()
806 }
807
808 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
809 let info = UploadFileInfo::from_uploaded_file(file)?;
810 let save_name = build_save_name(&info.tmp_name, &info.extension);
811 self.file_info = Some(info);
812 self.file_name = Some(save_name);
813 self.is_internal = false;
814 Ok(())
815 }
816
817 fn set_upload_file_by_real(
818 &mut self,
819 file_path: &Path,
820 extension: &str,
821 ) -> Result<(), UploadError> {
822 let info = UploadFileInfo::from_real_path(file_path, extension)?;
823 let save_name = build_internal_save_name(file_path);
824 self.file_info = Some(info);
825 self.file_name = Some(save_name);
826 self.is_internal = true;
827 Ok(())
828 }
829
830 fn is_internal(&self) -> bool {
831 self.is_internal
832 }
833}
834
835pub struct QcloudStorageEngine {
847 config: EngineConfig,
849 file_info: Option<UploadFileInfo>,
851 file_name: Option<String>,
853 error: Option<String>,
855 is_internal: bool,
857}
858
859impl QcloudStorageEngine {
860 pub fn new(config: EngineConfig) -> Self {
862 Self {
863 config,
864 file_info: None,
865 file_name: None,
866 error: None,
867 is_internal: false,
868 }
869 }
870
871 fn create_storage(&self) -> sz_orm_storage::TencentCosStorage {
873 sz_orm_storage::TencentCosStorage::new(
874 self.config.bucket.clone(),
875 self.config.region.clone(),
876 )
877 }
878}
879
880#[async_trait]
881impl StorageEngine for QcloudStorageEngine {
882 #[tracing::instrument(skip(self))]
883 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
884 let file_name = self
886 .file_name
887 .as_ref()
888 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
889 .clone();
890 let info = self
891 .file_info
892 .as_ref()
893 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
894 .clone();
895
896 let storage = self.create_storage();
897 let data = tokio::fs::read(&info.tmp_name).await?;
898 let content_type = mime_guess::from_path(&info.tmp_name)
899 .first_or_octet_stream()
900 .to_string();
901
902 match storage.put(&file_name, &data, &content_type).await {
903 Ok(_) => Ok(Some(file_name)),
904 Err(e) => {
905 let msg = e.to_string();
906 self.error = Some(msg.clone());
907 Err(UploadError::UploadFailed(msg))
908 }
909 }
910 }
911
912 #[tracing::instrument(skip(self))]
913 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
914 let storage = self.create_storage();
915 match storage.delete(file_name).await {
916 Ok(_) => Ok(true),
917 Err(e) => {
918 let msg = e.to_string();
919 self.error = Some(msg);
920 Ok(false)
921 }
922 }
923 }
924
925 fn file_name(&self) -> Option<&str> {
926 self.file_name.as_deref()
927 }
928
929 fn file_info(&self) -> Option<&UploadFileInfo> {
930 self.file_info.as_ref()
931 }
932
933 fn error(&self) -> Option<&str> {
934 self.error.as_deref()
935 }
936
937 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
938 let info = UploadFileInfo::from_uploaded_file(file)?;
939 let save_name = build_save_name(&info.tmp_name, &info.extension);
940 self.file_info = Some(info);
941 self.file_name = Some(save_name);
942 self.is_internal = false;
943 Ok(())
944 }
945
946 fn set_upload_file_by_real(
947 &mut self,
948 file_path: &Path,
949 extension: &str,
950 ) -> Result<(), UploadError> {
951 let info = UploadFileInfo::from_real_path(file_path, extension)?;
952 let save_name = build_internal_save_name(file_path);
953 self.file_info = Some(info);
954 self.file_name = Some(save_name);
955 self.is_internal = true;
956 Ok(())
957 }
958
959 fn is_internal(&self) -> bool {
960 self.is_internal
961 }
962}
963
964pub struct QiniuStorageEngine {
976 config: EngineConfig,
978 file_info: Option<UploadFileInfo>,
980 file_name: Option<String>,
982 error: Option<String>,
984 is_internal: bool,
986}
987
988impl QiniuStorageEngine {
989 pub fn new(config: EngineConfig) -> Self {
991 Self {
992 config,
993 file_info: None,
994 file_name: None,
995 error: None,
996 is_internal: false,
997 }
998 }
999
1000 fn create_storage(&self) -> sz_orm_storage::QiniuKodoStorage {
1002 sz_orm_storage::QiniuKodoStorage::new(self.config.bucket.clone())
1003 }
1004}
1005
1006#[async_trait]
1007impl StorageEngine for QiniuStorageEngine {
1008 #[tracing::instrument(skip(self))]
1009 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1010 let file_name = self
1012 .file_name
1013 .as_ref()
1014 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1015 .clone();
1016 let info = self
1017 .file_info
1018 .as_ref()
1019 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1020 .clone();
1021
1022 let storage = self.create_storage();
1023 let data = tokio::fs::read(&info.tmp_name).await?;
1024 let content_type = mime_guess::from_path(&info.tmp_name)
1025 .first_or_octet_stream()
1026 .to_string();
1027
1028 match storage.put(&file_name, &data, &content_type).await {
1029 Ok(_) => Ok(Some(file_name)),
1030 Err(e) => {
1031 let msg = e.to_string();
1032 self.error = Some(msg.clone());
1033 Err(UploadError::UploadFailed(msg))
1034 }
1035 }
1036 }
1037
1038 #[tracing::instrument(skip(self))]
1039 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1040 let storage = self.create_storage();
1041 match storage.delete(file_name).await {
1042 Ok(_) => Ok(true),
1043 Err(e) => {
1044 let msg = e.to_string();
1045 self.error = Some(msg);
1046 Ok(false)
1047 }
1048 }
1049 }
1050
1051 fn file_name(&self) -> Option<&str> {
1052 self.file_name.as_deref()
1053 }
1054
1055 fn file_info(&self) -> Option<&UploadFileInfo> {
1056 self.file_info.as_ref()
1057 }
1058
1059 fn error(&self) -> Option<&str> {
1060 self.error.as_deref()
1061 }
1062
1063 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1064 let info = UploadFileInfo::from_uploaded_file(file)?;
1065 let save_name = build_save_name(&info.tmp_name, &info.extension);
1066 self.file_info = Some(info);
1067 self.file_name = Some(save_name);
1068 self.is_internal = false;
1069 Ok(())
1070 }
1071
1072 fn set_upload_file_by_real(
1073 &mut self,
1074 file_path: &Path,
1075 extension: &str,
1076 ) -> Result<(), UploadError> {
1077 let info = UploadFileInfo::from_real_path(file_path, extension)?;
1078 let save_name = build_internal_save_name(file_path);
1079 self.file_info = Some(info);
1080 self.file_name = Some(save_name);
1081 self.is_internal = true;
1082 Ok(())
1083 }
1084
1085 fn is_internal(&self) -> bool {
1086 self.is_internal
1087 }
1088}
1089
1090pub struct S3StorageEngine {
1098 config: EngineConfig,
1100 file_info: Option<UploadFileInfo>,
1102 file_name: Option<String>,
1104 error: Option<String>,
1106 is_internal: bool,
1108}
1109
1110impl S3StorageEngine {
1111 pub fn new(config: EngineConfig) -> Self {
1113 Self {
1114 config,
1115 file_info: None,
1116 file_name: None,
1117 error: None,
1118 is_internal: false,
1119 }
1120 }
1121
1122 fn create_storage(&self) -> sz_orm_storage::S3Storage {
1124 sz_orm_storage::S3Storage::new(self.config.bucket.clone(), self.config.region.clone())
1125 }
1126}
1127
1128#[async_trait]
1129impl StorageEngine for S3StorageEngine {
1130 #[tracing::instrument(skip(self))]
1131 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1132 let file_name = self
1133 .file_name
1134 .as_ref()
1135 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1136 .clone();
1137 let info = self
1138 .file_info
1139 .as_ref()
1140 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1141 .clone();
1142
1143 let storage = self.create_storage();
1144 let data = tokio::fs::read(&info.tmp_name).await?;
1145 let content_type = mime_guess::from_path(&info.tmp_name)
1146 .first_or_octet_stream()
1147 .to_string();
1148
1149 match storage.put(&file_name, &data, &content_type).await {
1150 Ok(_) => Ok(Some(file_name)),
1151 Err(e) => {
1152 let msg = e.to_string();
1153 self.error = Some(msg.clone());
1154 Err(UploadError::UploadFailed(msg))
1155 }
1156 }
1157 }
1158
1159 #[tracing::instrument(skip(self))]
1160 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1161 let storage = self.create_storage();
1162 match storage.delete(file_name).await {
1163 Ok(_) => Ok(true),
1164 Err(e) => {
1165 let msg = e.to_string();
1166 self.error = Some(msg);
1167 Ok(false)
1168 }
1169 }
1170 }
1171
1172 fn file_name(&self) -> Option<&str> {
1173 self.file_name.as_deref()
1174 }
1175
1176 fn file_info(&self) -> Option<&UploadFileInfo> {
1177 self.file_info.as_ref()
1178 }
1179
1180 fn error(&self) -> Option<&str> {
1181 self.error.as_deref()
1182 }
1183
1184 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1185 let info = UploadFileInfo::from_uploaded_file(file)?;
1186 let save_name = build_save_name(&info.tmp_name, &info.extension);
1187 self.file_info = Some(info);
1188 self.file_name = Some(save_name);
1189 self.is_internal = false;
1190 Ok(())
1191 }
1192
1193 fn set_upload_file_by_real(
1194 &mut self,
1195 file_path: &Path,
1196 extension: &str,
1197 ) -> Result<(), UploadError> {
1198 let info = UploadFileInfo::from_real_path(file_path, extension)?;
1199 let save_name = build_internal_save_name(file_path);
1200 self.file_info = Some(info);
1201 self.file_name = Some(save_name);
1202 self.is_internal = true;
1203 Ok(())
1204 }
1205
1206 fn is_internal(&self) -> bool {
1207 self.is_internal
1208 }
1209}
1210
1211pub enum StorageDriver {
1222 Local(LocalStorageEngine),
1224 Aliyun(AliyunStorageEngine),
1226 Qcloud(QcloudStorageEngine),
1228 Qiniu(QiniuStorageEngine),
1230 S3(S3StorageEngine),
1232}
1233
1234impl StorageDriver {
1235 pub fn new(kind: StorageEngineKind, config: EngineConfig) -> Self {
1245 match kind {
1246 StorageEngineKind::Local => Self::Local(LocalStorageEngine::new(config)),
1247 StorageEngineKind::Aliyun => Self::Aliyun(AliyunStorageEngine::new(config)),
1248 StorageEngineKind::Qcloud => Self::Qcloud(QcloudStorageEngine::new(config)),
1249 StorageEngineKind::Qiniu => Self::Qiniu(QiniuStorageEngine::new(config)),
1250 StorageEngineKind::S3 => Self::S3(S3StorageEngine::new(config)),
1251 }
1252 }
1253
1254 #[tracing::instrument(skip(self))]
1256 pub async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1257 match self {
1258 Self::Local(e) => e.upload().await,
1259 Self::Aliyun(e) => e.upload().await,
1260 Self::Qcloud(e) => e.upload().await,
1261 Self::Qiniu(e) => e.upload().await,
1262 Self::S3(e) => e.upload().await,
1263 }
1264 }
1265
1266 #[tracing::instrument(skip(self))]
1268 pub async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1269 match self {
1270 Self::Local(e) => e.delete(file_name).await,
1271 Self::Aliyun(e) => e.delete(file_name).await,
1272 Self::Qcloud(e) => e.delete(file_name).await,
1273 Self::Qiniu(e) => e.delete(file_name).await,
1274 Self::S3(e) => e.delete(file_name).await,
1275 }
1276 }
1277
1278 pub fn file_name(&self) -> Option<&str> {
1280 match self {
1281 Self::Local(e) => e.file_name(),
1282 Self::Aliyun(e) => e.file_name(),
1283 Self::Qcloud(e) => e.file_name(),
1284 Self::Qiniu(e) => e.file_name(),
1285 Self::S3(e) => e.file_name(),
1286 }
1287 }
1288
1289 pub fn file_info(&self) -> Option<&UploadFileInfo> {
1291 match self {
1292 Self::Local(e) => e.file_info(),
1293 Self::Aliyun(e) => e.file_info(),
1294 Self::Qcloud(e) => e.file_info(),
1295 Self::Qiniu(e) => e.file_info(),
1296 Self::S3(e) => e.file_info(),
1297 }
1298 }
1299
1300 pub fn error(&self) -> Option<&str> {
1302 match self {
1303 Self::Local(e) => e.error(),
1304 Self::Aliyun(e) => e.error(),
1305 Self::Qcloud(e) => e.error(),
1306 Self::Qiniu(e) => e.error(),
1307 Self::S3(e) => e.error(),
1308 }
1309 }
1310
1311 pub fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1313 match self {
1314 Self::Local(e) => e.set_upload_file(file),
1315 Self::Aliyun(e) => e.set_upload_file(file),
1316 Self::Qcloud(e) => e.set_upload_file(file),
1317 Self::Qiniu(e) => e.set_upload_file(file),
1318 Self::S3(e) => e.set_upload_file(file),
1319 }
1320 }
1321
1322 pub fn set_upload_file_by_real(
1324 &mut self,
1325 file_path: &Path,
1326 extension: &str,
1327 ) -> Result<(), UploadError> {
1328 match self {
1329 Self::Local(e) => e.set_upload_file_by_real(file_path, extension),
1330 Self::Aliyun(e) => e.set_upload_file_by_real(file_path, extension),
1331 Self::Qcloud(e) => e.set_upload_file_by_real(file_path, extension),
1332 Self::Qiniu(e) => e.set_upload_file_by_real(file_path, extension),
1333 Self::S3(e) => e.set_upload_file_by_real(file_path, extension),
1334 }
1335 }
1336
1337 pub fn is_internal(&self) -> bool {
1339 match self {
1340 Self::Local(e) => e.is_internal(),
1341 Self::Aliyun(e) => e.is_internal(),
1342 Self::Qcloud(e) => e.is_internal(),
1343 Self::Qiniu(e) => e.is_internal(),
1344 Self::S3(e) => e.is_internal(),
1345 }
1346 }
1347
1348 pub fn kind(&self) -> StorageEngineKind {
1350 match self {
1351 Self::Local(_) => StorageEngineKind::Local,
1352 Self::Aliyun(_) => StorageEngineKind::Aliyun,
1353 Self::Qcloud(_) => StorageEngineKind::Qcloud,
1354 Self::Qiniu(_) => StorageEngineKind::Qiniu,
1355 Self::S3(_) => StorageEngineKind::S3,
1356 }
1357 }
1358}
1359
1360#[cfg(test)]
1365mod tests {
1366 use super::*;
1367 use std::io::Write;
1368
1369 fn create_temp_file(name: &str, content: &[u8]) -> std::path::PathBuf {
1375 let dir = std::env::temp_dir().join(format!(
1376 "sz_rust_storage_test_{}",
1377 std::time::SystemTime::now()
1378 .duration_since(std::time::UNIX_EPOCH)
1379 .unwrap_or_default()
1380 .as_nanos()
1381 ));
1382 std::fs::create_dir_all(&dir).unwrap();
1383 let path = dir.join(name);
1384 let mut f = std::fs::File::create(&path).unwrap();
1385 f.write_all(content).unwrap();
1386 path
1387 }
1388
1389 fn create_temp_base_path() -> std::path::PathBuf {
1391 let dir = std::env::temp_dir().join(format!(
1392 "sz_rust_storage_base_{}",
1393 std::time::SystemTime::now()
1394 .duration_since(std::time::UNIX_EPOCH)
1395 .unwrap_or_default()
1396 .as_nanos()
1397 ));
1398 std::fs::create_dir_all(&dir).unwrap();
1399 dir
1400 }
1401
1402 #[test]
1407 fn test_storage_engine_kind_parse_local() {
1408 assert_eq!(
1409 StorageEngineKind::parse("local"),
1410 Some(StorageEngineKind::Local)
1411 );
1412 }
1413
1414 #[test]
1415 fn test_storage_engine_kind_parse_aliyun() {
1416 assert_eq!(
1417 StorageEngineKind::parse("aliyun"),
1418 Some(StorageEngineKind::Aliyun)
1419 );
1420 }
1421
1422 #[test]
1423 fn test_storage_engine_kind_parse_qcloud() {
1424 assert_eq!(
1425 StorageEngineKind::parse("qcloud"),
1426 Some(StorageEngineKind::Qcloud)
1427 );
1428 }
1429
1430 #[test]
1431 fn test_storage_engine_kind_parse_qiniu() {
1432 assert_eq!(
1433 StorageEngineKind::parse("qiniu"),
1434 Some(StorageEngineKind::Qiniu)
1435 );
1436 }
1437
1438 #[test]
1439 fn test_storage_engine_kind_parse_s3() {
1440 assert_eq!(StorageEngineKind::parse("s3"), Some(StorageEngineKind::S3));
1441 }
1442
1443 #[test]
1444 fn test_storage_engine_kind_parse_case_insensitive() {
1445 assert_eq!(
1447 StorageEngineKind::parse("LOCAL"),
1448 Some(StorageEngineKind::Local)
1449 );
1450 assert_eq!(
1451 StorageEngineKind::parse("Aliyun"),
1452 Some(StorageEngineKind::Aliyun)
1453 );
1454 }
1455
1456 #[test]
1457 fn test_storage_engine_kind_parse_invalid() {
1458 assert_eq!(StorageEngineKind::parse("invalid"), None);
1459 assert_eq!(StorageEngineKind::parse(""), None);
1460 }
1461
1462 #[test]
1463 fn test_storage_engine_kind_as_str() {
1464 assert_eq!(StorageEngineKind::Local.as_str(), "local");
1465 assert_eq!(StorageEngineKind::Aliyun.as_str(), "aliyun");
1466 assert_eq!(StorageEngineKind::Qcloud.as_str(), "qcloud");
1467 assert_eq!(StorageEngineKind::Qiniu.as_str(), "qiniu");
1468 assert_eq!(StorageEngineKind::S3.as_str(), "s3");
1469 }
1470
1471 #[test]
1476 fn test_engine_config_builder() {
1477 let config = EngineConfig::new()
1478 .with_bucket("my-bucket")
1479 .with_region("us-east-1")
1480 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1481 .with_domain("https://cdn.example.com")
1482 .with_base_path("/var/www/uploads")
1483 .with_access_key_id("akid")
1484 .with_access_key_secret("aksecret")
1485 .with_secret_id("sid")
1486 .with_secret_key("sk")
1487 .with_access_key("ak");
1488 assert_eq!(config.bucket, "my-bucket");
1489 assert_eq!(config.region, "us-east-1");
1490 assert_eq!(config.endpoint, "oss-cn-hangzhou.aliyuncs.com");
1491 assert_eq!(config.domain, "https://cdn.example.com");
1492 assert_eq!(config.base_path, "/var/www/uploads");
1493 assert_eq!(config.access_key_id, "akid");
1494 assert_eq!(config.access_key_secret, "aksecret");
1495 assert_eq!(config.secret_id, "sid");
1496 assert_eq!(config.secret_key, "sk");
1497 assert_eq!(config.access_key, "ak");
1498 }
1499
1500 #[test]
1501 fn test_engine_config_default() {
1502 let config = EngineConfig::default();
1503 assert!(config.bucket.is_empty());
1504 assert!(config.region.is_empty());
1505 assert!(config.endpoint.is_empty());
1506 assert!(config.domain.is_empty());
1507 assert!(config.base_path.is_empty());
1508 }
1509
1510 #[test]
1515 fn test_build_save_name_format() {
1516 let path = Path::new("/tmp/photo.jpg");
1517 let name = build_save_name(path, "jpg");
1518 assert!(name.starts_with("storage/"), "name = {}", name);
1520 assert!(name.ends_with(".jpg"), "name = {}", name);
1521 let parts: Vec<&str> = name.split('/').collect();
1523 assert_eq!(parts.len(), 3, "name = {}", name);
1524 let ymd_part = parts[1];
1525 assert_eq!(ymd_part.len(), 8, "Ymd should be 8 chars");
1526 assert!(ymd_part.chars().all(|c| c.is_ascii_digit()));
1527 }
1528
1529 #[test]
1530 fn test_build_save_name_md5_prefix_consistent() {
1531 let path = Path::new("/tmp/test.png");
1533 let name1 = build_save_name(path, "png");
1534 let name2 = build_save_name(path, "png");
1535 let extract_md5 = |s: &str| -> String {
1539 let parts: Vec<&str> = s.split('/').collect();
1540 if parts.len() != 3 {
1541 return String::new();
1542 }
1543 let last = parts[2];
1544 let last = last.rsplit_once('.').map(|(l, _)| l).unwrap_or(last);
1546 if last.len() > 14 {
1548 last[14..19].to_string()
1549 } else {
1550 String::new()
1551 }
1552 };
1553 let md5_1 = extract_md5(&name1);
1554 let md5_2 = extract_md5(&name2);
1555 assert_eq!(
1556 md5_1, md5_2,
1557 "md5 prefix should be consistent for same path"
1558 );
1559 assert_eq!(md5_1.len(), 5, "md5 prefix should be 5 chars");
1560 }
1561
1562 #[test]
1563 fn test_build_save_name_empty_extension() {
1564 let path = Path::new("/tmp/noext");
1565 let name = build_save_name(path, "");
1566 assert!(!name.ends_with('.'), "name = {}", name);
1568 assert!(name.starts_with("storage/"));
1569 }
1570
1571 #[test]
1572 fn test_build_internal_save_name_format() {
1573 let path = Path::new("/tmp/photo.jpg");
1574 let name = build_internal_save_name(path);
1575 assert!(name.starts_with("storage/"), "name = {}", name);
1577 assert!(name.ends_with("photo.jpg"), "name = {}", name);
1578 let parts: Vec<&str> = name.split('/').collect();
1579 assert_eq!(parts.len(), 3, "name = {}", name);
1580 assert_eq!(parts[1].len(), 8);
1582 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
1583 }
1584
1585 #[test]
1590 fn test_upload_file_info_from_real_path() {
1591 let path = create_temp_file("test.txt", b"hello world");
1592 let info = UploadFileInfo::from_real_path(&path, "txt").unwrap();
1593 assert_eq!(info.name, "test.txt");
1594 assert_eq!(info.size, 11);
1595 assert_eq!(info.extension, "txt");
1596 assert_eq!(info.tmp_name, path);
1597 assert_eq!(info.error, 0);
1598 assert!(info.is_internal);
1599 std::fs::remove_file(&path).ok();
1600 }
1601
1602 #[test]
1603 fn test_upload_file_info_from_real_path_not_found() {
1604 let result = UploadFileInfo::from_real_path("/nonexistent/path.txt", "txt");
1605 assert!(result.is_err());
1606 }
1607
1608 #[tokio::test]
1613 async fn test_local_storage_engine_new() {
1614 let config = EngineConfig::new().with_base_path("/tmp/test");
1615 let engine = LocalStorageEngine::new(config);
1616 assert!(!engine.is_internal());
1617 assert!(engine.file_name().is_none());
1618 assert!(engine.file_info().is_none());
1619 assert!(engine.error().is_none());
1620 }
1621
1622 #[tokio::test]
1623 async fn test_local_storage_upload_dir_default() {
1624 let config = EngineConfig::new();
1625 let engine = LocalStorageEngine::new(config);
1626 assert_eq!(engine.upload_dir(), PathBuf::from("uploads"));
1627 }
1628
1629 #[tokio::test]
1630 async fn test_local_storage_upload_dir_with_base_path() {
1631 let base = create_temp_base_path();
1632 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1633 let engine = LocalStorageEngine::new(config);
1634 let expected = base.join("uploads");
1635 assert_eq!(engine.upload_dir(), expected);
1636 std::fs::remove_dir_all(&base).ok();
1637 }
1638
1639 #[tokio::test]
1640 async fn test_local_storage_set_upload_file_by_real() {
1641 let path = create_temp_file("internal.txt", b"internal data");
1642 let base = create_temp_base_path();
1643 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1644 let mut engine = LocalStorageEngine::new(config);
1645
1646 engine
1647 .set_upload_file_by_real(&path, "txt")
1648 .expect("set_upload_file_by_real failed");
1649
1650 assert!(engine.is_internal());
1652 let file_name = engine.file_name().expect("file name should be set");
1653 assert!(file_name.starts_with("storage/"));
1654 assert!(file_name.ends_with("internal.txt"));
1655
1656 let info = engine.file_info().expect("file info should be set");
1657 assert_eq!(info.name, "internal.txt");
1658 assert!(info.is_internal);
1659
1660 std::fs::remove_file(&path).ok();
1661 std::fs::remove_dir_all(&base).ok();
1662 }
1663
1664 #[tokio::test]
1665 async fn test_local_storage_upload_internal_uses_rename() {
1666 let path = create_temp_file("rename_test.txt", b"rename me");
1667 let base = create_temp_base_path();
1668 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1669 let mut engine = LocalStorageEngine::new(config);
1670
1671 engine
1672 .set_upload_file_by_real(&path, "txt")
1673 .expect("set_upload_file_by_real failed");
1674 let file_name = engine.file_name().unwrap().to_string();
1675
1676 let result = engine.upload().await;
1678 assert!(result.is_ok(), "upload should succeed");
1679 assert!(result.unwrap().is_none());
1681
1682 assert!(!path.exists(), "original file should be renamed away");
1684 let target = engine.upload_dir().join(&file_name);
1686 assert!(target.exists(), "target file should exist: {:?}", target);
1687
1688 std::fs::remove_dir_all(&base).ok();
1689 }
1690
1691 #[tokio::test]
1692 async fn test_local_storage_upload_external_returns_save_name() {
1693 let path = create_temp_file("external.txt", b"external data");
1694 let base = create_temp_base_path();
1695 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1696 let mut engine = LocalStorageEngine::new(config);
1697
1698 let file = UploadedFile::new(&path, "external.txt", Some("text/plain"), Some(0), true)
1700 .expect("UploadedFile::new failed");
1701 engine
1702 .set_upload_file(&file)
1703 .expect("set_upload_file failed");
1704
1705 let result = engine.upload().await;
1706 assert!(result.is_ok(), "upload should succeed");
1707 let save_name = result
1708 .unwrap()
1709 .expect("external upload should return save name");
1710 assert!(save_name.starts_with("storage/"));
1711
1712 assert!(path.exists(), "source file should be preserved");
1714
1715 let target = engine.upload_dir().join(&save_name);
1717 assert!(target.exists(), "target file should exist: {:?}", target);
1718
1719 std::fs::remove_file(&path).ok();
1720 std::fs::remove_dir_all(&base).ok();
1721 }
1722
1723 #[tokio::test]
1724 async fn test_local_storage_delete_existing_file() {
1725 let base = create_temp_base_path();
1726 let uploads_dir = base.join("uploads").join("storage").join("20260101");
1727 std::fs::create_dir_all(&uploads_dir).unwrap();
1728 let file_path = uploads_dir.join("delete_me.txt");
1729 std::fs::write(&file_path, b"delete me").unwrap();
1730
1731 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1732 let mut engine = LocalStorageEngine::new(config);
1733
1734 let result = engine.delete("storage/20260101/delete_me.txt").await;
1736 assert!(result.is_ok());
1737 assert!(result.unwrap(), "delete should return true");
1738 assert!(!file_path.exists(), "file should be deleted");
1739
1740 std::fs::remove_dir_all(&base).ok();
1741 }
1742
1743 #[tokio::test]
1744 async fn test_local_storage_delete_nonexistent_file() {
1745 let base = create_temp_base_path();
1746 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1747 let mut engine = LocalStorageEngine::new(config);
1748
1749 let result = engine.delete("nonexistent.txt").await;
1751 assert!(result.is_ok());
1752 assert!(result.unwrap(), "delete nonexistent should return true");
1753
1754 std::fs::remove_dir_all(&base).ok();
1755 }
1756
1757 #[test]
1762 fn test_aliyun_storage_engine_new() {
1763 let config = EngineConfig::new()
1764 .with_bucket("aliyun-bucket")
1765 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1766 .with_access_key_id("akid")
1767 .with_access_key_secret("aksecret");
1768 let engine = AliyunStorageEngine::new(config);
1769 assert!(!engine.is_internal());
1770 assert!(engine.file_name().is_none());
1771 }
1772
1773 #[test]
1774 fn test_qcloud_storage_engine_new() {
1775 let config = EngineConfig::new()
1776 .with_bucket("cos-bucket")
1777 .with_region("ap-guangzhou")
1778 .with_secret_id("sid")
1779 .with_secret_key("sk");
1780 let engine = QcloudStorageEngine::new(config);
1781 assert!(!engine.is_internal());
1782 }
1783
1784 #[test]
1785 fn test_qiniu_storage_engine_new() {
1786 let config = EngineConfig::new()
1787 .with_bucket("qiniu-bucket")
1788 .with_access_key("ak")
1789 .with_secret_key("sk");
1790 let engine = QiniuStorageEngine::new(config);
1791 assert!(!engine.is_internal());
1792 }
1793
1794 #[test]
1795 fn test_s3_storage_engine_new() {
1796 let config = EngineConfig::new()
1797 .with_bucket("s3-bucket")
1798 .with_region("us-east-1");
1799 let engine = S3StorageEngine::new(config);
1800 assert!(!engine.is_internal());
1801 }
1802
1803 #[test]
1808 fn test_aliyun_set_upload_file_by_real() {
1809 let path = create_temp_file("aliyun.txt", b"aliyun");
1810 let config = EngineConfig::new()
1811 .with_bucket("bucket")
1812 .with_endpoint("endpoint");
1813 let mut engine = AliyunStorageEngine::new(config);
1814 engine.set_upload_file_by_real(&path, "txt").unwrap();
1815 assert!(engine.is_internal());
1816 assert!(engine.file_name().unwrap().starts_with("storage/"));
1817 std::fs::remove_file(&path).ok();
1818 }
1819
1820 #[test]
1821 fn test_qcloud_set_upload_file_by_real() {
1822 let path = create_temp_file("qcloud.txt", b"qcloud");
1823 let config = EngineConfig::new()
1824 .with_bucket("bucket")
1825 .with_region("region");
1826 let mut engine = QcloudStorageEngine::new(config);
1827 engine.set_upload_file_by_real(&path, "txt").unwrap();
1828 assert!(engine.is_internal());
1829 std::fs::remove_file(&path).ok();
1830 }
1831
1832 #[test]
1833 fn test_qiniu_set_upload_file_by_real() {
1834 let path = create_temp_file("qiniu.txt", b"qiniu");
1835 let config = EngineConfig::new().with_bucket("bucket");
1836 let mut engine = QiniuStorageEngine::new(config);
1837 engine.set_upload_file_by_real(&path, "txt").unwrap();
1838 assert!(engine.is_internal());
1839 std::fs::remove_file(&path).ok();
1840 }
1841
1842 #[test]
1843 fn test_s3_set_upload_file_by_real() {
1844 let path = create_temp_file("s3.txt", b"s3");
1845 let config = EngineConfig::new()
1846 .with_bucket("bucket")
1847 .with_region("region");
1848 let mut engine = S3StorageEngine::new(config);
1849 engine.set_upload_file_by_real(&path, "txt").unwrap();
1850 assert!(engine.is_internal());
1851 std::fs::remove_file(&path).ok();
1852 }
1853
1854 #[test]
1859 fn test_storage_driver_new_local() {
1860 let config = EngineConfig::new();
1861 let driver = StorageDriver::new(StorageEngineKind::Local, config);
1862 assert_eq!(driver.kind(), StorageEngineKind::Local);
1863 assert!(!driver.is_internal());
1864 }
1865
1866 #[test]
1867 fn test_storage_driver_new_aliyun() {
1868 let config = EngineConfig::new();
1869 let driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1870 assert_eq!(driver.kind(), StorageEngineKind::Aliyun);
1871 }
1872
1873 #[test]
1874 fn test_storage_driver_new_qcloud() {
1875 let config = EngineConfig::new();
1876 let driver = StorageDriver::new(StorageEngineKind::Qcloud, config);
1877 assert_eq!(driver.kind(), StorageEngineKind::Qcloud);
1878 }
1879
1880 #[test]
1881 fn test_storage_driver_new_qiniu() {
1882 let config = EngineConfig::new();
1883 let driver = StorageDriver::new(StorageEngineKind::Qiniu, config);
1884 assert_eq!(driver.kind(), StorageEngineKind::Qiniu);
1885 }
1886
1887 #[test]
1888 fn test_storage_driver_new_s3() {
1889 let config = EngineConfig::new();
1890 let driver = StorageDriver::new(StorageEngineKind::S3, config);
1891 assert_eq!(driver.kind(), StorageEngineKind::S3);
1892 }
1893
1894 #[tokio::test]
1899 async fn test_storage_driver_local_upload_and_delete() {
1900 let path = create_temp_file("driver.txt", b"driver data");
1901 let base = create_temp_base_path();
1902 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1903 let mut driver = StorageDriver::new(StorageEngineKind::Local, config);
1904
1905 driver
1906 .set_upload_file_by_real(&path, "txt")
1907 .expect("set_upload_file_by_real failed");
1908 let file_name = driver.file_name().unwrap().to_string();
1909
1910 let result = driver.upload().await;
1911 assert!(result.is_ok());
1912 assert!(result.unwrap().is_none());
1913
1914 let del_result = driver.delete(&file_name).await;
1916 assert!(del_result.is_ok());
1917 assert!(del_result.unwrap());
1918
1919 std::fs::remove_file(&path).ok();
1920 std::fs::remove_dir_all(&base).ok();
1921 }
1922
1923 #[tokio::test]
1924 async fn test_storage_driver_aliyun_upload() {
1925 let path = create_temp_file("aliyun_driver.txt", b"aliyun data");
1926 let config = EngineConfig::new()
1927 .with_bucket("test-bucket")
1928 .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
1929 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1930
1931 driver
1932 .set_upload_file_by_real(&path, "txt")
1933 .expect("set_upload_file_by_real failed");
1934 let result = driver.upload().await;
1935 assert!(result.is_ok(), "upload should succeed");
1936 assert!(result.unwrap().is_some());
1938
1939 std::fs::remove_file(&path).ok();
1940 }
1941
1942 #[tokio::test]
1943 async fn test_storage_driver_error_after_failed_delete() {
1944 let config = EngineConfig::new()
1946 .with_bucket("bucket")
1947 .with_endpoint("endpoint");
1948 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1949 let result = driver.delete("nonexistent.txt").await;
1950 assert!(result.is_ok());
1951 }
1952
1953 #[test]
1959 fn test_r5_16_build_save_name_format() {
1960 let path = Path::new("/tmp/r5_16_test.jpg");
1961 let name = build_save_name(path, "jpg");
1962
1963 assert!(name.starts_with("storage/"), "name = {}", name);
1965 assert!(name.ends_with(".jpg"), "name = {}", name);
1966
1967 let parts: Vec<&str> = name.split('/').collect();
1968 assert_eq!(parts.len(), 3, "should have 3 parts: storage, Ymd, rest");
1969
1970 let ymd = parts[1];
1972 assert_eq!(ymd.len(), 8, "Ymd should be 8 digits");
1973 assert!(ymd.chars().all(|c| c.is_ascii_digit()));
1974
1975 let last = parts[2];
1977 assert_eq!(
1978 last.len(),
1979 27,
1980 "last part length = {}, last = {}",
1981 last.len(),
1982 last
1983 );
1984 }
1985
1986 #[test]
1988 fn test_r5_17_internal_save_name_uses_basename() {
1989 let path = Path::new("/tmp/r5_17_test.png");
1990 let name = build_internal_save_name(path);
1991
1992 assert!(name.starts_with("storage/"));
1994 assert!(name.ends_with("r5_17_test.png"));
1995
1996 let parts: Vec<&str> = name.split('/').collect();
1997 assert_eq!(parts.len(), 3, "should have 3 parts");
1998 assert_eq!(parts[1].len(), 8);
2000 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
2001 assert_eq!(parts[2], "r5_17_test.png");
2003 }
2004
2005 #[tokio::test]
2007 async fn test_r5_18_real_path_internal_vs_external() {
2008 let internal_path = create_temp_file("internal.txt", b"internal");
2009 let external_path = create_temp_file("external.txt", b"external");
2010 let base = create_temp_base_path();
2011 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2012
2013 let mut engine = LocalStorageEngine::new(config.clone());
2015 engine
2016 .set_upload_file_by_real(&internal_path, "txt")
2017 .unwrap();
2018 assert!(engine.is_internal());
2019 let real_path = engine.real_path().expect("real_path should be set");
2020 assert_eq!(real_path, internal_path);
2021
2022 let mut engine2 = LocalStorageEngine::new(config);
2024 let file = UploadedFile::new(
2025 &external_path,
2026 "external.txt",
2027 Some("text/plain"),
2028 Some(0),
2029 true,
2030 )
2031 .expect("UploadedFile::new failed");
2032 engine2.set_upload_file(&file).unwrap();
2033 assert!(!engine2.is_internal());
2034 let real_path = engine2.real_path().expect("real_path should be set");
2035 assert_eq!(real_path, external_path);
2036
2037 std::fs::remove_file(&internal_path).ok();
2038 std::fs::remove_file(&external_path).ok();
2039 std::fs::remove_dir_all(&base).ok();
2040 }
2041
2042 #[tokio::test]
2044 async fn test_r5_19_upload_internal_rename_external_copy() {
2045 let internal_path = create_temp_file("r5_19_internal.txt", b"internal");
2046 let external_path = create_temp_file("r5_19_external.txt", b"external");
2047 let base = create_temp_base_path();
2048 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2049
2050 let mut engine = LocalStorageEngine::new(config.clone());
2052 engine
2053 .set_upload_file_by_real(&internal_path, "txt")
2054 .unwrap();
2055 engine.upload().await.expect("internal upload failed");
2056 assert!(!internal_path.exists(), "rename should move file away");
2057
2058 let mut engine2 = LocalStorageEngine::new(config);
2060 let file = UploadedFile::new(
2061 &external_path,
2062 "r5_19_external.txt",
2063 Some("text/plain"),
2064 Some(0),
2065 true,
2066 )
2067 .expect("UploadedFile::new failed");
2068 engine2.set_upload_file(&file).unwrap();
2069 engine2.upload().await.expect("external upload failed");
2070 assert!(external_path.exists(), "copy should preserve source");
2071
2072 std::fs::remove_file(&internal_path).ok();
2073 std::fs::remove_file(&external_path).ok();
2074 std::fs::remove_dir_all(&base).ok();
2075 }
2076
2077 #[tokio::test]
2079 async fn test_r5_20_delete_elvis_short_circuit() {
2080 let base = create_temp_base_path();
2081 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2082 let mut engine = LocalStorageEngine::new(config);
2083
2084 let result = engine.delete("nonexistent_file.txt").await;
2086 assert!(result.is_ok());
2087 assert!(result.unwrap(), "delete nonexistent should return true");
2088
2089 let uploads_dir = base.join("uploads").join("storage").join("20260101");
2091 std::fs::create_dir_all(&uploads_dir).unwrap();
2092 let file_path = uploads_dir.join("exists.txt");
2093 std::fs::write(&file_path, b"exists").unwrap();
2094
2095 let result = engine.delete("storage/20260101/exists.txt").await;
2096 assert!(result.is_ok());
2097 assert!(result.unwrap(), "delete existing should return true");
2098 assert!(!file_path.exists(), "file should be deleted");
2099
2100 std::fs::remove_dir_all(&base).ok();
2101 }
2102
2103 #[tokio::test]
2105 async fn test_r5_21_internal_upload_failure_sets_error() {
2106 let nonexistent_path = std::env::temp_dir().join(format!(
2108 "nonexistent_{}.txt",
2109 std::time::SystemTime::now()
2110 .duration_since(std::time::UNIX_EPOCH)
2111 .unwrap_or_default()
2112 .as_nanos()
2113 ));
2114 let path = create_temp_file("will_fail.txt", b"will fail");
2117 let base = create_temp_base_path();
2118 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2120 let mut engine = LocalStorageEngine::new(config);
2121
2122 engine
2123 .set_upload_file_by_real(&path, "txt")
2124 .expect("set_upload_file_by_real failed");
2125
2126 std::fs::remove_file(&path).unwrap();
2128
2129 let result = engine.upload().await;
2130 assert!(result.is_err(), "upload should fail when source missing");
2132 let error_msg = engine.error().unwrap_or("");
2134 assert!(
2135 error_msg.contains("upload write error"),
2136 "error should contain 'upload write error', got: {}",
2137 error_msg
2138 );
2139
2140 let _ = nonexistent_path;
2141 std::fs::remove_dir_all(&base).ok();
2142 }
2143
2144 #[tokio::test]
2146 async fn test_r5_22_cloud_upload_returns_save_name() {
2147 let path = create_temp_file("r5_22.txt", b"cloud upload");
2148 let config = EngineConfig::new()
2149 .with_bucket("r5_22_bucket")
2150 .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
2151 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
2152
2153 driver
2154 .set_upload_file_by_real(&path, "txt")
2155 .expect("set_upload_file_by_real failed");
2156 let file_name = driver.file_name().unwrap().to_string();
2157
2158 let result = driver.upload().await;
2159 assert!(result.is_ok(), "upload should succeed");
2160 let returned = result.unwrap().expect("cloud upload should return Some");
2161 assert_eq!(returned, file_name, "should return save name");
2162
2163 std::fs::remove_file(&path).ok();
2164 }
2165
2166 #[tokio::test]
2168 async fn test_r5_23_local_external_upload_returns_save_name() {
2169 let path = create_temp_file("r5_23.txt", b"external save name");
2170 let base = create_temp_base_path();
2171 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2172 let mut engine = LocalStorageEngine::new(config);
2173
2174 let file = UploadedFile::new(&path, "r5_23.txt", Some("text/plain"), Some(0), true)
2175 .expect("UploadedFile::new failed");
2176 engine.set_upload_file(&file).unwrap();
2177 let expected_name = engine.file_name().unwrap().to_string();
2178
2179 let result = engine.upload().await;
2180 assert!(result.is_ok(), "upload should succeed");
2181 let save_name = result
2182 .unwrap()
2183 .expect("external upload should return save name");
2184 assert_eq!(save_name, expected_name, "should return save name");
2185
2186 std::fs::remove_file(&path).ok();
2187 std::fs::remove_dir_all(&base).ok();
2188 }
2189}