1use std::collections::HashMap;
75
76use std::path::{Path, PathBuf};
77
78use chrono::Local;
79use md5::{Digest, Md5};
80
81pub mod image;
86pub mod storage;
87pub mod validate;
88
89#[derive(Debug, thiserror::Error)]
95pub enum UploadError {
96 #[error("The file \"{0}\" does not exist")]
98 FileNotFound(String),
99
100 #[error("Could not move the file \"{from}\" to \"{to}\" ({error})")]
102 MoveFailed {
103 from: String,
105 to: String,
107 error: String,
109 },
110
111 #[error("Unable to create the \"{0}\" directory")]
113 DirectoryCreateFailed(String),
114
115 #[error("Unable to write in the \"{0}\" directory")]
117 DirectoryNotWritable(String),
118
119 #[error("{0}")]
121 UploadFailed(String),
122
123 #[error(transparent)]
125 Io(#[from] std::io::Error),
126
127 #[error(transparent)]
129 Persist(#[from] tempfile::PersistError),
130
131 #[error("Invalid file name \"{0}\" — potential path traversal attack")]
133 InvalidFileName(String),
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144#[repr(i32)]
145pub enum UploadErrCode {
146 Ok = 0,
148 IniSize = 1,
150 FormSize = 2,
152 Partial = 3,
154 NoFile = 4,
156 NoTmpDir = 6,
158 CantWrite = 7,
160}
161
162impl UploadErrCode {
163 pub fn error_message(self) -> &'static str {
173 match self {
174 UploadErrCode::IniSize | UploadErrCode::FormSize => {
175 "upload File size exceeds the maximum value"
176 }
177 UploadErrCode::Partial => "only the portion of file is uploaded",
178 UploadErrCode::NoFile => "no file to uploaded",
179 UploadErrCode::NoTmpDir => "upload temp dir not found",
180 UploadErrCode::CantWrite => "file write error",
181 UploadErrCode::Ok => "unknown upload error",
182 }
183 }
184
185 pub fn from_i32(code: i32) -> Self {
187 match code {
188 0 => UploadErrCode::Ok,
189 1 => UploadErrCode::IniSize,
190 2 => UploadErrCode::FormSize,
191 3 => UploadErrCode::Partial,
192 4 => UploadErrCode::NoFile,
193 6 => UploadErrCode::NoTmpDir,
194 7 => UploadErrCode::CantWrite,
195 _ => UploadErrCode::Ok,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum HashAlgo {
207 Md5,
209 Sha1,
211 Sha256,
213 Sha512,
215}
216
217impl HashAlgo {
218 pub fn as_str(self) -> &'static str {
220 match self {
221 HashAlgo::Md5 => "md5",
222 HashAlgo::Sha1 => "sha1",
223 HashAlgo::Sha256 => "sha256",
224 HashAlgo::Sha512 => "sha512",
225 }
226 }
227
228 pub fn parse_algo(s: &str) -> Option<Self> {
233 match s {
234 "md5" => Some(HashAlgo::Md5),
235 "sha1" => Some(HashAlgo::Sha1),
236 "sha256" => Some(HashAlgo::Sha256),
237 "sha512" => Some(HashAlgo::Sha512),
238 _ => None,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
259pub enum HashNameRule {
260 #[default]
264 Default,
265
266 Hash(HashAlgo),
270}
271
272#[derive(Debug, Clone)]
293pub struct File {
294 path: PathBuf,
296 hash: HashMap<String, String>,
298 hash_name: Option<String>,
300 extension: Option<String>,
302}
303
304impl File {
305 pub fn new<P: AsRef<Path>>(path: P, check_path: bool) -> Result<Self, UploadError> {
309 let path = path.as_ref().to_path_buf();
310 if check_path && !path.is_file() {
311 return Err(UploadError::FileNotFound(
312 path.to_string_lossy().to_string(),
313 ));
314 }
315 Ok(Self {
316 path,
317 hash: HashMap::new(),
318 hash_name: None,
319 extension: None,
320 })
321 }
322
323 pub fn new_unchecked<P: AsRef<Path>>(path: P) -> Self {
325 Self {
326 path: path.as_ref().to_path_buf(),
327 hash: HashMap::new(),
328 hash_name: None,
329 extension: None,
330 }
331 }
332
333 pub fn path(&self) -> &Path {
335 &self.path
336 }
337
338 pub fn path_name(&self) -> String {
340 self.path.to_string_lossy().to_string()
341 }
342
343 pub async fn hash(&mut self, algo: HashAlgo) -> Result<String, UploadError> {
355 let key = algo.as_str().to_string();
356 if let Some(h) = self.hash.get(&key) {
357 return Ok(h.clone());
358 }
359 let h = compute_file_hash(&self.path, algo).await?;
360 self.hash.insert(key, h.clone());
361 Ok(h)
362 }
363
364 pub async fn md5(&mut self) -> Result<String, UploadError> {
366 self.hash(HashAlgo::Md5).await
367 }
368
369 pub async fn sha1(&mut self) -> Result<String, UploadError> {
371 self.hash(HashAlgo::Sha1).await
372 }
373
374 pub fn get_mime(&self) -> Result<String, UploadError> {
379 if let Ok(Some(t)) = infer::get_from_path(&self.path) {
381 return Ok(t.mime_type().to_string());
382 }
383 let mime = mime_guess::from_path(&self.path)
385 .first_or_octet_stream()
386 .to_string();
387 Ok(mime)
388 }
389
390 pub async fn move_to<P: AsRef<Path>>(
399 &mut self,
400 directory: P,
401 name: Option<&str>,
402 ) -> Result<File, UploadError> {
403 let target = self.get_target_file(directory.as_ref(), name).await?;
404
405 tokio::fs::rename(&self.path, &target.path)
406 .await
407 .map_err(|e| UploadError::MoveFailed {
408 from: self.path.to_string_lossy().to_string(),
409 to: target.path.to_string_lossy().to_string(),
410 error: e.to_string(),
411 })?;
412
413 #[cfg(unix)]
415 {
416 use std::os::unix::fs::PermissionsExt;
417 let _ =
418 tokio::fs::set_permissions(&target.path, std::fs::Permissions::from_mode(0o666))
419 .await;
420 }
421
422 Ok(target)
423 }
424
425 async fn get_target_file(
433 &self,
434 directory: &Path,
435 name: Option<&str>,
436 ) -> Result<File, UploadError> {
437 if !directory.is_dir() {
438 tokio::fs::create_dir_all(directory).await.map_err(|_| {
439 UploadError::DirectoryCreateFailed(directory.to_string_lossy().to_string())
440 })?;
441 }
442
443 let file_name = match name {
444 Some(n) => get_name(n)?,
445 None => self
446 .path
447 .file_name()
448 .map(|n| n.to_string_lossy().to_string())
449 .unwrap_or_default(),
450 };
451
452 if file_name == ".." || file_name.contains('/') || file_name.contains('\\') {
454 return Err(UploadError::InvalidFileName(file_name));
455 }
456
457 let target_path = directory.join(&file_name);
458 Ok(File::new_unchecked(&target_path))
459 }
460
461 pub fn extension(&self) -> String {
465 self.path
466 .extension()
467 .map(|e| e.to_string_lossy().to_string())
468 .unwrap_or_default()
469 }
470
471 pub fn set_extension(&mut self, extension: &str) {
473 self.extension = Some(extension.to_string());
474 }
475
476 pub async fn hash_name(&mut self, rule: HashNameRule) -> Result<String, UploadError> {
506 if self.hash_name.is_none() {
507 let hash_name = match rule {
508 HashNameRule::Hash(algo) => {
509 let hash = self.hash(algo).await?;
511 if hash.len() < 2 {
512 hash
513 } else {
514 format!("{}/{}", &hash[..2], &hash[2..])
515 }
516 }
517 HashNameRule::Default => {
518 let now = Local::now();
520 let date_str = now.format("%Y%m%d").to_string();
521 let secs = now.timestamp();
523 let micros = now.timestamp_subsec_micros();
524 let microtime_str = format!("{}.{:06}", secs, micros);
525 let pathname = self.path.to_string_lossy();
526 let mut md5 = Md5::new();
527 md5.update(microtime_str.as_bytes());
528 md5.update(pathname.as_bytes());
529 let hash = hex::encode(md5.finalize());
530 format!("{}/{}", date_str, hash)
531 }
532 };
533 self.hash_name = Some(hash_name);
534 }
535
536 let extension = match &self.extension {
538 Some(ext) => ext.clone(),
539 None => self.extension(),
540 };
541
542 let hash_name = self
543 .hash_name
544 .as_ref()
545 .expect("hash_name 已在上方初始化")
546 .clone();
547 if extension.is_empty() {
548 Ok(hash_name)
549 } else {
550 Ok(format!("{}.{}", hash_name, extension))
552 }
553 }
554
555 pub fn basename(&self) -> String {
557 self.path
558 .file_name()
559 .map(|n| n.to_string_lossy().to_string())
560 .unwrap_or_default()
561 }
562}
563
564#[derive(Debug, Clone)]
587pub struct UploadedFile {
588 file: File,
590 test: bool,
592 original_name: String,
594 mime_type: String,
596 error: UploadErrCode,
598}
599
600impl UploadedFile {
601 pub fn new<P: AsRef<Path>>(
603 path: P,
604 original_name: &str,
605 mime_type: Option<&str>,
606 error: Option<i32>,
607 test: bool,
608 ) -> Result<Self, UploadError> {
609 let error = UploadErrCode::from_i32(error.unwrap_or(0));
610 let mime = mime_type.unwrap_or("application/octet-stream").to_string();
611
612 let check_path = error == UploadErrCode::Ok;
614 let file = File::new(path, check_path)?;
615
616 Ok(Self {
617 file,
618 test,
619 original_name: original_name.to_string(),
620 mime_type: mime,
621 error,
622 })
623 }
624
625 pub fn is_valid(&self) -> bool {
638 let is_ok = self.error == UploadErrCode::Ok;
639 if self.test {
640 is_ok
641 } else {
642 is_ok && self.file.path().is_file()
644 }
645 }
646
647 pub async fn move_to<P: AsRef<Path>>(
657 &mut self,
658 directory: P,
659 name: Option<&str>,
660 ) -> Result<File, UploadError> {
661 if !self.is_valid() {
662 return Err(UploadError::UploadFailed(
663 self.error.error_message().to_string(),
664 ));
665 }
666
667 if self.test {
668 return self.file.move_to(directory, name).await;
670 }
671
672 let target = self.file.get_target_file(directory.as_ref(), name).await?;
675 tokio::fs::rename(self.file.path(), &target.path)
676 .await
677 .map_err(|e| UploadError::MoveFailed {
678 from: self.file.path().to_string_lossy().to_string(),
679 to: target.path.to_string_lossy().to_string(),
680 error: e.to_string(),
681 })?;
682
683 #[cfg(unix)]
685 {
686 use std::os::unix::fs::PermissionsExt;
687 let _ =
688 tokio::fs::set_permissions(&target.path, std::fs::Permissions::from_mode(0o666))
689 .await;
690 }
691
692 Ok(target)
693 }
694
695 pub fn original_mime(&self) -> &str {
697 &self.mime_type
698 }
699
700 pub fn original_name(&self) -> &str {
702 &self.original_name
703 }
704
705 pub fn original_extension(&self) -> String {
709 Path::new(&self.original_name)
710 .extension()
711 .map(|e| e.to_string_lossy().to_string())
712 .unwrap_or_default()
713 }
714
715 pub fn extension(&self) -> String {
724 self.original_extension()
725 }
726
727 pub fn error_message(&self) -> &'static str {
729 self.error.error_message()
730 }
731
732 pub fn error_code(&self) -> UploadErrCode {
734 self.error
735 }
736
737 pub fn as_file(&self) -> &File {
739 &self.file
740 }
741
742 pub fn as_file_mut(&mut self) -> &mut File {
744 &mut self.file
745 }
746}
747
748fn get_name(name: &str) -> Result<String, UploadError> {
764 if name.contains("..") {
766 return Err(UploadError::InvalidFileName(name.to_string()));
767 }
768 let original_name = name.replace('\\', "/");
770 match original_name.rfind('/') {
772 Some(pos) => Ok(original_name[pos + 1..].to_string()),
773 None => Ok(original_name),
774 }
775}
776
777async fn compute_file_hash(path: &Path, algo: HashAlgo) -> Result<String, UploadError> {
779 let buf = tokio::fs::read(path).await?;
780
781 let hash = match algo {
782 HashAlgo::Md5 => {
783 let mut h = Md5::new();
784 h.update(&buf);
785 hex::encode(h.finalize())
786 }
787 HashAlgo::Sha1 => {
788 let mut h = sha1::Sha1::new();
789 h.update(&buf);
790 hex::encode(h.finalize())
791 }
792 HashAlgo::Sha256 => {
793 let mut h = sha2::Sha256::new();
794 h.update(&buf);
795 hex::encode(h.finalize())
796 }
797 HashAlgo::Sha512 => {
798 let mut h = sha2::Sha512::new();
799 h.update(&buf);
800 hex::encode(h.finalize())
801 }
802 };
803 Ok(hash)
804}
805
806use axum::extract::Multipart;
811use std::io::Write;
812use tempfile::NamedTempFile;
813
814#[derive(Debug, Default)]
816pub struct MultipartResult {
817 pub files: HashMap<String, Vec<UploadedFile>>,
821
822 pub fields: HashMap<String, String>,
826}
827
828impl MultipartResult {
829 pub fn file(&self, name: &str) -> Option<&UploadedFile> {
835 self.files.get(name).and_then(|list| list.first())
836 }
837
838 pub fn files(&self, name: &str) -> Option<&Vec<UploadedFile>> {
840 self.files.get(name)
841 }
842
843 pub fn field(&self, name: &str) -> Option<&str> {
845 self.fields.get(name).map(|s| s.as_str())
846 }
847
848 pub fn file_count(&self) -> usize {
850 self.files.values().map(|v| v.len()).sum()
851 }
852
853 pub fn is_empty(&self) -> bool {
855 self.files.is_empty() && self.fields.is_empty()
856 }
857}
858
859pub async fn parse_multipart(multipart: &mut Multipart) -> Result<MultipartResult, UploadError> {
889 let mut result = MultipartResult::default();
890
891 while let Some(field) = multipart
892 .next_field()
893 .await
894 .map_err(|e| UploadError::UploadFailed(e.to_string()))?
895 {
896 let name = field.name().unwrap_or("").to_string();
897 let file_name = field.file_name().map(|s| s.to_string());
898 let content_type = field.content_type().map(|s| s.to_string());
899
900 let data = field
901 .bytes()
902 .await
903 .map_err(|e| UploadError::UploadFailed(e.to_string()))?;
904
905 if let Some(file_name) = file_name {
906 let ext = Path::new(&file_name)
908 .extension()
909 .map(|e| format!(".{}", e.to_string_lossy()))
910 .unwrap_or_default();
911
912 let mut temp = NamedTempFile::with_suffix(&ext)?;
913 temp.write_all(&data)?;
914
915 let (_file, path) = temp.keep()?;
918
919 let uploaded = UploadedFile::new(
920 &path,
921 &file_name,
922 content_type.as_deref(),
923 Some(0),
924 true,
927 )?;
928
929 result.files.entry(name).or_default().push(uploaded);
930 } else {
931 let value = String::from_utf8_lossy(&data).to_string();
933 result.fields.insert(name, value);
934 }
935 }
936
937 Ok(result)
938}
939
940#[cfg(test)]
946mod tests {
947 use super::*;
948 use std::io::Write;
949 use tempfile::NamedTempFile;
950
951 fn create_temp_file(content: &[u8], suffix: &str) -> NamedTempFile {
953 let mut file = NamedTempFile::with_suffix(suffix).expect("创建临时文件失败");
954 file.write_all(content).expect("写入临时文件失败");
955 file
956 }
957
958 #[test]
963 fn test_upload_err_code_from_i32() {
964 assert_eq!(UploadErrCode::from_i32(0), UploadErrCode::Ok);
965 assert_eq!(UploadErrCode::from_i32(1), UploadErrCode::IniSize);
966 assert_eq!(UploadErrCode::from_i32(2), UploadErrCode::FormSize);
967 assert_eq!(UploadErrCode::from_i32(3), UploadErrCode::Partial);
968 assert_eq!(UploadErrCode::from_i32(4), UploadErrCode::NoFile);
969 assert_eq!(UploadErrCode::from_i32(6), UploadErrCode::NoTmpDir);
970 assert_eq!(UploadErrCode::from_i32(7), UploadErrCode::CantWrite);
971 assert_eq!(UploadErrCode::from_i32(99), UploadErrCode::Ok);
973 }
974
975 #[test]
976 fn test_upload_err_code_error_message() {
977 assert_eq!(
979 UploadErrCode::IniSize.error_message(),
980 "upload File size exceeds the maximum value"
981 );
982 assert_eq!(
983 UploadErrCode::FormSize.error_message(),
984 "upload File size exceeds the maximum value"
985 );
986 assert_eq!(
988 UploadErrCode::Partial.error_message(),
989 "only the portion of file is uploaded"
990 );
991 assert_eq!(UploadErrCode::NoFile.error_message(), "no file to uploaded");
993 assert_eq!(
995 UploadErrCode::NoTmpDir.error_message(),
996 "upload temp dir not found"
997 );
998 assert_eq!(UploadErrCode::CantWrite.error_message(), "file write error");
1000 assert_eq!(UploadErrCode::Ok.error_message(), "unknown upload error");
1002 }
1003
1004 #[test]
1009 fn test_hash_algo_as_str() {
1010 assert_eq!(HashAlgo::Md5.as_str(), "md5");
1011 assert_eq!(HashAlgo::Sha1.as_str(), "sha1");
1012 assert_eq!(HashAlgo::Sha256.as_str(), "sha256");
1013 assert_eq!(HashAlgo::Sha512.as_str(), "sha512");
1014 }
1015
1016 #[test]
1017 fn test_hash_algo_parse_algo() {
1018 assert_eq!(HashAlgo::parse_algo("md5"), Some(HashAlgo::Md5));
1019 assert_eq!(HashAlgo::parse_algo("sha1"), Some(HashAlgo::Sha1));
1020 assert_eq!(HashAlgo::parse_algo("sha256"), Some(HashAlgo::Sha256));
1021 assert_eq!(HashAlgo::parse_algo("sha512"), Some(HashAlgo::Sha512));
1022 assert_eq!(HashAlgo::parse_algo("unknown"), None);
1024 }
1025
1026 #[test]
1031 fn test_file_new_with_check_path() {
1032 let temp = create_temp_file(b"hello", ".txt");
1034 let file = File::new(temp.path(), true);
1035 assert!(file.is_ok());
1036
1037 let file = File::new("/nonexistent/file.txt", true);
1039 assert!(matches!(file, Err(UploadError::FileNotFound(_))));
1040 }
1041
1042 #[test]
1043 fn test_file_new_without_check_path() {
1044 let file = File::new("/nonexistent/file.txt", false);
1046 assert!(file.is_ok());
1047 }
1048
1049 #[test]
1050 fn test_file_new_unchecked() {
1051 let file = File::new_unchecked("/some/path/file.txt");
1052 assert_eq!(file.path(), Path::new("/some/path/file.txt"));
1053 }
1054
1055 #[test]
1056 fn test_file_path() {
1057 let temp = create_temp_file(b"hello", ".txt");
1058 let file = File::new(temp.path(), true).unwrap();
1059 assert_eq!(file.path(), temp.path());
1060 }
1061
1062 #[test]
1063 fn test_file_path_name() {
1064 let temp = create_temp_file(b"hello", ".txt");
1065 let file = File::new(temp.path(), true).unwrap();
1066 assert_eq!(file.path_name(), temp.path().to_string_lossy().to_string());
1067 }
1068
1069 #[test]
1070 fn test_file_extension() {
1071 let temp = create_temp_file(b"hello", ".txt");
1073 let file = File::new(temp.path(), true).unwrap();
1074 assert_eq!(file.extension(), "txt");
1075 }
1076
1077 #[test]
1078 fn test_file_extension_no_extension() {
1079 let mut file = NamedTempFile::new().unwrap();
1081 file.write_all(b"hello").unwrap();
1082 let file = File::new(file.path(), true).unwrap();
1083 assert_eq!(file.extension(), "");
1084 }
1085
1086 #[test]
1087 fn test_file_set_extension() {
1088 let temp = create_temp_file(b"hello", ".txt");
1090 let mut file = File::new(temp.path(), true).unwrap();
1091 assert_eq!(file.extension(), "txt");
1092 file.set_extension("jpg");
1093 assert_eq!(file.extension, Some("jpg".to_string()));
1094 }
1095
1096 #[test]
1097 fn test_file_basename() {
1098 let temp = create_temp_file(b"hello", ".txt");
1100 let file = File::new(temp.path(), true).unwrap();
1101 let basename = file.basename();
1102 assert!(basename.ends_with(".txt"));
1103 }
1104
1105 #[tokio::test]
1110 async fn test_file_md5() {
1111 let temp = create_temp_file(b"hello", ".txt");
1113 let mut file = File::new(temp.path(), true).unwrap();
1114 let md5 = file.md5().await.unwrap();
1115 assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
1117 }
1118
1119 #[tokio::test]
1120 async fn test_file_sha1() {
1121 let temp = create_temp_file(b"hello", ".txt");
1123 let mut file = File::new(temp.path(), true).unwrap();
1124 let sha1 = file.sha1().await.unwrap();
1125 assert_eq!(sha1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1127 }
1128
1129 #[tokio::test]
1130 async fn test_file_hash_md5() {
1131 let temp = create_temp_file(b"hello", ".txt");
1133 let mut file = File::new(temp.path(), true).unwrap();
1134 let hash = file.hash(HashAlgo::Md5).await.unwrap();
1135 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
1136 }
1137
1138 #[tokio::test]
1139 async fn test_file_hash_sha1() {
1140 let temp = create_temp_file(b"hello", ".txt");
1142 let mut file = File::new(temp.path(), true).unwrap();
1143 let hash = file.hash(HashAlgo::Sha1).await.unwrap();
1144 assert_eq!(hash, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1145 }
1146
1147 #[tokio::test]
1148 async fn test_file_hash_sha256() {
1149 let temp = create_temp_file(b"hello", ".txt");
1151 let mut file = File::new(temp.path(), true).unwrap();
1152 let hash = file.hash(HashAlgo::Sha256).await.unwrap();
1153 assert_eq!(
1155 hash,
1156 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
1157 );
1158 }
1159
1160 #[tokio::test]
1161 async fn test_file_hash_sha512() {
1162 let temp = create_temp_file(b"hello", ".txt");
1164 let mut file = File::new(temp.path(), true).unwrap();
1165 let hash = file.hash(HashAlgo::Sha512).await.unwrap();
1166 assert!(hash.starts_with("9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca"));
1168 }
1169
1170 #[tokio::test]
1171 async fn test_file_hash_caching() {
1172 let temp = create_temp_file(b"hello", ".txt");
1174 let mut file = File::new(temp.path(), true).unwrap();
1175 let hash1 = file.hash(HashAlgo::Md5).await.unwrap();
1176 let hash2 = file.hash(HashAlgo::Md5).await.unwrap();
1177 assert_eq!(hash1, hash2);
1178 assert!(file.hash.contains_key("md5"));
1180 }
1181
1182 #[tokio::test]
1183 async fn test_file_hash_multiple_algos() {
1184 let temp = create_temp_file(b"hello", ".txt");
1186 let mut file = File::new(temp.path(), true).unwrap();
1187 let md5 = file.hash(HashAlgo::Md5).await.unwrap();
1188 let sha1 = file.hash(HashAlgo::Sha1).await.unwrap();
1189 assert_ne!(md5, sha1);
1190 assert!(file.hash.contains_key("md5"));
1191 assert!(file.hash.contains_key("sha1"));
1192 }
1193
1194 #[test]
1199 fn test_file_get_mime_text() {
1200 let temp = create_temp_file(b"hello", ".txt");
1202 let file = File::new(temp.path(), true).unwrap();
1203 let mime = file.get_mime().unwrap();
1204 assert!(
1207 mime == "text/plain" || mime == "application/octet-stream",
1208 "mime = {}",
1209 mime
1210 );
1211 }
1212
1213 #[test]
1214 fn test_file_get_mime_png() {
1215 let png_header = [
1217 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, ];
1221 let mut file = NamedTempFile::with_suffix(".png").unwrap();
1222 file.write_all(&png_header).unwrap();
1223 let file = File::new(file.path(), true).unwrap();
1224 let mime = file.get_mime().unwrap();
1225 assert_eq!(mime, "image/png");
1226 }
1227
1228 #[test]
1229 fn test_file_get_mime_jpg() {
1230 let jpg_header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
1232 let mut file = NamedTempFile::with_suffix(".jpg").unwrap();
1233 file.write_all(&jpg_header).unwrap();
1234 let file = File::new(file.path(), true).unwrap();
1235 let mime = file.get_mime().unwrap();
1236 assert_eq!(mime, "image/jpeg");
1237 }
1238
1239 #[test]
1240 fn test_file_get_mime_unknown_extension() {
1241 let temp = create_temp_file(&[0x00, 0x01, 0x02, 0x03], "");
1243 let file = File::new(temp.path(), true).unwrap();
1244 let mime = file.get_mime().unwrap();
1245 assert_eq!(mime, "application/octet-stream");
1246 }
1247
1248 #[tokio::test]
1253 async fn test_file_move_with_default_name() {
1254 let temp = create_temp_file(b"hello", ".txt");
1256 let temp_dir = tempfile::tempdir().unwrap();
1257 let target_dir = temp_dir.path().join("subdir");
1258
1259 let mut file = File::new(temp.path(), true).unwrap();
1260 let original_basename = file.basename();
1261 let moved = file.move_to(&target_dir, None).await.unwrap();
1262
1263 assert!(moved.path().is_file());
1264 assert_eq!(moved.basename(), original_basename);
1265 assert!(!temp.path().exists());
1267 }
1268
1269 #[tokio::test]
1270 async fn test_file_move_with_custom_name() {
1271 let temp = create_temp_file(b"hello", ".txt");
1273 let temp_dir = tempfile::tempdir().unwrap();
1274
1275 let mut file = File::new(temp.path(), true).unwrap();
1276 let moved = file.move_to(&temp_dir, Some("custom.txt")).await.unwrap();
1277
1278 assert!(moved.path().is_file());
1279 assert_eq!(moved.basename(), "custom.txt");
1280 }
1281
1282 #[tokio::test]
1283 async fn test_file_move_creates_directory() {
1284 let temp = create_temp_file(b"hello", ".txt");
1286 let temp_dir = tempfile::tempdir().unwrap();
1287 let nested_dir = temp_dir.path().join("a").join("b").join("c");
1288
1289 let mut file = File::new(temp.path(), true).unwrap();
1290 let moved = file.move_to(&nested_dir, Some("file.txt")).await.unwrap();
1291
1292 assert!(moved.path().is_file());
1293 assert!(nested_dir.is_dir());
1294 }
1295
1296 #[tokio::test]
1297 async fn test_file_move_preserves_content() {
1298 let temp = create_temp_file(b"hello world", ".txt");
1299 let temp_dir = tempfile::tempdir().unwrap();
1300
1301 let mut file = File::new(temp.path(), true).unwrap();
1302 let moved = file.move_to(&temp_dir, Some("moved.txt")).await.unwrap();
1303
1304 let content = std::fs::read_to_string(moved.path()).unwrap();
1305 assert_eq!(content, "hello world");
1306 }
1307
1308 #[tokio::test]
1313 async fn test_file_hash_name_default_format() {
1314 let temp = create_temp_file(b"hello", ".txt");
1316 let mut file = File::new(temp.path(), true).unwrap();
1317 let hash_name = file.hash_name(HashNameRule::Default).await.unwrap();
1318
1319 let parts: Vec<&str> = hash_name.split('/').collect();
1321 assert_eq!(parts.len(), 2);
1322 let (date_part, md5_ext) = (parts[0], parts[1]);
1323
1324 assert_eq!(date_part.len(), 8);
1326 assert!(date_part.chars().all(|c| c.is_ascii_digit()));
1327
1328 let ext_parts: Vec<&str> = md5_ext.split('.').collect();
1330 assert_eq!(ext_parts.len(), 2);
1331 assert_eq!(ext_parts[0].len(), 32); assert!(ext_parts[0].chars().all(|c| c.is_ascii_hexdigit()));
1333 assert_eq!(ext_parts[1], "txt"); }
1335
1336 #[tokio::test]
1337 async fn test_file_hash_name_default_no_extension() {
1338 let temp = NamedTempFile::new().unwrap();
1340 std::fs::write(temp.path(), b"hello").unwrap();
1341 let mut file = File::new(temp.path(), true).unwrap();
1342 let hash_name = file.hash_name(HashNameRule::Default).await.unwrap();
1343
1344 let parts: Vec<&str> = hash_name.split('/').collect();
1346 assert_eq!(parts.len(), 2);
1347 assert!(!parts[1].contains('.'));
1348 }
1349
1350 #[tokio::test]
1351 async fn test_file_hash_name_hash_md5() {
1352 let temp = create_temp_file(b"hello", ".txt");
1354 let mut file = File::new(temp.path(), true).unwrap();
1355 let hash_name = file
1356 .hash_name(HashNameRule::Hash(HashAlgo::Md5))
1357 .await
1358 .unwrap();
1359
1360 assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592.txt");
1362 }
1363
1364 #[tokio::test]
1365 async fn test_file_hash_name_hash_sha1() {
1366 let temp = create_temp_file(b"hello", ".txt");
1368 let mut file = File::new(temp.path(), true).unwrap();
1369 let hash_name = file
1370 .hash_name(HashNameRule::Hash(HashAlgo::Sha1))
1371 .await
1372 .unwrap();
1373
1374 assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d.txt");
1376 }
1377
1378 #[tokio::test]
1379 async fn test_file_hash_name_caching() {
1380 let temp = create_temp_file(b"hello", ".txt");
1382 let mut file = File::new(temp.path(), true).unwrap();
1383 let name1 = file.hash_name(HashNameRule::Default).await.unwrap();
1384 let name2 = file.hash_name(HashNameRule::Default).await.unwrap();
1385 assert_eq!(name1, name2);
1386 }
1387
1388 #[tokio::test]
1389 async fn test_file_hash_name_with_set_extension() {
1390 let temp = create_temp_file(b"hello", ".txt");
1393 let mut file = File::new(temp.path(), true).unwrap();
1394 file.set_extension("jpg");
1395 let hash_name = file
1396 .hash_name(HashNameRule::Hash(HashAlgo::Md5))
1397 .await
1398 .unwrap();
1399 assert!(hash_name.ends_with(".jpg"));
1400 }
1401
1402 #[test]
1407 fn test_uploaded_file_new_ok() {
1408 let temp = create_temp_file(b"hello", ".txt");
1410 let uploaded = UploadedFile::new(
1411 temp.path(),
1412 "original.txt",
1413 Some("text/plain"),
1414 Some(0),
1415 false,
1416 );
1417 assert!(uploaded.is_ok());
1418 }
1419
1420 #[test]
1421 fn test_uploaded_file_new_with_error() {
1422 let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(3), false);
1424 assert!(uploaded.is_ok());
1425 }
1426
1427 #[test]
1428 fn test_uploaded_file_new_check_path_on_ok() {
1429 let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(0), false);
1431 assert!(matches!(uploaded, Err(UploadError::FileNotFound(_))));
1432 }
1433
1434 #[test]
1435 fn test_uploaded_file_original_name() {
1436 let temp = create_temp_file(b"hello", ".txt");
1437 let uploaded = UploadedFile::new(
1438 temp.path(),
1439 "my_file.txt",
1440 Some("text/plain"),
1441 Some(0),
1442 false,
1443 )
1444 .unwrap();
1445 assert_eq!(uploaded.original_name(), "my_file.txt");
1446 }
1447
1448 #[test]
1449 fn test_uploaded_file_original_mime() {
1450 let temp = create_temp_file(b"hello", ".txt");
1451 let uploaded = UploadedFile::new(
1452 temp.path(),
1453 "my_file.txt",
1454 Some("text/plain"),
1455 Some(0),
1456 false,
1457 )
1458 .unwrap();
1459 assert_eq!(uploaded.original_mime(), "text/plain");
1460 }
1461
1462 #[test]
1463 fn test_uploaded_file_original_mime_default() {
1464 let temp = create_temp_file(b"hello", ".txt");
1466 let uploaded = UploadedFile::new(temp.path(), "my_file.txt", None, Some(0), false).unwrap();
1467 assert_eq!(uploaded.original_mime(), "application/octet-stream");
1468 }
1469
1470 #[test]
1471 fn test_uploaded_file_original_extension() {
1472 let temp = create_temp_file(b"hello", ".txt");
1474 let uploaded = UploadedFile::new(
1475 temp.path(),
1476 "my_file.txt",
1477 Some("text/plain"),
1478 Some(0),
1479 false,
1480 )
1481 .unwrap();
1482 assert_eq!(uploaded.original_extension(), "txt");
1483 }
1484
1485 #[test]
1486 fn test_uploaded_file_original_extension_no_ext() {
1487 let temp = create_temp_file(b"hello", ".txt");
1488 let uploaded = UploadedFile::new(
1489 temp.path(),
1490 "no_extension",
1491 Some("text/plain"),
1492 Some(0),
1493 false,
1494 )
1495 .unwrap();
1496 assert_eq!(uploaded.original_extension(), "");
1497 }
1498
1499 #[test]
1500 fn test_uploaded_file_extension_overrides_parent() {
1501 let temp = create_temp_file(b"hello", ".txt");
1503 let uploaded =
1505 UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
1506 .unwrap();
1507 assert_eq!(uploaded.extension(), "jpg");
1509 assert_eq!(uploaded.as_file().extension(), "txt");
1511 }
1512
1513 #[test]
1518 fn test_uploaded_file_is_valid_ok() {
1519 let temp = create_temp_file(b"hello", ".txt");
1521 let uploaded = UploadedFile::new(
1522 temp.path(),
1523 "my_file.txt",
1524 Some("text/plain"),
1525 Some(0),
1526 false,
1527 )
1528 .unwrap();
1529 assert!(uploaded.is_valid());
1531 }
1532
1533 #[test]
1534 fn test_uploaded_file_is_valid_with_error() {
1535 let temp = create_temp_file(b"hello", ".txt");
1536 let uploaded = UploadedFile::new(
1537 temp.path(),
1538 "my_file.txt",
1539 Some("text/plain"),
1540 Some(3),
1541 false,
1542 )
1543 .unwrap();
1544 assert!(!uploaded.is_valid());
1546 }
1547
1548 #[test]
1549 fn test_uploaded_file_is_valid_test_mode() {
1550 let temp = create_temp_file(b"hello", ".txt");
1553 let uploaded = UploadedFile::new(
1554 temp.path(),
1555 "my_file.txt",
1556 Some("text/plain"),
1557 Some(0),
1558 true,
1559 )
1560 .unwrap();
1561 assert!(uploaded.is_valid());
1562 }
1563
1564 #[test]
1565 fn test_uploaded_file_is_valid_test_mode_with_error() {
1566 let uploaded = UploadedFile::new(
1567 "/nonexistent",
1568 "my_file.txt",
1569 Some("text/plain"),
1570 Some(4),
1571 true,
1572 )
1573 .unwrap();
1574 assert!(!uploaded.is_valid());
1575 }
1576
1577 #[tokio::test]
1582 async fn test_uploaded_file_move_test_mode() {
1583 let temp = create_temp_file(b"hello", ".txt");
1585 let temp_dir = tempfile::tempdir().unwrap();
1586 let mut uploaded = UploadedFile::new(
1587 temp.path(),
1588 "original.txt",
1589 Some("text/plain"),
1590 Some(0),
1591 true,
1592 )
1593 .unwrap();
1594 let moved = uploaded
1595 .move_to(&temp_dir, Some("moved.txt"))
1596 .await
1597 .unwrap();
1598 assert!(moved.path().is_file());
1599 assert_eq!(moved.basename(), "moved.txt");
1600 }
1601
1602 #[tokio::test]
1603 async fn test_uploaded_file_move_invalid() {
1604 let temp = create_temp_file(b"hello", ".txt");
1606 let temp_dir = tempfile::tempdir().unwrap();
1607 let mut uploaded = UploadedFile::new(
1608 temp.path(),
1609 "original.txt",
1610 Some("text/plain"),
1611 Some(3),
1612 false,
1613 )
1614 .unwrap();
1615 let result = uploaded.move_to(&temp_dir, Some("moved.txt")).await;
1616 assert!(matches!(result, Err(UploadError::UploadFailed(_))));
1617 if let Err(UploadError::UploadFailed(msg)) = result {
1619 assert_eq!(msg, "only the portion of file is uploaded");
1620 }
1621 }
1622
1623 #[tokio::test]
1624 async fn test_uploaded_file_move_real() {
1625 let temp = create_temp_file(b"hello", ".txt");
1627 let temp_dir = tempfile::tempdir().unwrap();
1628 let mut uploaded = UploadedFile::new(
1629 temp.path(),
1630 "original.txt",
1631 Some("text/plain"),
1632 Some(0),
1633 false,
1634 )
1635 .unwrap();
1636 let moved = uploaded
1637 .move_to(&temp_dir, Some("uploaded.txt"))
1638 .await
1639 .unwrap();
1640 assert!(moved.path().is_file());
1641 assert_eq!(moved.basename(), "uploaded.txt");
1642 assert!(!temp.path().exists());
1644 }
1645
1646 #[test]
1651 fn test_uploaded_file_error_message() {
1652 let temp = create_temp_file(b"hello", ".txt");
1654
1655 let uploaded_ok = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
1656 assert_eq!(uploaded_ok.error_message(), "unknown upload error");
1657
1658 let uploaded_1 = UploadedFile::new(temp.path(), "f.txt", None, Some(1), false).unwrap();
1659 assert_eq!(
1660 uploaded_1.error_message(),
1661 "upload File size exceeds the maximum value"
1662 );
1663
1664 let uploaded_3 = UploadedFile::new(temp.path(), "f.txt", None, Some(3), false).unwrap();
1665 assert_eq!(
1666 uploaded_3.error_message(),
1667 "only the portion of file is uploaded"
1668 );
1669
1670 let uploaded_4 = UploadedFile::new(temp.path(), "f.txt", None, Some(4), false).unwrap();
1671 assert_eq!(uploaded_4.error_message(), "no file to uploaded");
1672 }
1673
1674 #[test]
1675 fn test_uploaded_file_error_code() {
1676 let temp = create_temp_file(b"hello", ".txt");
1677 let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(7), false).unwrap();
1678 assert_eq!(uploaded.error_code(), UploadErrCode::CantWrite);
1679 }
1680
1681 #[test]
1686 fn test_get_name_simple() {
1687 assert_eq!(get_name("file.txt").unwrap(), "file.txt");
1689 }
1690
1691 #[test]
1692 fn test_get_name_with_path() {
1693 assert_eq!(get_name("/path/to/file.txt").unwrap(), "file.txt");
1695 }
1696
1697 #[test]
1698 fn test_get_name_with_backslash() {
1699 assert_eq!(get_name("\\path\\to\\file.txt").unwrap(), "file.txt");
1701 }
1702
1703 #[test]
1704 fn test_get_name_mixed_separators() {
1705 assert_eq!(get_name("\\path/to\\file.txt").unwrap(), "file.txt");
1707 }
1708
1709 #[test]
1710 fn test_get_name_only_filename() {
1711 assert_eq!(get_name("filename").unwrap(), "filename");
1712 }
1713
1714 #[test]
1715 fn test_get_name_rejects_path_traversal() {
1716 assert!(get_name("../etc/passwd").is_err());
1718 assert!(get_name("file..txt").is_err());
1719 assert!(get_name("..hidden").is_err());
1720 }
1721
1722 #[tokio::test]
1727 async fn test_php_behavior_hash_name_md5_split() {
1728 let temp = create_temp_file(b"hello", "");
1732 let mut file = File::new(temp.path(), true).unwrap();
1733 let hash_name = file
1734 .hash_name(HashNameRule::Hash(HashAlgo::Md5))
1735 .await
1736 .unwrap();
1737 assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592");
1738 }
1739
1740 #[tokio::test]
1741 async fn test_php_behavior_hash_name_sha1_split() {
1742 let temp = create_temp_file(b"hello", "");
1746 let mut file = File::new(temp.path(), true).unwrap();
1747 let hash_name = file
1748 .hash_name(HashNameRule::Hash(HashAlgo::Sha1))
1749 .await
1750 .unwrap();
1751 assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1752 }
1753
1754 #[tokio::test]
1755 async fn test_php_behavior_hash_name_default_format() {
1756 let temp = create_temp_file(b"hello", ".txt");
1758 let mut file = File::new(temp.path(), true).unwrap();
1759 let hash_name = file.hash_name(HashNameRule::Default).await.unwrap();
1760
1761 let re = regex::Regex::new(r"^\d{8}/[0-9a-f]{32}\.txt$").unwrap();
1763 assert!(
1764 re.is_match(&hash_name),
1765 "hash_name 格式不匹配:{}",
1766 hash_name
1767 );
1768 }
1769
1770 #[test]
1771 fn test_php_behavior_uploaded_file_extension_override() {
1772 let temp = create_temp_file(b"hello", ".txt");
1774 let uploaded =
1775 UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
1776 .unwrap();
1777 assert_eq!(uploaded.extension(), "jpg");
1779 assert_eq!(uploaded.as_file().extension(), "txt");
1781 }
1782
1783 #[test]
1784 fn test_php_behavior_is_valid_test_mode() {
1785 let temp = create_temp_file(b"hello", ".txt");
1787 let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), true).unwrap();
1788 assert!(uploaded.is_valid());
1789 }
1790
1791 #[test]
1792 fn test_php_behavior_is_valid_non_test_mode_requires_file() {
1793 let temp = create_temp_file(b"hello", ".txt");
1796 let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
1797 assert!(uploaded.is_valid());
1798
1799 let result = UploadedFile::new("/nonexistent/path", "f.txt", None, Some(0), false);
1801 assert!(matches!(result, Err(UploadError::FileNotFound(_))));
1802 }
1803
1804 #[test]
1805 fn test_php_behavior_error_message_mapping() {
1806 let temp = create_temp_file(b"hello", ".txt");
1808 assert_eq!(
1809 UploadedFile::new(temp.path(), "f", None, Some(1), false)
1810 .unwrap()
1811 .error_message(),
1812 "upload File size exceeds the maximum value"
1813 );
1814 assert_eq!(
1815 UploadedFile::new(temp.path(), "f", None, Some(2), false)
1816 .unwrap()
1817 .error_message(),
1818 "upload File size exceeds the maximum value"
1819 );
1820 assert_eq!(
1821 UploadedFile::new(temp.path(), "f", None, Some(3), false)
1822 .unwrap()
1823 .error_message(),
1824 "only the portion of file is uploaded"
1825 );
1826 assert_eq!(
1827 UploadedFile::new(temp.path(), "f", None, Some(4), false)
1828 .unwrap()
1829 .error_message(),
1830 "no file to uploaded"
1831 );
1832 assert_eq!(
1833 UploadedFile::new(temp.path(), "f", None, Some(6), false)
1834 .unwrap()
1835 .error_message(),
1836 "upload temp dir not found"
1837 );
1838 assert_eq!(
1839 UploadedFile::new(temp.path(), "f", None, Some(7), false)
1840 .unwrap()
1841 .error_message(),
1842 "file write error"
1843 );
1844 assert_eq!(
1845 UploadedFile::new(temp.path(), "f", None, Some(0), false)
1846 .unwrap()
1847 .error_message(),
1848 "unknown upload error"
1849 );
1850 }
1851
1852 #[tokio::test]
1853 async fn test_php_behavior_move_creates_directory() {
1854 let temp = create_temp_file(b"hello", ".txt");
1856 let temp_dir = tempfile::tempdir().unwrap();
1857 let nested = temp_dir.path().join("a").join("b").join("c");
1858
1859 let mut file = File::new(temp.path(), true).unwrap();
1860 let moved = file.move_to(&nested, Some("file.txt")).await.unwrap();
1861
1862 assert!(moved.path().is_file());
1863 assert!(nested.is_dir());
1864 }
1865
1866 #[tokio::test]
1867 async fn test_php_behavior_move_chmod_unix() {
1868 let temp = create_temp_file(b"hello", ".txt");
1871 let temp_dir = tempfile::tempdir().unwrap();
1872
1873 let mut file = File::new(temp.path(), true).unwrap();
1874 let moved = file.move_to(&temp_dir, Some("file.txt")).await.unwrap();
1875
1876 #[cfg(unix)]
1877 {
1878 use std::os::unix::fs::PermissionsExt;
1879 let perms = std::fs::metadata(moved.path())
1880 .unwrap()
1881 .permissions()
1882 .mode();
1883 assert_eq!(perms & 0o111, 0, "chmod 不应设置执行位");
1885 assert!(perms & 0o600 == 0o600, "owner 应有读写权限");
1886 }
1887 #[cfg(not(unix))]
1888 {
1889 let _ = moved;
1890 }
1891 }
1892
1893 #[tokio::test]
1894 async fn test_php_behavior_hash_caching() {
1895 let temp = create_temp_file(b"hello", ".txt");
1897 let mut file = File::new(temp.path(), true).unwrap();
1898 let md5_1 = file.hash(HashAlgo::Md5).await.unwrap();
1899 let md5_2 = file.hash(HashAlgo::Md5).await.unwrap();
1901 assert_eq!(md5_1, md5_2);
1902 }
1903
1904 #[tokio::test]
1905 async fn test_php_behavior_hash_name_caching() {
1906 let temp = create_temp_file(b"hello", ".txt");
1908 let mut file = File::new(temp.path(), true).unwrap();
1909 let name_1 = file
1910 .hash_name(HashNameRule::Hash(HashAlgo::Md5))
1911 .await
1912 .unwrap();
1913 let name_2 = file
1914 .hash_name(HashNameRule::Hash(HashAlgo::Sha1))
1915 .await
1916 .unwrap();
1917 assert_eq!(name_1, name_2);
1919 }
1920
1921 #[test]
1922 fn test_php_behavior_get_mime_infer() {
1923 let png_header = [
1926 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
1927 0x44, 0x52,
1928 ];
1929 let mut file = NamedTempFile::with_suffix(".png").unwrap();
1930 file.write_all(&png_header).unwrap();
1931 let file = File::new(file.path(), true).unwrap();
1932 assert_eq!(file.get_mime().unwrap(), "image/png");
1933 }
1934
1935 #[tokio::test]
1936 async fn test_php_behavior_uploaded_file_move_test_uses_rename() {
1937 let temp = create_temp_file(b"hello", ".txt");
1939 let temp_dir = tempfile::tempdir().unwrap();
1940 let mut uploaded =
1941 UploadedFile::new(temp.path(), "original.txt", None, Some(0), true).unwrap();
1942 let moved = uploaded
1943 .move_to(&temp_dir, Some("moved.txt"))
1944 .await
1945 .unwrap();
1946 assert!(moved.path().is_file());
1947 assert!(!temp.path().exists());
1949 }
1950
1951 #[tokio::test]
1952 async fn test_php_behavior_uploaded_file_move_non_test_uses_move_uploaded_file() {
1953 let temp = create_temp_file(b"hello", ".txt");
1955 let temp_dir = tempfile::tempdir().unwrap();
1956 let mut uploaded =
1957 UploadedFile::new(temp.path(), "original.txt", None, Some(0), false).unwrap();
1958 let moved = uploaded
1959 .move_to(&temp_dir, Some("moved.txt"))
1960 .await
1961 .unwrap();
1962 assert!(moved.path().is_file());
1963 assert!(!temp.path().exists());
1964 }
1965
1966 #[test]
1967 fn test_uploaded_file_as_file_access() {
1968 let temp = create_temp_file(b"hello", ".txt");
1969 let uploaded = UploadedFile::new(
1970 temp.path(),
1971 "original.txt",
1972 Some("text/plain"),
1973 Some(0),
1974 false,
1975 )
1976 .unwrap();
1977 assert_eq!(uploaded.as_file().path(), temp.path());
1979 assert_eq!(uploaded.as_file().extension(), "txt");
1980 }
1981
1982 #[tokio::test]
1983 async fn test_uploaded_file_as_file_mut_access() {
1984 let temp = create_temp_file(b"hello", ".txt");
1985 let mut uploaded = UploadedFile::new(
1986 temp.path(),
1987 "original.txt",
1988 Some("text/plain"),
1989 Some(0),
1990 false,
1991 )
1992 .unwrap();
1993 let md5 = uploaded.as_file_mut().md5().await.unwrap();
1995 assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
1996 }
1997}