use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use chrono::Local;
use md5::{Digest, Md5};
pub mod image;
pub mod storage;
pub mod validate;
#[derive(Debug, thiserror::Error)]
pub enum UploadError {
#[error("The file \"{0}\" does not exist")]
FileNotFound(String),
#[error("Could not move the file \"{from}\" to \"{to}\" ({error})")]
MoveFailed {
from: String,
to: String,
error: String,
},
#[error("Unable to create the \"{0}\" directory")]
DirectoryCreateFailed(String),
#[error("Unable to write in the \"{0}\" directory")]
DirectoryNotWritable(String),
#[error("{0}")]
UploadFailed(String),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Persist(#[from] tempfile::PersistError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum UploadErrCode {
Ok = 0,
IniSize = 1,
FormSize = 2,
Partial = 3,
NoFile = 4,
NoTmpDir = 6,
CantWrite = 7,
}
impl UploadErrCode {
pub fn error_message(self) -> &'static str {
match self {
UploadErrCode::IniSize | UploadErrCode::FormSize => {
"upload File size exceeds the maximum value"
}
UploadErrCode::Partial => "only the portion of file is uploaded",
UploadErrCode::NoFile => "no file to uploaded",
UploadErrCode::NoTmpDir => "upload temp dir not found",
UploadErrCode::CantWrite => "file write error",
UploadErrCode::Ok => "unknown upload error",
}
}
pub fn from_i32(code: i32) -> Self {
match code {
0 => UploadErrCode::Ok,
1 => UploadErrCode::IniSize,
2 => UploadErrCode::FormSize,
3 => UploadErrCode::Partial,
4 => UploadErrCode::NoFile,
6 => UploadErrCode::NoTmpDir,
7 => UploadErrCode::CantWrite,
_ => UploadErrCode::Ok,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgo {
Md5,
Sha1,
Sha256,
Sha512,
}
impl HashAlgo {
pub fn as_str(self) -> &'static str {
match self {
HashAlgo::Md5 => "md5",
HashAlgo::Sha1 => "sha1",
HashAlgo::Sha256 => "sha256",
HashAlgo::Sha512 => "sha512",
}
}
pub fn parse_algo(s: &str) -> Option<Self> {
match s {
"md5" => Some(HashAlgo::Md5),
"sha1" => Some(HashAlgo::Sha1),
"sha256" => Some(HashAlgo::Sha256),
"sha512" => Some(HashAlgo::Sha512),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HashNameRule {
#[default]
Default,
Hash(HashAlgo),
}
#[derive(Debug, Clone)]
pub struct File {
path: PathBuf,
hash: HashMap<String, String>,
hash_name: Option<String>,
extension: Option<String>,
}
impl File {
pub fn new<P: AsRef<Path>>(path: P, check_path: bool) -> Result<Self, UploadError> {
let path = path.as_ref().to_path_buf();
if check_path && !path.is_file() {
return Err(UploadError::FileNotFound(
path.to_string_lossy().to_string(),
));
}
Ok(Self {
path,
hash: HashMap::new(),
hash_name: None,
extension: None,
})
}
pub fn new_unchecked<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
hash: HashMap::new(),
hash_name: None,
extension: None,
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn path_name(&self) -> String {
self.path.to_string_lossy().to_string()
}
pub fn hash(&mut self, algo: HashAlgo) -> Result<String, UploadError> {
let key = algo.as_str().to_string();
if let Some(h) = self.hash.get(&key) {
return Ok(h.clone());
}
let h = compute_file_hash(&self.path, algo)?;
self.hash.insert(key, h.clone());
Ok(h)
}
pub fn md5(&mut self) -> Result<String, UploadError> {
self.hash(HashAlgo::Md5)
}
pub fn sha1(&mut self) -> Result<String, UploadError> {
self.hash(HashAlgo::Sha1)
}
pub fn get_mime(&self) -> Result<String, UploadError> {
if let Ok(Some(t)) = infer::get_from_path(&self.path) {
return Ok(t.mime_type().to_string());
}
let mime = mime_guess::from_path(&self.path)
.first_or_octet_stream()
.to_string();
Ok(mime)
}
pub fn move_to<P: AsRef<Path>>(
&mut self,
directory: P,
name: Option<&str>,
) -> Result<File, UploadError> {
let target = self.get_target_file(directory.as_ref(), name)?;
fs::rename(&self.path, &target.path).map_err(|e| UploadError::MoveFailed {
from: self.path.to_string_lossy().to_string(),
to: target.path.to_string_lossy().to_string(),
error: e.to_string(),
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&target.path, fs::Permissions::from_mode(0o666));
}
Ok(target)
}
fn get_target_file(&self, directory: &Path, name: Option<&str>) -> Result<File, UploadError> {
if !directory.is_dir() {
fs::create_dir_all(directory).map_err(|_| {
UploadError::DirectoryCreateFailed(directory.to_string_lossy().to_string())
})?;
}
let file_name = match name {
Some(n) => get_name(n),
None => self
.path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default(),
};
let target_path = directory.join(&file_name);
Ok(File::new_unchecked(&target_path))
}
pub fn extension(&self) -> String {
self.path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn set_extension(&mut self, extension: &str) {
self.extension = Some(extension.to_string());
}
pub fn hash_name(&mut self, rule: HashNameRule) -> Result<String, UploadError> {
if self.hash_name.is_none() {
let hash_name = match rule {
HashNameRule::Hash(algo) => {
let hash = self.hash(algo)?;
if hash.len() < 2 {
hash
} else {
format!("{}/{}", &hash[..2], &hash[2..])
}
}
HashNameRule::Default => {
let now = Local::now();
let date_str = now.format("%Y%m%d").to_string();
let secs = now.timestamp();
let micros = now.timestamp_subsec_micros();
let microtime_str = format!("{}.{:06}", secs, micros);
let pathname = self.path.to_string_lossy();
let mut md5 = Md5::new();
md5.update(microtime_str.as_bytes());
md5.update(pathname.as_bytes());
let hash = hex::encode(md5.finalize());
format!("{}/{}", date_str, hash)
}
};
self.hash_name = Some(hash_name);
}
let extension = match &self.extension {
Some(ext) => ext.clone(),
None => self.extension(),
};
let hash_name = self.hash_name.as_ref().unwrap().clone();
if extension.is_empty() {
Ok(hash_name)
} else {
Ok(format!("{}.{}", hash_name, extension))
}
}
pub fn basename(&self) -> String {
self.path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct UploadedFile {
file: File,
test: bool,
original_name: String,
mime_type: String,
error: UploadErrCode,
}
impl UploadedFile {
pub fn new<P: AsRef<Path>>(
path: P,
original_name: &str,
mime_type: Option<&str>,
error: Option<i32>,
test: bool,
) -> Result<Self, UploadError> {
let error = UploadErrCode::from_i32(error.unwrap_or(0));
let mime = mime_type.unwrap_or("application/octet-stream").to_string();
let check_path = error == UploadErrCode::Ok;
let file = File::new(path, check_path)?;
Ok(Self {
file,
test,
original_name: original_name.to_string(),
mime_type: mime,
error,
})
}
pub fn is_valid(&self) -> bool {
let is_ok = self.error == UploadErrCode::Ok;
if self.test {
is_ok
} else {
is_ok && self.file.path().is_file()
}
}
pub fn move_to<P: AsRef<Path>>(
&mut self,
directory: P,
name: Option<&str>,
) -> Result<File, UploadError> {
if !self.is_valid() {
return Err(UploadError::UploadFailed(
self.error.error_message().to_string(),
));
}
if self.test {
return self.file.move_to(directory, name);
}
let target = self.file.get_target_file(directory.as_ref(), name)?;
fs::rename(self.file.path(), &target.path).map_err(|e| UploadError::MoveFailed {
from: self.file.path().to_string_lossy().to_string(),
to: target.path.to_string_lossy().to_string(),
error: e.to_string(),
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&target.path, fs::Permissions::from_mode(0o666));
}
Ok(target)
}
pub fn original_mime(&self) -> &str {
&self.mime_type
}
pub fn original_name(&self) -> &str {
&self.original_name
}
pub fn original_extension(&self) -> String {
Path::new(&self.original_name)
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn extension(&self) -> String {
self.original_extension()
}
pub fn error_message(&self) -> &'static str {
self.error.error_message()
}
pub fn error_code(&self) -> UploadErrCode {
self.error
}
pub fn as_file(&self) -> &File {
&self.file
}
pub fn as_file_mut(&mut self) -> &mut File {
&mut self.file
}
}
fn get_name(name: &str) -> String {
let original_name = name.replace('\\', "/");
match original_name.rfind('/') {
Some(pos) => original_name[pos + 1..].to_string(),
None => original_name,
}
}
fn compute_file_hash(path: &Path, algo: HashAlgo) -> Result<String, UploadError> {
let mut file = fs::File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
let hash = match algo {
HashAlgo::Md5 => {
let mut h = Md5::new();
h.update(&buf);
hex::encode(h.finalize())
}
HashAlgo::Sha1 => {
let mut h = sha1::Sha1::new();
h.update(&buf);
hex::encode(h.finalize())
}
HashAlgo::Sha256 => {
let mut h = sha2::Sha256::new();
h.update(&buf);
hex::encode(h.finalize())
}
HashAlgo::Sha512 => {
let mut h = sha2::Sha512::new();
h.update(&buf);
hex::encode(h.finalize())
}
};
Ok(hash)
}
use axum::extract::Multipart;
use std::io::Write;
use tempfile::NamedTempFile;
#[derive(Debug, Default)]
pub struct MultipartResult {
pub files: HashMap<String, Vec<UploadedFile>>,
pub fields: HashMap<String, String>,
}
impl MultipartResult {
pub fn file(&self, name: &str) -> Option<&UploadedFile> {
self.files.get(name).and_then(|list| list.first())
}
pub fn files(&self, name: &str) -> Option<&Vec<UploadedFile>> {
self.files.get(name)
}
pub fn field(&self, name: &str) -> Option<&str> {
self.fields.get(name).map(|s| s.as_str())
}
pub fn file_count(&self) -> usize {
self.files.values().map(|v| v.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty() && self.fields.is_empty()
}
}
pub async fn parse_multipart(multipart: &mut Multipart) -> Result<MultipartResult, UploadError> {
let mut result = MultipartResult::default();
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| UploadError::UploadFailed(e.to_string()))?
{
let name = field.name().unwrap_or("").to_string();
let file_name = field.file_name().map(|s| s.to_string());
let content_type = field.content_type().map(|s| s.to_string());
let data = field
.bytes()
.await
.map_err(|e| UploadError::UploadFailed(e.to_string()))?;
if let Some(file_name) = file_name {
let ext = Path::new(&file_name)
.extension()
.map(|e| format!(".{}", e.to_string_lossy()))
.unwrap_or_default();
let mut temp = NamedTempFile::with_suffix(&ext)?;
temp.write_all(&data)?;
let (_file, path) = temp.keep()?;
let uploaded = UploadedFile::new(
&path,
&file_name,
content_type.as_deref(),
Some(0),
true,
)?;
result.files.entry(name).or_default().push(uploaded);
} else {
let value = String::from_utf8_lossy(&data).to_string();
result.fields.insert(name, value);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_file(content: &[u8], suffix: &str) -> NamedTempFile {
let mut file = NamedTempFile::with_suffix(suffix).expect("创建临时文件失败");
file.write_all(content).expect("写入临时文件失败");
file
}
#[test]
fn test_upload_err_code_from_i32() {
assert_eq!(UploadErrCode::from_i32(0), UploadErrCode::Ok);
assert_eq!(UploadErrCode::from_i32(1), UploadErrCode::IniSize);
assert_eq!(UploadErrCode::from_i32(2), UploadErrCode::FormSize);
assert_eq!(UploadErrCode::from_i32(3), UploadErrCode::Partial);
assert_eq!(UploadErrCode::from_i32(4), UploadErrCode::NoFile);
assert_eq!(UploadErrCode::from_i32(6), UploadErrCode::NoTmpDir);
assert_eq!(UploadErrCode::from_i32(7), UploadErrCode::CantWrite);
assert_eq!(UploadErrCode::from_i32(99), UploadErrCode::Ok);
}
#[test]
fn test_upload_err_code_error_message() {
assert_eq!(
UploadErrCode::IniSize.error_message(),
"upload File size exceeds the maximum value"
);
assert_eq!(
UploadErrCode::FormSize.error_message(),
"upload File size exceeds the maximum value"
);
assert_eq!(
UploadErrCode::Partial.error_message(),
"only the portion of file is uploaded"
);
assert_eq!(UploadErrCode::NoFile.error_message(), "no file to uploaded");
assert_eq!(
UploadErrCode::NoTmpDir.error_message(),
"upload temp dir not found"
);
assert_eq!(UploadErrCode::CantWrite.error_message(), "file write error");
assert_eq!(UploadErrCode::Ok.error_message(), "unknown upload error");
}
#[test]
fn test_hash_algo_as_str() {
assert_eq!(HashAlgo::Md5.as_str(), "md5");
assert_eq!(HashAlgo::Sha1.as_str(), "sha1");
assert_eq!(HashAlgo::Sha256.as_str(), "sha256");
assert_eq!(HashAlgo::Sha512.as_str(), "sha512");
}
#[test]
fn test_hash_algo_parse_algo() {
assert_eq!(HashAlgo::parse_algo("md5"), Some(HashAlgo::Md5));
assert_eq!(HashAlgo::parse_algo("sha1"), Some(HashAlgo::Sha1));
assert_eq!(HashAlgo::parse_algo("sha256"), Some(HashAlgo::Sha256));
assert_eq!(HashAlgo::parse_algo("sha512"), Some(HashAlgo::Sha512));
assert_eq!(HashAlgo::parse_algo("unknown"), None);
}
#[test]
fn test_file_new_with_check_path() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true);
assert!(file.is_ok());
let file = File::new("/nonexistent/file.txt", true);
assert!(matches!(file, Err(UploadError::FileNotFound(_))));
}
#[test]
fn test_file_new_without_check_path() {
let file = File::new("/nonexistent/file.txt", false);
assert!(file.is_ok());
}
#[test]
fn test_file_new_unchecked() {
let file = File::new_unchecked("/some/path/file.txt");
assert_eq!(file.path(), Path::new("/some/path/file.txt"));
}
#[test]
fn test_file_path() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true).unwrap();
assert_eq!(file.path(), temp.path());
}
#[test]
fn test_file_path_name() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true).unwrap();
assert_eq!(file.path_name(), temp.path().to_string_lossy().to_string());
}
#[test]
fn test_file_extension() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true).unwrap();
assert_eq!(file.extension(), "txt");
}
#[test]
fn test_file_extension_no_extension() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"hello").unwrap();
let file = File::new(file.path(), true).unwrap();
assert_eq!(file.extension(), "");
}
#[test]
fn test_file_set_extension() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
assert_eq!(file.extension(), "txt");
file.set_extension("jpg");
assert_eq!(file.extension, Some("jpg".to_string()));
}
#[test]
fn test_file_basename() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true).unwrap();
let basename = file.basename();
assert!(basename.ends_with(".txt"));
}
#[test]
fn test_file_md5() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let md5 = file.md5().unwrap();
assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
}
#[test]
fn test_file_sha1() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let sha1 = file.sha1().unwrap();
assert_eq!(sha1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
}
#[test]
fn test_file_hash_md5() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash = file.hash(HashAlgo::Md5).unwrap();
assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
}
#[test]
fn test_file_hash_sha1() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash = file.hash(HashAlgo::Sha1).unwrap();
assert_eq!(hash, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
}
#[test]
fn test_file_hash_sha256() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash = file.hash(HashAlgo::Sha256).unwrap();
assert_eq!(
hash,
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
}
#[test]
fn test_file_hash_sha512() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash = file.hash(HashAlgo::Sha512).unwrap();
assert!(hash.starts_with("9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca"));
}
#[test]
fn test_file_hash_caching() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash1 = file.hash(HashAlgo::Md5).unwrap();
let hash2 = file.hash(HashAlgo::Md5).unwrap();
assert_eq!(hash1, hash2);
assert!(file.hash.contains_key("md5"));
}
#[test]
fn test_file_hash_multiple_algos() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let md5 = file.hash(HashAlgo::Md5).unwrap();
let sha1 = file.hash(HashAlgo::Sha1).unwrap();
assert_ne!(md5, sha1);
assert!(file.hash.contains_key("md5"));
assert!(file.hash.contains_key("sha1"));
}
#[test]
fn test_file_get_mime_text() {
let temp = create_temp_file(b"hello", ".txt");
let file = File::new(temp.path(), true).unwrap();
let mime = file.get_mime().unwrap();
assert!(
mime == "text/plain" || mime == "application/octet-stream",
"mime = {}",
mime
);
}
#[test]
fn test_file_get_mime_png() {
let png_header = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, ];
let mut file = NamedTempFile::with_suffix(".png").unwrap();
file.write_all(&png_header).unwrap();
let file = File::new(file.path(), true).unwrap();
let mime = file.get_mime().unwrap();
assert_eq!(mime, "image/png");
}
#[test]
fn test_file_get_mime_jpg() {
let jpg_header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
let mut file = NamedTempFile::with_suffix(".jpg").unwrap();
file.write_all(&jpg_header).unwrap();
let file = File::new(file.path(), true).unwrap();
let mime = file.get_mime().unwrap();
assert_eq!(mime, "image/jpeg");
}
#[test]
fn test_file_get_mime_unknown_extension() {
let temp = create_temp_file(&[0x00, 0x01, 0x02, 0x03], "");
let file = File::new(temp.path(), true).unwrap();
let mime = file.get_mime().unwrap();
assert_eq!(mime, "application/octet-stream");
}
#[test]
fn test_file_move_with_default_name() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let target_dir = temp_dir.path().join("subdir");
let mut file = File::new(temp.path(), true).unwrap();
let original_basename = file.basename();
let moved = file.move_to(&target_dir, None).unwrap();
assert!(moved.path().is_file());
assert_eq!(moved.basename(), original_basename);
assert!(!temp.path().exists());
}
#[test]
fn test_file_move_with_custom_name() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut file = File::new(temp.path(), true).unwrap();
let moved = file.move_to(&temp_dir, Some("custom.txt")).unwrap();
assert!(moved.path().is_file());
assert_eq!(moved.basename(), "custom.txt");
}
#[test]
fn test_file_move_creates_directory() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let nested_dir = temp_dir.path().join("a").join("b").join("c");
let mut file = File::new(temp.path(), true).unwrap();
let moved = file.move_to(&nested_dir, Some("file.txt")).unwrap();
assert!(moved.path().is_file());
assert!(nested_dir.is_dir());
}
#[test]
fn test_file_move_preserves_content() {
let temp = create_temp_file(b"hello world", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut file = File::new(temp.path(), true).unwrap();
let moved = file.move_to(&temp_dir, Some("moved.txt")).unwrap();
let content = std::fs::read_to_string(moved.path()).unwrap();
assert_eq!(content, "hello world");
}
#[test]
fn test_file_hash_name_default_format() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Default).unwrap();
let parts: Vec<&str> = hash_name.split('/').collect();
assert_eq!(parts.len(), 2);
let (date_part, md5_ext) = (parts[0], parts[1]);
assert_eq!(date_part.len(), 8);
assert!(date_part.chars().all(|c| c.is_ascii_digit()));
let ext_parts: Vec<&str> = md5_ext.split('.').collect();
assert_eq!(ext_parts.len(), 2);
assert_eq!(ext_parts[0].len(), 32); assert!(ext_parts[0].chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(ext_parts[1], "txt"); }
#[test]
fn test_file_hash_name_default_no_extension() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), b"hello").unwrap();
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Default).unwrap();
let parts: Vec<&str> = hash_name.split('/').collect();
assert_eq!(parts.len(), 2);
assert!(!parts[1].contains('.'));
}
#[test]
fn test_file_hash_name_hash_md5() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592.txt");
}
#[test]
fn test_file_hash_name_hash_sha1() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d.txt");
}
#[test]
fn test_file_hash_name_caching() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let name1 = file.hash_name(HashNameRule::Default).unwrap();
let name2 = file.hash_name(HashNameRule::Default).unwrap();
assert_eq!(name1, name2);
}
#[test]
fn test_file_hash_name_with_set_extension() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
file.set_extension("jpg");
let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
assert!(hash_name.ends_with(".jpg"));
}
#[test]
fn test_uploaded_file_new_ok() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(0),
false,
);
assert!(uploaded.is_ok());
}
#[test]
fn test_uploaded_file_new_with_error() {
let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(3), false);
assert!(uploaded.is_ok());
}
#[test]
fn test_uploaded_file_new_check_path_on_ok() {
let uploaded = UploadedFile::new("/nonexistent", "original.txt", None, Some(0), false);
assert!(matches!(uploaded, Err(UploadError::FileNotFound(_))));
}
#[test]
fn test_uploaded_file_original_name() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert_eq!(uploaded.original_name(), "my_file.txt");
}
#[test]
fn test_uploaded_file_original_mime() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert_eq!(uploaded.original_mime(), "text/plain");
}
#[test]
fn test_uploaded_file_original_mime_default() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(temp.path(), "my_file.txt", None, Some(0), false).unwrap();
assert_eq!(uploaded.original_mime(), "application/octet-stream");
}
#[test]
fn test_uploaded_file_original_extension() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert_eq!(uploaded.original_extension(), "txt");
}
#[test]
fn test_uploaded_file_original_extension_no_ext() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"no_extension",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert_eq!(uploaded.original_extension(), "");
}
#[test]
fn test_uploaded_file_extension_overrides_parent() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded =
UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
.unwrap();
assert_eq!(uploaded.extension(), "jpg");
assert_eq!(uploaded.as_file().extension(), "txt");
}
#[test]
fn test_uploaded_file_is_valid_ok() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert!(uploaded.is_valid());
}
#[test]
fn test_uploaded_file_is_valid_with_error() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(3),
false,
)
.unwrap();
assert!(!uploaded.is_valid());
}
#[test]
fn test_uploaded_file_is_valid_test_mode() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"my_file.txt",
Some("text/plain"),
Some(0),
true,
)
.unwrap();
assert!(uploaded.is_valid());
}
#[test]
fn test_uploaded_file_is_valid_test_mode_with_error() {
let uploaded = UploadedFile::new(
"/nonexistent",
"my_file.txt",
Some("text/plain"),
Some(4),
true,
)
.unwrap();
assert!(!uploaded.is_valid());
}
#[test]
fn test_uploaded_file_move_test_mode() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(0),
true,
)
.unwrap();
let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
assert!(moved.path().is_file());
assert_eq!(moved.basename(), "moved.txt");
}
#[test]
fn test_uploaded_file_move_invalid() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(3),
false,
)
.unwrap();
let result = uploaded.move_to(&temp_dir, Some("moved.txt"));
assert!(matches!(result, Err(UploadError::UploadFailed(_))));
if let Err(UploadError::UploadFailed(msg)) = result {
assert_eq!(msg, "only the portion of file is uploaded");
}
}
#[test]
fn test_uploaded_file_move_real() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
let moved = uploaded.move_to(&temp_dir, Some("uploaded.txt")).unwrap();
assert!(moved.path().is_file());
assert_eq!(moved.basename(), "uploaded.txt");
assert!(!temp.path().exists());
}
#[test]
fn test_uploaded_file_error_message() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded_ok = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
assert_eq!(uploaded_ok.error_message(), "unknown upload error");
let uploaded_1 = UploadedFile::new(temp.path(), "f.txt", None, Some(1), false).unwrap();
assert_eq!(
uploaded_1.error_message(),
"upload File size exceeds the maximum value"
);
let uploaded_3 = UploadedFile::new(temp.path(), "f.txt", None, Some(3), false).unwrap();
assert_eq!(
uploaded_3.error_message(),
"only the portion of file is uploaded"
);
let uploaded_4 = UploadedFile::new(temp.path(), "f.txt", None, Some(4), false).unwrap();
assert_eq!(uploaded_4.error_message(), "no file to uploaded");
}
#[test]
fn test_uploaded_file_error_code() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(7), false).unwrap();
assert_eq!(uploaded.error_code(), UploadErrCode::CantWrite);
}
#[test]
fn test_get_name_simple() {
assert_eq!(get_name("file.txt"), "file.txt");
}
#[test]
fn test_get_name_with_path() {
assert_eq!(get_name("/path/to/file.txt"), "file.txt");
}
#[test]
fn test_get_name_with_backslash() {
assert_eq!(get_name("\\path\\to\\file.txt"), "file.txt");
}
#[test]
fn test_get_name_mixed_separators() {
assert_eq!(get_name("\\path/to\\file.txt"), "file.txt");
}
#[test]
fn test_get_name_only_filename() {
assert_eq!(get_name("filename"), "filename");
}
#[test]
fn test_php_behavior_hash_name_md5_split() {
let temp = create_temp_file(b"hello", "");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
assert_eq!(hash_name, "5d/41402abc4b2a76b9719d911017c592");
}
#[test]
fn test_php_behavior_hash_name_sha1_split() {
let temp = create_temp_file(b"hello", "");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
assert_eq!(hash_name, "aa/f4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
}
#[test]
fn test_php_behavior_hash_name_default_format() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let hash_name = file.hash_name(HashNameRule::Default).unwrap();
let re = regex::Regex::new(r"^\d{8}/[0-9a-f]{32}\.txt$").unwrap();
assert!(
re.is_match(&hash_name),
"hash_name 格式不匹配:{}",
hash_name
);
}
#[test]
fn test_php_behavior_uploaded_file_extension_override() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded =
UploadedFile::new(temp.path(), "photo.jpg", Some("image/jpeg"), Some(0), false)
.unwrap();
assert_eq!(uploaded.extension(), "jpg");
assert_eq!(uploaded.as_file().extension(), "txt");
}
#[test]
fn test_php_behavior_is_valid_test_mode() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), true).unwrap();
assert!(uploaded.is_valid());
}
#[test]
fn test_php_behavior_is_valid_non_test_mode_requires_file() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(temp.path(), "f.txt", None, Some(0), false).unwrap();
assert!(uploaded.is_valid());
let result = UploadedFile::new("/nonexistent/path", "f.txt", None, Some(0), false);
assert!(matches!(result, Err(UploadError::FileNotFound(_))));
}
#[test]
fn test_php_behavior_error_message_mapping() {
let temp = create_temp_file(b"hello", ".txt");
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(1), false)
.unwrap()
.error_message(),
"upload File size exceeds the maximum value"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(2), false)
.unwrap()
.error_message(),
"upload File size exceeds the maximum value"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(3), false)
.unwrap()
.error_message(),
"only the portion of file is uploaded"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(4), false)
.unwrap()
.error_message(),
"no file to uploaded"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(6), false)
.unwrap()
.error_message(),
"upload temp dir not found"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(7), false)
.unwrap()
.error_message(),
"file write error"
);
assert_eq!(
UploadedFile::new(temp.path(), "f", None, Some(0), false)
.unwrap()
.error_message(),
"unknown upload error"
);
}
#[test]
fn test_php_behavior_move_creates_directory() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let nested = temp_dir.path().join("a").join("b").join("c");
let mut file = File::new(temp.path(), true).unwrap();
let moved = file.move_to(&nested, Some("file.txt")).unwrap();
assert!(moved.path().is_file());
assert!(nested.is_dir());
}
#[test]
fn test_php_behavior_move_chmod_unix() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut file = File::new(temp.path(), true).unwrap();
let moved = file.move_to(&temp_dir, Some("file.txt")).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(moved.path())
.unwrap()
.permissions()
.mode();
assert_eq!(perms & 0o777, 0o644);
}
#[cfg(not(unix))]
{
let _ = moved;
}
}
#[test]
fn test_php_behavior_hash_caching() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let md5_1 = file.hash(HashAlgo::Md5).unwrap();
let md5_2 = file.hash(HashAlgo::Md5).unwrap();
assert_eq!(md5_1, md5_2);
}
#[test]
fn test_php_behavior_hash_name_caching() {
let temp = create_temp_file(b"hello", ".txt");
let mut file = File::new(temp.path(), true).unwrap();
let name_1 = file.hash_name(HashNameRule::Hash(HashAlgo::Md5)).unwrap();
let name_2 = file.hash_name(HashNameRule::Hash(HashAlgo::Sha1)).unwrap();
assert_eq!(name_1, name_2);
}
#[test]
fn test_php_behavior_get_mime_infer() {
let png_header = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
0x44, 0x52,
];
let mut file = NamedTempFile::with_suffix(".png").unwrap();
file.write_all(&png_header).unwrap();
let file = File::new(file.path(), true).unwrap();
assert_eq!(file.get_mime().unwrap(), "image/png");
}
#[test]
fn test_php_behavior_uploaded_file_move_test_uses_rename() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut uploaded =
UploadedFile::new(temp.path(), "original.txt", None, Some(0), true).unwrap();
let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
assert!(moved.path().is_file());
assert!(!temp.path().exists());
}
#[test]
fn test_php_behavior_uploaded_file_move_non_test_uses_move_uploaded_file() {
let temp = create_temp_file(b"hello", ".txt");
let temp_dir = tempfile::tempdir().unwrap();
let mut uploaded =
UploadedFile::new(temp.path(), "original.txt", None, Some(0), false).unwrap();
let moved = uploaded.move_to(&temp_dir, Some("moved.txt")).unwrap();
assert!(moved.path().is_file());
assert!(!temp.path().exists());
}
#[test]
fn test_uploaded_file_as_file_access() {
let temp = create_temp_file(b"hello", ".txt");
let uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
assert_eq!(uploaded.as_file().path(), temp.path());
assert_eq!(uploaded.as_file().extension(), "txt");
}
#[test]
fn test_uploaded_file_as_file_mut_access() {
let temp = create_temp_file(b"hello", ".txt");
let mut uploaded = UploadedFile::new(
temp.path(),
"original.txt",
Some("text/plain"),
Some(0),
false,
)
.unwrap();
let md5 = uploaded.as_file_mut().md5().unwrap();
assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592");
}
}