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(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 fn from_uploaded_file(file: &UploadedFile) -> Result<Self, UploadError> {
311 let path = file.as_file().path();
312 let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
313 Ok(Self {
314 name: file.original_name().to_string(),
315 size,
316 extension: file.original_extension(),
317 tmp_name: path.to_path_buf(),
318 error: file.error_code() as i32,
319 is_internal: false,
320 })
321 }
322
323 pub fn from_real_path<P: AsRef<Path>>(path: P, extension: &str) -> Result<Self, UploadError> {
327 let path = path.as_ref();
328 if !path.exists() {
329 return Err(UploadError::FileNotFound(
330 path.to_string_lossy().to_string(),
331 ));
332 }
333 let name = path
334 .file_name()
335 .map(|n| n.to_string_lossy().to_string())
336 .unwrap_or_default();
337 let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
338 Ok(Self {
339 name,
340 size,
341 extension: extension.to_string(),
342 tmp_name: path.to_path_buf(),
343 error: 0,
344 is_internal: true,
345 })
346 }
347}
348
349pub fn build_save_name(real_path: &Path, extension: &str) -> String {
369 let now = Local::now();
370 let ymd = now.format("%Y%m%d").to_string();
371 let ymd_his = now.format("%Y%m%d%H%M%S").to_string();
372
373 let mut md5 = Md5::new();
375 md5.update(real_path.to_string_lossy().as_bytes());
376 let md5_hex = hex::encode(md5.finalize());
377 let md5_prefix = &md5_hex[..5];
378
379 let rand_num: u32 = rand::thread_rng().gen_range(0..=9999);
381 let rand_padded = format!("{:04}", rand_num);
382
383 if extension.is_empty() {
386 format!("storage/{}/{}{}{}", ymd, ymd_his, md5_prefix, rand_padded)
387 } else {
388 format!(
389 "storage/{}/{}{}{}.{}",
390 ymd, ymd_his, md5_prefix, rand_padded, extension
391 )
392 }
393}
394
395pub fn build_internal_save_name(file_path: &Path) -> String {
404 let now = Local::now();
405 let ymd = now.format("%Y%m%d").to_string();
406 let basename = file_path
407 .file_name()
408 .map(|n| n.to_string_lossy().to_string())
409 .unwrap_or_default();
410 format!("storage/{}/{}", ymd, basename)
411}
412
413#[async_trait]
424pub trait StorageEngine: Send + Sync {
425 async fn upload(&mut self) -> Result<Option<String>, UploadError>;
432
433 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError>;
440
441 fn file_name(&self) -> Option<&str>;
443
444 fn file_info(&self) -> Option<&UploadFileInfo>;
446
447 fn error(&self) -> Option<&str>;
449
450 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError>;
455
456 fn set_upload_file_by_real(
460 &mut self,
461 file_path: &Path,
462 extension: &str,
463 ) -> Result<(), UploadError>;
464
465 fn is_internal(&self) -> bool;
467
468 fn real_path(&self) -> Option<&Path> {
474 self.file_info().map(|info| info.tmp_name.as_path())
475 }
476}
477
478pub struct LocalStorageEngine {
489 config: EngineConfig,
491 file_info: Option<UploadFileInfo>,
493 file_name: Option<String>,
495 error: Option<String>,
497 is_internal: bool,
499 upload_source_path: Option<PathBuf>,
501}
502
503impl LocalStorageEngine {
504 pub fn new(config: EngineConfig) -> Self {
506 Self {
507 config,
508 file_info: None,
509 file_name: None,
510 error: None,
511 is_internal: false,
512 upload_source_path: None,
513 }
514 }
515
516 pub fn upload_dir(&self) -> PathBuf {
521 if self.config.base_path.is_empty() {
522 PathBuf::from("uploads")
523 } else {
524 PathBuf::from(&self.config.base_path).join("uploads")
525 }
526 }
527
528 #[tracing::instrument(skip(self))]
543 async fn upload_by_internal(&mut self) -> Result<bool, UploadError> {
544 let target = self.upload_dir().join(
545 self.file_name
546 .as_ref()
547 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
548 .as_str(),
549 );
550 let real_path = self
551 .file_info
552 .as_ref()
553 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
554 .tmp_name
555 .clone();
556
557 if let Some(parent) = target.parent() {
559 tokio::fs::create_dir_all(parent).await?;
560 }
561 match tokio::fs::rename(&real_path, &target).await {
562 Ok(_) => Ok(true),
563 Err(e) => {
564 self.error = Some("upload write error".to_string());
566 Err(UploadError::MoveFailed {
567 from: real_path.to_string_lossy().to_string(),
568 to: target.to_string_lossy().to_string(),
569 error: e.to_string(),
570 })
571 }
572 }
573 }
574
575 #[tracing::instrument(skip(self))]
592 async fn upload_by_external(&mut self) -> Result<String, UploadError> {
593 let file_name = self
594 .file_name
595 .as_ref()
596 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
597 .clone();
598 let source = self
599 .upload_source_path
600 .as_ref()
601 .or_else(|| self.file_info.as_ref().map(|info| &info.tmp_name))
602 .cloned()
603 .ok_or_else(|| UploadError::UploadFailed("source path not set".to_string()))?;
604
605 let target = self.upload_dir().join(&file_name);
606 if let Some(parent) = target.parent() {
607 tokio::fs::create_dir_all(parent).await?;
608 }
609 tokio::fs::copy(&source, &target).await?;
612 Ok(file_name)
613 }
614}
615
616#[async_trait]
617impl StorageEngine for LocalStorageEngine {
618 #[tracing::instrument(skip(self))]
619 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
620 if self.is_internal {
622 self.upload_by_internal().await?;
624 Ok(None)
625 } else {
626 let save_name = self.upload_by_external().await?;
628 Ok(Some(save_name))
629 }
630 }
631
632 #[tracing::instrument(skip(self))]
633 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
634 if file_name.contains("..") || std::path::Path::new(file_name).is_absolute() {
641 return Err(UploadError::InvalidFileName(file_name.to_string()));
642 }
643
644 let file_path = self.upload_dir().join(file_name);
645
646 if let (Ok(upload_canon), Ok(file_canon)) = (
648 std::fs::canonicalize(self.upload_dir()),
649 std::fs::canonicalize(&file_path),
650 ) {
651 if !file_canon.starts_with(&upload_canon) {
652 return Err(UploadError::InvalidFileName(file_name.to_string()));
653 }
654 }
655
656 if !file_path.exists() {
657 return Ok(true);
659 }
660 match tokio::fs::remove_file(&file_path).await {
662 Ok(_) => Ok(true),
663 Err(e) => {
664 self.error = Some(e.to_string());
666 Ok(false)
667 }
668 }
669 }
670
671 fn file_name(&self) -> Option<&str> {
672 self.file_name.as_deref()
673 }
674
675 fn file_info(&self) -> Option<&UploadFileInfo> {
676 self.file_info.as_ref()
677 }
678
679 fn error(&self) -> Option<&str> {
680 self.error.as_deref()
681 }
682
683 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
684 let info = UploadFileInfo::from_uploaded_file(file)?;
688 let save_name = build_save_name(&info.tmp_name, &info.extension);
689 self.upload_source_path = Some(info.tmp_name.clone());
690 self.file_info = Some(info);
691 self.file_name = Some(save_name);
692 self.is_internal = false;
693 Ok(())
694 }
695
696 fn set_upload_file_by_real(
697 &mut self,
698 file_path: &Path,
699 extension: &str,
700 ) -> Result<(), UploadError> {
701 let info = UploadFileInfo::from_real_path(file_path, extension)?;
706 let save_name = build_internal_save_name(file_path);
707 self.file_info = Some(info);
708 self.file_name = Some(save_name);
709 self.is_internal = true;
710 Ok(())
711 }
712
713 fn is_internal(&self) -> bool {
714 self.is_internal
715 }
716}
717
718pub struct AliyunStorageEngine {
730 config: EngineConfig,
732 file_info: Option<UploadFileInfo>,
734 file_name: Option<String>,
736 error: Option<String>,
738 is_internal: bool,
740}
741
742impl AliyunStorageEngine {
743 pub fn new(config: EngineConfig) -> Self {
745 Self {
746 config,
747 file_info: None,
748 file_name: None,
749 error: None,
750 is_internal: false,
751 }
752 }
753
754 fn create_storage(&self) -> sz_orm_storage::AliyunOssStorage {
756 sz_orm_storage::AliyunOssStorage::new(
757 self.config.bucket.clone(),
758 self.config.endpoint.clone(),
759 )
760 }
761}
762
763#[async_trait]
764impl StorageEngine for AliyunStorageEngine {
765 #[tracing::instrument(skip(self))]
766 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
767 let file_name = self
769 .file_name
770 .as_ref()
771 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
772 .clone();
773 let info = self
774 .file_info
775 .as_ref()
776 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
777 .clone();
778
779 let storage = self.create_storage();
780 let data = tokio::fs::read(&info.tmp_name).await?;
781 let content_type = mime_guess::from_path(&info.tmp_name)
782 .first_or_octet_stream()
783 .to_string();
784
785 match storage.put(&file_name, &data, &content_type).await {
786 Ok(_) => {
787 Ok(Some(file_name))
789 }
790 Err(e) => {
791 let msg = e.to_string();
793 self.error = Some(msg.clone());
794 Err(UploadError::UploadFailed(msg))
795 }
796 }
797 }
798
799 #[tracing::instrument(skip(self))]
800 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
801 let storage = self.create_storage();
803 match storage.delete(file_name).await {
804 Ok(_) => Ok(true),
805 Err(e) => {
806 let msg = e.to_string();
807 self.error = Some(msg);
808 Ok(false)
809 }
810 }
811 }
812
813 fn file_name(&self) -> Option<&str> {
814 self.file_name.as_deref()
815 }
816
817 fn file_info(&self) -> Option<&UploadFileInfo> {
818 self.file_info.as_ref()
819 }
820
821 fn error(&self) -> Option<&str> {
822 self.error.as_deref()
823 }
824
825 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
826 let info = UploadFileInfo::from_uploaded_file(file)?;
827 let save_name = build_save_name(&info.tmp_name, &info.extension);
828 self.file_info = Some(info);
829 self.file_name = Some(save_name);
830 self.is_internal = false;
831 Ok(())
832 }
833
834 fn set_upload_file_by_real(
835 &mut self,
836 file_path: &Path,
837 extension: &str,
838 ) -> Result<(), UploadError> {
839 let info = UploadFileInfo::from_real_path(file_path, extension)?;
840 let save_name = build_internal_save_name(file_path);
841 self.file_info = Some(info);
842 self.file_name = Some(save_name);
843 self.is_internal = true;
844 Ok(())
845 }
846
847 fn is_internal(&self) -> bool {
848 self.is_internal
849 }
850}
851
852pub struct QcloudStorageEngine {
864 config: EngineConfig,
866 file_info: Option<UploadFileInfo>,
868 file_name: Option<String>,
870 error: Option<String>,
872 is_internal: bool,
874}
875
876impl QcloudStorageEngine {
877 pub fn new(config: EngineConfig) -> Self {
879 Self {
880 config,
881 file_info: None,
882 file_name: None,
883 error: None,
884 is_internal: false,
885 }
886 }
887
888 fn create_storage(&self) -> sz_orm_storage::TencentCosStorage {
890 sz_orm_storage::TencentCosStorage::new(
891 self.config.bucket.clone(),
892 self.config.region.clone(),
893 )
894 }
895}
896
897#[async_trait]
898impl StorageEngine for QcloudStorageEngine {
899 #[tracing::instrument(skip(self))]
900 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
901 let file_name = self
903 .file_name
904 .as_ref()
905 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
906 .clone();
907 let info = self
908 .file_info
909 .as_ref()
910 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
911 .clone();
912
913 let storage = self.create_storage();
914 let data = tokio::fs::read(&info.tmp_name).await?;
915 let content_type = mime_guess::from_path(&info.tmp_name)
916 .first_or_octet_stream()
917 .to_string();
918
919 match storage.put(&file_name, &data, &content_type).await {
920 Ok(_) => Ok(Some(file_name)),
921 Err(e) => {
922 let msg = e.to_string();
923 self.error = Some(msg.clone());
924 Err(UploadError::UploadFailed(msg))
925 }
926 }
927 }
928
929 #[tracing::instrument(skip(self))]
930 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
931 let storage = self.create_storage();
932 match storage.delete(file_name).await {
933 Ok(_) => Ok(true),
934 Err(e) => {
935 let msg = e.to_string();
936 self.error = Some(msg);
937 Ok(false)
938 }
939 }
940 }
941
942 fn file_name(&self) -> Option<&str> {
943 self.file_name.as_deref()
944 }
945
946 fn file_info(&self) -> Option<&UploadFileInfo> {
947 self.file_info.as_ref()
948 }
949
950 fn error(&self) -> Option<&str> {
951 self.error.as_deref()
952 }
953
954 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
955 let info = UploadFileInfo::from_uploaded_file(file)?;
956 let save_name = build_save_name(&info.tmp_name, &info.extension);
957 self.file_info = Some(info);
958 self.file_name = Some(save_name);
959 self.is_internal = false;
960 Ok(())
961 }
962
963 fn set_upload_file_by_real(
964 &mut self,
965 file_path: &Path,
966 extension: &str,
967 ) -> Result<(), UploadError> {
968 let info = UploadFileInfo::from_real_path(file_path, extension)?;
969 let save_name = build_internal_save_name(file_path);
970 self.file_info = Some(info);
971 self.file_name = Some(save_name);
972 self.is_internal = true;
973 Ok(())
974 }
975
976 fn is_internal(&self) -> bool {
977 self.is_internal
978 }
979}
980
981pub struct QiniuStorageEngine {
993 config: EngineConfig,
995 file_info: Option<UploadFileInfo>,
997 file_name: Option<String>,
999 error: Option<String>,
1001 is_internal: bool,
1003}
1004
1005impl QiniuStorageEngine {
1006 pub fn new(config: EngineConfig) -> Self {
1008 Self {
1009 config,
1010 file_info: None,
1011 file_name: None,
1012 error: None,
1013 is_internal: false,
1014 }
1015 }
1016
1017 fn create_storage(&self) -> sz_orm_storage::QiniuKodoStorage {
1019 sz_orm_storage::QiniuKodoStorage::new(self.config.bucket.clone())
1020 }
1021}
1022
1023#[async_trait]
1024impl StorageEngine for QiniuStorageEngine {
1025 #[tracing::instrument(skip(self))]
1026 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1027 let file_name = self
1029 .file_name
1030 .as_ref()
1031 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1032 .clone();
1033 let info = self
1034 .file_info
1035 .as_ref()
1036 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1037 .clone();
1038
1039 let storage = self.create_storage();
1040 let data = tokio::fs::read(&info.tmp_name).await?;
1041 let content_type = mime_guess::from_path(&info.tmp_name)
1042 .first_or_octet_stream()
1043 .to_string();
1044
1045 match storage.put(&file_name, &data, &content_type).await {
1046 Ok(_) => Ok(Some(file_name)),
1047 Err(e) => {
1048 let msg = e.to_string();
1049 self.error = Some(msg.clone());
1050 Err(UploadError::UploadFailed(msg))
1051 }
1052 }
1053 }
1054
1055 #[tracing::instrument(skip(self))]
1056 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1057 let storage = self.create_storage();
1058 match storage.delete(file_name).await {
1059 Ok(_) => Ok(true),
1060 Err(e) => {
1061 let msg = e.to_string();
1062 self.error = Some(msg);
1063 Ok(false)
1064 }
1065 }
1066 }
1067
1068 fn file_name(&self) -> Option<&str> {
1069 self.file_name.as_deref()
1070 }
1071
1072 fn file_info(&self) -> Option<&UploadFileInfo> {
1073 self.file_info.as_ref()
1074 }
1075
1076 fn error(&self) -> Option<&str> {
1077 self.error.as_deref()
1078 }
1079
1080 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1081 let info = UploadFileInfo::from_uploaded_file(file)?;
1082 let save_name = build_save_name(&info.tmp_name, &info.extension);
1083 self.file_info = Some(info);
1084 self.file_name = Some(save_name);
1085 self.is_internal = false;
1086 Ok(())
1087 }
1088
1089 fn set_upload_file_by_real(
1090 &mut self,
1091 file_path: &Path,
1092 extension: &str,
1093 ) -> Result<(), UploadError> {
1094 let info = UploadFileInfo::from_real_path(file_path, extension)?;
1095 let save_name = build_internal_save_name(file_path);
1096 self.file_info = Some(info);
1097 self.file_name = Some(save_name);
1098 self.is_internal = true;
1099 Ok(())
1100 }
1101
1102 fn is_internal(&self) -> bool {
1103 self.is_internal
1104 }
1105}
1106
1107pub struct S3StorageEngine {
1115 config: EngineConfig,
1117 file_info: Option<UploadFileInfo>,
1119 file_name: Option<String>,
1121 error: Option<String>,
1123 is_internal: bool,
1125}
1126
1127impl S3StorageEngine {
1128 pub fn new(config: EngineConfig) -> Self {
1130 Self {
1131 config,
1132 file_info: None,
1133 file_name: None,
1134 error: None,
1135 is_internal: false,
1136 }
1137 }
1138
1139 fn create_storage(&self) -> sz_orm_storage::S3Storage {
1141 sz_orm_storage::S3Storage::new(self.config.bucket.clone(), self.config.region.clone())
1142 }
1143}
1144
1145#[async_trait]
1146impl StorageEngine for S3StorageEngine {
1147 #[tracing::instrument(skip(self))]
1148 async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1149 let file_name = self
1150 .file_name
1151 .as_ref()
1152 .ok_or_else(|| UploadError::UploadFailed("file name not set".to_string()))?
1153 .clone();
1154 let info = self
1155 .file_info
1156 .as_ref()
1157 .ok_or_else(|| UploadError::UploadFailed("file info not set".to_string()))?
1158 .clone();
1159
1160 let storage = self.create_storage();
1161 let data = tokio::fs::read(&info.tmp_name).await?;
1162 let content_type = mime_guess::from_path(&info.tmp_name)
1163 .first_or_octet_stream()
1164 .to_string();
1165
1166 match storage.put(&file_name, &data, &content_type).await {
1167 Ok(_) => Ok(Some(file_name)),
1168 Err(e) => {
1169 let msg = e.to_string();
1170 self.error = Some(msg.clone());
1171 Err(UploadError::UploadFailed(msg))
1172 }
1173 }
1174 }
1175
1176 #[tracing::instrument(skip(self))]
1177 async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1178 let storage = self.create_storage();
1179 match storage.delete(file_name).await {
1180 Ok(_) => Ok(true),
1181 Err(e) => {
1182 let msg = e.to_string();
1183 self.error = Some(msg);
1184 Ok(false)
1185 }
1186 }
1187 }
1188
1189 fn file_name(&self) -> Option<&str> {
1190 self.file_name.as_deref()
1191 }
1192
1193 fn file_info(&self) -> Option<&UploadFileInfo> {
1194 self.file_info.as_ref()
1195 }
1196
1197 fn error(&self) -> Option<&str> {
1198 self.error.as_deref()
1199 }
1200
1201 fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1202 let info = UploadFileInfo::from_uploaded_file(file)?;
1203 let save_name = build_save_name(&info.tmp_name, &info.extension);
1204 self.file_info = Some(info);
1205 self.file_name = Some(save_name);
1206 self.is_internal = false;
1207 Ok(())
1208 }
1209
1210 fn set_upload_file_by_real(
1211 &mut self,
1212 file_path: &Path,
1213 extension: &str,
1214 ) -> Result<(), UploadError> {
1215 let info = UploadFileInfo::from_real_path(file_path, extension)?;
1216 let save_name = build_internal_save_name(file_path);
1217 self.file_info = Some(info);
1218 self.file_name = Some(save_name);
1219 self.is_internal = true;
1220 Ok(())
1221 }
1222
1223 fn is_internal(&self) -> bool {
1224 self.is_internal
1225 }
1226}
1227
1228pub enum StorageDriver {
1239 Local(LocalStorageEngine),
1241 Aliyun(AliyunStorageEngine),
1243 Qcloud(QcloudStorageEngine),
1245 Qiniu(QiniuStorageEngine),
1247 S3(S3StorageEngine),
1249}
1250
1251impl StorageDriver {
1252 pub fn new(kind: StorageEngineKind, config: EngineConfig) -> Self {
1262 match kind {
1263 StorageEngineKind::Local => Self::Local(LocalStorageEngine::new(config)),
1264 StorageEngineKind::Aliyun => Self::Aliyun(AliyunStorageEngine::new(config)),
1265 StorageEngineKind::Qcloud => Self::Qcloud(QcloudStorageEngine::new(config)),
1266 StorageEngineKind::Qiniu => Self::Qiniu(QiniuStorageEngine::new(config)),
1267 StorageEngineKind::S3 => Self::S3(S3StorageEngine::new(config)),
1268 }
1269 }
1270
1271 #[tracing::instrument(skip(self))]
1273 pub async fn upload(&mut self) -> Result<Option<String>, UploadError> {
1274 match self {
1275 Self::Local(e) => e.upload().await,
1276 Self::Aliyun(e) => e.upload().await,
1277 Self::Qcloud(e) => e.upload().await,
1278 Self::Qiniu(e) => e.upload().await,
1279 Self::S3(e) => e.upload().await,
1280 }
1281 }
1282
1283 #[tracing::instrument(skip(self))]
1285 pub async fn delete(&mut self, file_name: &str) -> Result<bool, UploadError> {
1286 match self {
1287 Self::Local(e) => e.delete(file_name).await,
1288 Self::Aliyun(e) => e.delete(file_name).await,
1289 Self::Qcloud(e) => e.delete(file_name).await,
1290 Self::Qiniu(e) => e.delete(file_name).await,
1291 Self::S3(e) => e.delete(file_name).await,
1292 }
1293 }
1294
1295 pub fn file_name(&self) -> Option<&str> {
1297 match self {
1298 Self::Local(e) => e.file_name(),
1299 Self::Aliyun(e) => e.file_name(),
1300 Self::Qcloud(e) => e.file_name(),
1301 Self::Qiniu(e) => e.file_name(),
1302 Self::S3(e) => e.file_name(),
1303 }
1304 }
1305
1306 pub fn file_info(&self) -> Option<&UploadFileInfo> {
1308 match self {
1309 Self::Local(e) => e.file_info(),
1310 Self::Aliyun(e) => e.file_info(),
1311 Self::Qcloud(e) => e.file_info(),
1312 Self::Qiniu(e) => e.file_info(),
1313 Self::S3(e) => e.file_info(),
1314 }
1315 }
1316
1317 pub fn error(&self) -> Option<&str> {
1319 match self {
1320 Self::Local(e) => e.error(),
1321 Self::Aliyun(e) => e.error(),
1322 Self::Qcloud(e) => e.error(),
1323 Self::Qiniu(e) => e.error(),
1324 Self::S3(e) => e.error(),
1325 }
1326 }
1327
1328 pub fn set_upload_file(&mut self, file: &UploadedFile) -> Result<(), UploadError> {
1330 match self {
1331 Self::Local(e) => e.set_upload_file(file),
1332 Self::Aliyun(e) => e.set_upload_file(file),
1333 Self::Qcloud(e) => e.set_upload_file(file),
1334 Self::Qiniu(e) => e.set_upload_file(file),
1335 Self::S3(e) => e.set_upload_file(file),
1336 }
1337 }
1338
1339 pub fn set_upload_file_by_real(
1341 &mut self,
1342 file_path: &Path,
1343 extension: &str,
1344 ) -> Result<(), UploadError> {
1345 match self {
1346 Self::Local(e) => e.set_upload_file_by_real(file_path, extension),
1347 Self::Aliyun(e) => e.set_upload_file_by_real(file_path, extension),
1348 Self::Qcloud(e) => e.set_upload_file_by_real(file_path, extension),
1349 Self::Qiniu(e) => e.set_upload_file_by_real(file_path, extension),
1350 Self::S3(e) => e.set_upload_file_by_real(file_path, extension),
1351 }
1352 }
1353
1354 pub fn is_internal(&self) -> bool {
1356 match self {
1357 Self::Local(e) => e.is_internal(),
1358 Self::Aliyun(e) => e.is_internal(),
1359 Self::Qcloud(e) => e.is_internal(),
1360 Self::Qiniu(e) => e.is_internal(),
1361 Self::S3(e) => e.is_internal(),
1362 }
1363 }
1364
1365 pub fn kind(&self) -> StorageEngineKind {
1367 match self {
1368 Self::Local(_) => StorageEngineKind::Local,
1369 Self::Aliyun(_) => StorageEngineKind::Aliyun,
1370 Self::Qcloud(_) => StorageEngineKind::Qcloud,
1371 Self::Qiniu(_) => StorageEngineKind::Qiniu,
1372 Self::S3(_) => StorageEngineKind::S3,
1373 }
1374 }
1375}
1376
1377#[cfg(test)]
1382mod tests {
1383 use super::*;
1384 use std::io::Write;
1385
1386 fn create_temp_file(name: &str, content: &[u8]) -> std::path::PathBuf {
1392 let dir = std::env::temp_dir().join(format!(
1393 "sz_rust_storage_test_{}",
1394 std::time::SystemTime::now()
1395 .duration_since(std::time::UNIX_EPOCH)
1396 .unwrap_or_default()
1397 .as_nanos()
1398 ));
1399 std::fs::create_dir_all(&dir).unwrap();
1400 let path = dir.join(name);
1401 let mut f = std::fs::File::create(&path).unwrap();
1402 f.write_all(content).unwrap();
1403 path
1404 }
1405
1406 fn create_temp_base_path() -> std::path::PathBuf {
1408 let dir = std::env::temp_dir().join(format!(
1409 "sz_rust_storage_base_{}",
1410 std::time::SystemTime::now()
1411 .duration_since(std::time::UNIX_EPOCH)
1412 .unwrap_or_default()
1413 .as_nanos()
1414 ));
1415 std::fs::create_dir_all(&dir).unwrap();
1416 dir
1417 }
1418
1419 #[test]
1424 fn test_storage_engine_kind_parse_local() {
1425 assert_eq!(
1426 StorageEngineKind::parse("local"),
1427 Some(StorageEngineKind::Local)
1428 );
1429 }
1430
1431 #[test]
1432 fn test_storage_engine_kind_parse_aliyun() {
1433 assert_eq!(
1434 StorageEngineKind::parse("aliyun"),
1435 Some(StorageEngineKind::Aliyun)
1436 );
1437 }
1438
1439 #[test]
1440 fn test_storage_engine_kind_parse_qcloud() {
1441 assert_eq!(
1442 StorageEngineKind::parse("qcloud"),
1443 Some(StorageEngineKind::Qcloud)
1444 );
1445 }
1446
1447 #[test]
1448 fn test_storage_engine_kind_parse_qiniu() {
1449 assert_eq!(
1450 StorageEngineKind::parse("qiniu"),
1451 Some(StorageEngineKind::Qiniu)
1452 );
1453 }
1454
1455 #[test]
1456 fn test_storage_engine_kind_parse_s3() {
1457 assert_eq!(StorageEngineKind::parse("s3"), Some(StorageEngineKind::S3));
1458 }
1459
1460 #[test]
1461 fn test_storage_engine_kind_parse_case_insensitive() {
1462 assert_eq!(
1464 StorageEngineKind::parse("LOCAL"),
1465 Some(StorageEngineKind::Local)
1466 );
1467 assert_eq!(
1468 StorageEngineKind::parse("Aliyun"),
1469 Some(StorageEngineKind::Aliyun)
1470 );
1471 }
1472
1473 #[test]
1474 fn test_storage_engine_kind_parse_invalid() {
1475 assert_eq!(StorageEngineKind::parse("invalid"), None);
1476 assert_eq!(StorageEngineKind::parse(""), None);
1477 }
1478
1479 #[test]
1480 fn test_storage_engine_kind_as_str() {
1481 assert_eq!(StorageEngineKind::Local.as_str(), "local");
1482 assert_eq!(StorageEngineKind::Aliyun.as_str(), "aliyun");
1483 assert_eq!(StorageEngineKind::Qcloud.as_str(), "qcloud");
1484 assert_eq!(StorageEngineKind::Qiniu.as_str(), "qiniu");
1485 assert_eq!(StorageEngineKind::S3.as_str(), "s3");
1486 }
1487
1488 #[test]
1493 fn test_engine_config_builder() {
1494 let config = EngineConfig::new()
1495 .with_bucket("my-bucket")
1496 .with_region("us-east-1")
1497 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1498 .with_domain("https://cdn.example.com")
1499 .with_base_path("/var/www/uploads")
1500 .with_access_key_id("akid")
1501 .with_access_key_secret("aksecret")
1502 .with_secret_id("sid")
1503 .with_secret_key("sk")
1504 .with_access_key("ak");
1505 assert_eq!(config.bucket, "my-bucket");
1506 assert_eq!(config.region, "us-east-1");
1507 assert_eq!(config.endpoint, "oss-cn-hangzhou.aliyuncs.com");
1508 assert_eq!(config.domain, "https://cdn.example.com");
1509 assert_eq!(config.base_path, "/var/www/uploads");
1510 assert_eq!(config.access_key_id, "akid");
1511 assert_eq!(config.access_key_secret, "aksecret");
1512 assert_eq!(config.secret_id, "sid");
1513 assert_eq!(config.secret_key, "sk");
1514 assert_eq!(config.access_key, "ak");
1515 }
1516
1517 #[test]
1518 fn test_engine_config_default() {
1519 let config = EngineConfig::default();
1520 assert!(config.bucket.is_empty());
1521 assert!(config.region.is_empty());
1522 assert!(config.endpoint.is_empty());
1523 assert!(config.domain.is_empty());
1524 assert!(config.base_path.is_empty());
1525 }
1526
1527 #[test]
1532 fn test_build_save_name_format() {
1533 let path = Path::new("/tmp/photo.jpg");
1534 let name = build_save_name(path, "jpg");
1535 assert!(name.starts_with("storage/"), "name = {}", name);
1537 assert!(name.ends_with(".jpg"), "name = {}", name);
1538 let parts: Vec<&str> = name.split('/').collect();
1540 assert_eq!(parts.len(), 3, "name = {}", name);
1541 let ymd_part = parts[1];
1542 assert_eq!(ymd_part.len(), 8, "Ymd should be 8 chars");
1543 assert!(ymd_part.chars().all(|c| c.is_ascii_digit()));
1544 }
1545
1546 #[test]
1547 fn test_build_save_name_md5_prefix_consistent() {
1548 let path = Path::new("/tmp/test.png");
1550 let name1 = build_save_name(path, "png");
1551 let name2 = build_save_name(path, "png");
1552 let extract_md5 = |s: &str| -> String {
1556 let parts: Vec<&str> = s.split('/').collect();
1557 if parts.len() != 3 {
1558 return String::new();
1559 }
1560 let last = parts[2];
1561 let last = last.rsplit_once('.').map(|(l, _)| l).unwrap_or(last);
1563 if last.len() > 14 {
1565 last[14..19].to_string()
1566 } else {
1567 String::new()
1568 }
1569 };
1570 let md5_1 = extract_md5(&name1);
1571 let md5_2 = extract_md5(&name2);
1572 assert_eq!(
1573 md5_1, md5_2,
1574 "md5 prefix should be consistent for same path"
1575 );
1576 assert_eq!(md5_1.len(), 5, "md5 prefix should be 5 chars");
1577 }
1578
1579 #[test]
1580 fn test_build_save_name_empty_extension() {
1581 let path = Path::new("/tmp/noext");
1582 let name = build_save_name(path, "");
1583 assert!(!name.ends_with('.'), "name = {}", name);
1585 assert!(name.starts_with("storage/"));
1586 }
1587
1588 #[test]
1589 fn test_build_internal_save_name_format() {
1590 let path = Path::new("/tmp/photo.jpg");
1591 let name = build_internal_save_name(path);
1592 assert!(name.starts_with("storage/"), "name = {}", name);
1594 assert!(name.ends_with("photo.jpg"), "name = {}", name);
1595 let parts: Vec<&str> = name.split('/').collect();
1596 assert_eq!(parts.len(), 3, "name = {}", name);
1597 assert_eq!(parts[1].len(), 8);
1599 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
1600 }
1601
1602 #[test]
1607 fn test_upload_file_info_from_real_path() {
1608 let path = create_temp_file("test.txt", b"hello world");
1609 let info = UploadFileInfo::from_real_path(&path, "txt").unwrap();
1610 assert_eq!(info.name, "test.txt");
1611 assert_eq!(info.size, 11);
1612 assert_eq!(info.extension, "txt");
1613 assert_eq!(info.tmp_name, path);
1614 assert_eq!(info.error, 0);
1615 assert!(info.is_internal);
1616 std::fs::remove_file(&path).ok();
1617 }
1618
1619 #[test]
1620 fn test_upload_file_info_from_real_path_not_found() {
1621 let result = UploadFileInfo::from_real_path("/nonexistent/path.txt", "txt");
1622 assert!(result.is_err());
1623 }
1624
1625 #[tokio::test]
1630 async fn test_local_storage_engine_new() {
1631 let config = EngineConfig::new().with_base_path("/tmp/test");
1632 let engine = LocalStorageEngine::new(config);
1633 assert!(!engine.is_internal());
1634 assert!(engine.file_name().is_none());
1635 assert!(engine.file_info().is_none());
1636 assert!(engine.error().is_none());
1637 }
1638
1639 #[tokio::test]
1640 async fn test_local_storage_upload_dir_default() {
1641 let config = EngineConfig::new();
1642 let engine = LocalStorageEngine::new(config);
1643 assert_eq!(engine.upload_dir(), PathBuf::from("uploads"));
1644 }
1645
1646 #[tokio::test]
1647 async fn test_local_storage_upload_dir_with_base_path() {
1648 let base = create_temp_base_path();
1649 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1650 let engine = LocalStorageEngine::new(config);
1651 let expected = base.join("uploads");
1652 assert_eq!(engine.upload_dir(), expected);
1653 std::fs::remove_dir_all(&base).ok();
1654 }
1655
1656 #[tokio::test]
1657 async fn test_local_storage_set_upload_file_by_real() {
1658 let path = create_temp_file("internal.txt", b"internal data");
1659 let base = create_temp_base_path();
1660 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1661 let mut engine = LocalStorageEngine::new(config);
1662
1663 engine
1664 .set_upload_file_by_real(&path, "txt")
1665 .expect("set_upload_file_by_real failed");
1666
1667 assert!(engine.is_internal());
1669 let file_name = engine.file_name().expect("file name should be set");
1670 assert!(file_name.starts_with("storage/"));
1671 assert!(file_name.ends_with("internal.txt"));
1672
1673 let info = engine.file_info().expect("file info should be set");
1674 assert_eq!(info.name, "internal.txt");
1675 assert!(info.is_internal);
1676
1677 std::fs::remove_file(&path).ok();
1678 std::fs::remove_dir_all(&base).ok();
1679 }
1680
1681 #[tokio::test]
1682 async fn test_local_storage_upload_internal_uses_rename() {
1683 let path = create_temp_file("rename_test.txt", b"rename me");
1684 let base = create_temp_base_path();
1685 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1686 let mut engine = LocalStorageEngine::new(config);
1687
1688 engine
1689 .set_upload_file_by_real(&path, "txt")
1690 .expect("set_upload_file_by_real failed");
1691 let file_name = engine.file_name().unwrap().to_string();
1692
1693 let result = engine.upload().await;
1695 assert!(result.is_ok(), "upload should succeed");
1696 assert!(result.unwrap().is_none());
1698
1699 assert!(!path.exists(), "original file should be renamed away");
1701 let target = engine.upload_dir().join(&file_name);
1703 assert!(target.exists(), "target file should exist: {:?}", target);
1704
1705 std::fs::remove_dir_all(&base).ok();
1706 }
1707
1708 #[tokio::test]
1709 async fn test_local_storage_upload_external_returns_save_name() {
1710 let path = create_temp_file("external.txt", b"external data");
1711 let base = create_temp_base_path();
1712 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1713 let mut engine = LocalStorageEngine::new(config);
1714
1715 let file = UploadedFile::new(&path, "external.txt", Some("text/plain"), Some(0), true)
1717 .expect("UploadedFile::new failed");
1718 engine
1719 .set_upload_file(&file)
1720 .expect("set_upload_file failed");
1721
1722 let result = engine.upload().await;
1723 assert!(result.is_ok(), "upload should succeed");
1724 let save_name = result
1725 .unwrap()
1726 .expect("external upload should return save name");
1727 assert!(save_name.starts_with("storage/"));
1728
1729 assert!(path.exists(), "source file should be preserved");
1731
1732 let target = engine.upload_dir().join(&save_name);
1734 assert!(target.exists(), "target file should exist: {:?}", target);
1735
1736 std::fs::remove_file(&path).ok();
1737 std::fs::remove_dir_all(&base).ok();
1738 }
1739
1740 #[tokio::test]
1741 async fn test_local_storage_delete_existing_file() {
1742 let base = create_temp_base_path();
1743 let uploads_dir = base.join("uploads").join("storage").join("20260101");
1744 std::fs::create_dir_all(&uploads_dir).unwrap();
1745 let file_path = uploads_dir.join("delete_me.txt");
1746 std::fs::write(&file_path, b"delete me").unwrap();
1747
1748 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1749 let mut engine = LocalStorageEngine::new(config);
1750
1751 let result = engine.delete("storage/20260101/delete_me.txt").await;
1753 assert!(result.is_ok());
1754 assert!(result.unwrap(), "delete should return true");
1755 assert!(!file_path.exists(), "file should be deleted");
1756
1757 std::fs::remove_dir_all(&base).ok();
1758 }
1759
1760 #[tokio::test]
1761 async fn test_local_storage_delete_nonexistent_file() {
1762 let base = create_temp_base_path();
1763 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1764 let mut engine = LocalStorageEngine::new(config);
1765
1766 let result = engine.delete("nonexistent.txt").await;
1768 assert!(result.is_ok());
1769 assert!(result.unwrap(), "delete nonexistent should return true");
1770
1771 std::fs::remove_dir_all(&base).ok();
1772 }
1773
1774 #[test]
1779 fn test_aliyun_storage_engine_new() {
1780 let config = EngineConfig::new()
1781 .with_bucket("aliyun-bucket")
1782 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
1783 .with_access_key_id("akid")
1784 .with_access_key_secret("aksecret");
1785 let engine = AliyunStorageEngine::new(config);
1786 assert!(!engine.is_internal());
1787 assert!(engine.file_name().is_none());
1788 }
1789
1790 #[test]
1791 fn test_qcloud_storage_engine_new() {
1792 let config = EngineConfig::new()
1793 .with_bucket("cos-bucket")
1794 .with_region("ap-guangzhou")
1795 .with_secret_id("sid")
1796 .with_secret_key("sk");
1797 let engine = QcloudStorageEngine::new(config);
1798 assert!(!engine.is_internal());
1799 }
1800
1801 #[test]
1802 fn test_qiniu_storage_engine_new() {
1803 let config = EngineConfig::new()
1804 .with_bucket("qiniu-bucket")
1805 .with_access_key("ak")
1806 .with_secret_key("sk");
1807 let engine = QiniuStorageEngine::new(config);
1808 assert!(!engine.is_internal());
1809 }
1810
1811 #[test]
1812 fn test_s3_storage_engine_new() {
1813 let config = EngineConfig::new()
1814 .with_bucket("s3-bucket")
1815 .with_region("us-east-1");
1816 let engine = S3StorageEngine::new(config);
1817 assert!(!engine.is_internal());
1818 }
1819
1820 #[test]
1825 fn test_aliyun_set_upload_file_by_real() {
1826 let path = create_temp_file("aliyun.txt", b"aliyun");
1827 let config = EngineConfig::new()
1828 .with_bucket("bucket")
1829 .with_endpoint("endpoint");
1830 let mut engine = AliyunStorageEngine::new(config);
1831 engine.set_upload_file_by_real(&path, "txt").unwrap();
1832 assert!(engine.is_internal());
1833 assert!(engine.file_name().unwrap().starts_with("storage/"));
1834 std::fs::remove_file(&path).ok();
1835 }
1836
1837 #[test]
1838 fn test_qcloud_set_upload_file_by_real() {
1839 let path = create_temp_file("qcloud.txt", b"qcloud");
1840 let config = EngineConfig::new()
1841 .with_bucket("bucket")
1842 .with_region("region");
1843 let mut engine = QcloudStorageEngine::new(config);
1844 engine.set_upload_file_by_real(&path, "txt").unwrap();
1845 assert!(engine.is_internal());
1846 std::fs::remove_file(&path).ok();
1847 }
1848
1849 #[test]
1850 fn test_qiniu_set_upload_file_by_real() {
1851 let path = create_temp_file("qiniu.txt", b"qiniu");
1852 let config = EngineConfig::new().with_bucket("bucket");
1853 let mut engine = QiniuStorageEngine::new(config);
1854 engine.set_upload_file_by_real(&path, "txt").unwrap();
1855 assert!(engine.is_internal());
1856 std::fs::remove_file(&path).ok();
1857 }
1858
1859 #[test]
1860 fn test_s3_set_upload_file_by_real() {
1861 let path = create_temp_file("s3.txt", b"s3");
1862 let config = EngineConfig::new()
1863 .with_bucket("bucket")
1864 .with_region("region");
1865 let mut engine = S3StorageEngine::new(config);
1866 engine.set_upload_file_by_real(&path, "txt").unwrap();
1867 assert!(engine.is_internal());
1868 std::fs::remove_file(&path).ok();
1869 }
1870
1871 #[test]
1876 fn test_storage_driver_new_local() {
1877 let config = EngineConfig::new();
1878 let driver = StorageDriver::new(StorageEngineKind::Local, config);
1879 assert_eq!(driver.kind(), StorageEngineKind::Local);
1880 assert!(!driver.is_internal());
1881 }
1882
1883 #[test]
1884 fn test_storage_driver_new_aliyun() {
1885 let config = EngineConfig::new();
1886 let driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1887 assert_eq!(driver.kind(), StorageEngineKind::Aliyun);
1888 }
1889
1890 #[test]
1891 fn test_storage_driver_new_qcloud() {
1892 let config = EngineConfig::new();
1893 let driver = StorageDriver::new(StorageEngineKind::Qcloud, config);
1894 assert_eq!(driver.kind(), StorageEngineKind::Qcloud);
1895 }
1896
1897 #[test]
1898 fn test_storage_driver_new_qiniu() {
1899 let config = EngineConfig::new();
1900 let driver = StorageDriver::new(StorageEngineKind::Qiniu, config);
1901 assert_eq!(driver.kind(), StorageEngineKind::Qiniu);
1902 }
1903
1904 #[test]
1905 fn test_storage_driver_new_s3() {
1906 let config = EngineConfig::new();
1907 let driver = StorageDriver::new(StorageEngineKind::S3, config);
1908 assert_eq!(driver.kind(), StorageEngineKind::S3);
1909 }
1910
1911 #[tokio::test]
1916 async fn test_storage_driver_local_upload_and_delete() {
1917 let path = create_temp_file("driver.txt", b"driver data");
1918 let base = create_temp_base_path();
1919 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
1920 let mut driver = StorageDriver::new(StorageEngineKind::Local, config);
1921
1922 driver
1923 .set_upload_file_by_real(&path, "txt")
1924 .expect("set_upload_file_by_real failed");
1925 let file_name = driver.file_name().unwrap().to_string();
1926
1927 let result = driver.upload().await;
1928 assert!(result.is_ok());
1929 assert!(result.unwrap().is_none());
1930
1931 let del_result = driver.delete(&file_name).await;
1933 assert!(del_result.is_ok());
1934 assert!(del_result.unwrap());
1935
1936 std::fs::remove_file(&path).ok();
1937 std::fs::remove_dir_all(&base).ok();
1938 }
1939
1940 #[tokio::test]
1941 async fn test_storage_driver_aliyun_upload() {
1942 let path = create_temp_file("aliyun_driver.txt", b"aliyun data");
1943 let config = EngineConfig::new()
1944 .with_bucket("test-bucket")
1945 .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
1946 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1947
1948 driver
1949 .set_upload_file_by_real(&path, "txt")
1950 .expect("set_upload_file_by_real failed");
1951 let result = driver.upload().await;
1952 assert!(result.is_ok(), "upload should succeed");
1953 assert!(result.unwrap().is_some());
1955
1956 std::fs::remove_file(&path).ok();
1957 }
1958
1959 #[tokio::test]
1960 async fn test_storage_driver_error_after_failed_delete() {
1961 let config = EngineConfig::new()
1963 .with_bucket("bucket")
1964 .with_endpoint("endpoint");
1965 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
1966 let result = driver.delete("nonexistent.txt").await;
1967 assert!(result.is_ok());
1968 }
1969
1970 #[test]
1976 fn test_r5_16_build_save_name_format() {
1977 let path = Path::new("/tmp/r5_16_test.jpg");
1978 let name = build_save_name(path, "jpg");
1979
1980 assert!(name.starts_with("storage/"), "name = {}", name);
1982 assert!(name.ends_with(".jpg"), "name = {}", name);
1983
1984 let parts: Vec<&str> = name.split('/').collect();
1985 assert_eq!(parts.len(), 3, "should have 3 parts: storage, Ymd, rest");
1986
1987 let ymd = parts[1];
1989 assert_eq!(ymd.len(), 8, "Ymd should be 8 digits");
1990 assert!(ymd.chars().all(|c| c.is_ascii_digit()));
1991
1992 let last = parts[2];
1994 assert_eq!(
1995 last.len(),
1996 27,
1997 "last part length = {}, last = {}",
1998 last.len(),
1999 last
2000 );
2001 }
2002
2003 #[test]
2005 fn test_r5_17_internal_save_name_uses_basename() {
2006 let path = Path::new("/tmp/r5_17_test.png");
2007 let name = build_internal_save_name(path);
2008
2009 assert!(name.starts_with("storage/"));
2011 assert!(name.ends_with("r5_17_test.png"));
2012
2013 let parts: Vec<&str> = name.split('/').collect();
2014 assert_eq!(parts.len(), 3, "should have 3 parts");
2015 assert_eq!(parts[1].len(), 8);
2017 assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
2018 assert_eq!(parts[2], "r5_17_test.png");
2020 }
2021
2022 #[tokio::test]
2024 async fn test_r5_18_real_path_internal_vs_external() {
2025 let internal_path = create_temp_file("internal.txt", b"internal");
2026 let external_path = create_temp_file("external.txt", b"external");
2027 let base = create_temp_base_path();
2028 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2029
2030 let mut engine = LocalStorageEngine::new(config.clone());
2032 engine
2033 .set_upload_file_by_real(&internal_path, "txt")
2034 .unwrap();
2035 assert!(engine.is_internal());
2036 let real_path = engine.real_path().expect("real_path should be set");
2037 assert_eq!(real_path, internal_path);
2038
2039 let mut engine2 = LocalStorageEngine::new(config);
2041 let file = UploadedFile::new(
2042 &external_path,
2043 "external.txt",
2044 Some("text/plain"),
2045 Some(0),
2046 true,
2047 )
2048 .expect("UploadedFile::new failed");
2049 engine2.set_upload_file(&file).unwrap();
2050 assert!(!engine2.is_internal());
2051 let real_path = engine2.real_path().expect("real_path should be set");
2052 assert_eq!(real_path, external_path);
2053
2054 std::fs::remove_file(&internal_path).ok();
2055 std::fs::remove_file(&external_path).ok();
2056 std::fs::remove_dir_all(&base).ok();
2057 }
2058
2059 #[tokio::test]
2061 async fn test_r5_19_upload_internal_rename_external_copy() {
2062 let internal_path = create_temp_file("r5_19_internal.txt", b"internal");
2063 let external_path = create_temp_file("r5_19_external.txt", b"external");
2064 let base = create_temp_base_path();
2065 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2066
2067 let mut engine = LocalStorageEngine::new(config.clone());
2069 engine
2070 .set_upload_file_by_real(&internal_path, "txt")
2071 .unwrap();
2072 engine.upload().await.expect("internal upload failed");
2073 assert!(!internal_path.exists(), "rename should move file away");
2074
2075 let mut engine2 = LocalStorageEngine::new(config);
2077 let file = UploadedFile::new(
2078 &external_path,
2079 "r5_19_external.txt",
2080 Some("text/plain"),
2081 Some(0),
2082 true,
2083 )
2084 .expect("UploadedFile::new failed");
2085 engine2.set_upload_file(&file).unwrap();
2086 engine2.upload().await.expect("external upload failed");
2087 assert!(external_path.exists(), "copy should preserve source");
2088
2089 std::fs::remove_file(&internal_path).ok();
2090 std::fs::remove_file(&external_path).ok();
2091 std::fs::remove_dir_all(&base).ok();
2092 }
2093
2094 #[tokio::test]
2096 async fn test_r5_20_delete_elvis_short_circuit() {
2097 let base = create_temp_base_path();
2098 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2099 let mut engine = LocalStorageEngine::new(config);
2100
2101 let result = engine.delete("nonexistent_file.txt").await;
2103 assert!(result.is_ok());
2104 assert!(result.unwrap(), "delete nonexistent should return true");
2105
2106 let uploads_dir = base.join("uploads").join("storage").join("20260101");
2108 std::fs::create_dir_all(&uploads_dir).unwrap();
2109 let file_path = uploads_dir.join("exists.txt");
2110 std::fs::write(&file_path, b"exists").unwrap();
2111
2112 let result = engine.delete("storage/20260101/exists.txt").await;
2113 assert!(result.is_ok());
2114 assert!(result.unwrap(), "delete existing should return true");
2115 assert!(!file_path.exists(), "file should be deleted");
2116
2117 std::fs::remove_dir_all(&base).ok();
2118 }
2119
2120 #[tokio::test]
2122 async fn test_r5_21_internal_upload_failure_sets_error() {
2123 let nonexistent_path = std::env::temp_dir().join(format!(
2125 "nonexistent_{}.txt",
2126 std::time::SystemTime::now()
2127 .duration_since(std::time::UNIX_EPOCH)
2128 .unwrap_or_default()
2129 .as_nanos()
2130 ));
2131 let path = create_temp_file("will_fail.txt", b"will fail");
2134 let base = create_temp_base_path();
2135 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2137 let mut engine = LocalStorageEngine::new(config);
2138
2139 engine
2140 .set_upload_file_by_real(&path, "txt")
2141 .expect("set_upload_file_by_real failed");
2142
2143 std::fs::remove_file(&path).unwrap();
2145
2146 let result = engine.upload().await;
2147 assert!(result.is_err(), "upload should fail when source missing");
2149 let error_msg = engine.error().unwrap_or("");
2151 assert!(
2152 error_msg.contains("upload write error"),
2153 "error should contain 'upload write error', got: {}",
2154 error_msg
2155 );
2156
2157 let _ = nonexistent_path;
2158 std::fs::remove_dir_all(&base).ok();
2159 }
2160
2161 #[tokio::test]
2163 async fn test_r5_22_cloud_upload_returns_save_name() {
2164 let path = create_temp_file("r5_22.txt", b"cloud upload");
2165 let config = EngineConfig::new()
2166 .with_bucket("r5_22_bucket")
2167 .with_endpoint("oss-cn-hangzhou.aliyuncs.com");
2168 let mut driver = StorageDriver::new(StorageEngineKind::Aliyun, config);
2169
2170 driver
2171 .set_upload_file_by_real(&path, "txt")
2172 .expect("set_upload_file_by_real failed");
2173 let file_name = driver.file_name().unwrap().to_string();
2174
2175 let result = driver.upload().await;
2176 assert!(result.is_ok(), "upload should succeed");
2177 let returned = result.unwrap().expect("cloud upload should return Some");
2178 assert_eq!(returned, file_name, "should return save name");
2179
2180 std::fs::remove_file(&path).ok();
2181 }
2182
2183 #[tokio::test]
2185 async fn test_r5_23_local_external_upload_returns_save_name() {
2186 let path = create_temp_file("r5_23.txt", b"external save name");
2187 let base = create_temp_base_path();
2188 let config = EngineConfig::new().with_base_path(base.to_string_lossy().to_string());
2189 let mut engine = LocalStorageEngine::new(config);
2190
2191 let file = UploadedFile::new(&path, "r5_23.txt", Some("text/plain"), Some(0), true)
2192 .expect("UploadedFile::new failed");
2193 engine.set_upload_file(&file).unwrap();
2194 let expected_name = engine.file_name().unwrap().to_string();
2195
2196 let result = engine.upload().await;
2197 assert!(result.is_ok(), "upload should succeed");
2198 let save_name = result
2199 .unwrap()
2200 .expect("external upload should return save name");
2201 assert_eq!(save_name, expected_name, "should return save name");
2202
2203 std::fs::remove_file(&path).ok();
2204 std::fs::remove_dir_all(&base).ok();
2205 }
2206}