murgamu 0.8.4

A NestJS-inspired web framework for Rust
Documentation
use super::MurMultipartUtils;
use crate::server::error::MurError;
use hyper::body::Bytes;
use std::io::Write;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct MurUploadedFile {
	pub filename: String,
	pub sanitized_filename: String,
	pub content_type: String,
	pub extension: Option<String>,
	pub data: Bytes,
	pub field_name: String,
}

impl MurUploadedFile {
	pub fn new(filename: String, content_type: String, data: Bytes, field_name: String) -> Self {
		let sanitized_filename = MurMultipartUtils::sanitize_filename(&filename);
		let extension = MurMultipartUtils::extract_extension(&filename);

		Self {
			filename,
			sanitized_filename,
			content_type,
			extension,
			data,
			field_name,
		}
	}

	pub fn filename(&self) -> &str {
		&self.filename
	}

	pub fn sanitized_filename(&self) -> &str {
		&self.sanitized_filename
	}

	pub fn content_type(&self) -> &str {
		&self.content_type
	}

	pub fn extension(&self) -> Option<&str> {
		self.extension.as_deref()
	}

	pub fn size(&self) -> usize {
		self.data.len()
	}

	pub fn data(&self) -> &Bytes {
		&self.data
	}

	pub fn as_bytes(&self) -> &[u8] {
		&self.data
	}

	pub fn into_bytes(self) -> Bytes {
		self.data
	}

	pub fn field_name(&self) -> &str {
		&self.field_name
	}

	pub fn has_extension(&self, ext: &str) -> bool {
		self.extension
			.as_ref()
			.map(|e| e.eq_ignore_ascii_case(ext))
			.unwrap_or(false)
	}

	pub fn has_extension_in(&self, extensions: &[&str]) -> bool {
		extensions.iter().any(|ext| self.has_extension(ext))
	}

	pub fn is_content_type(&self, content_type: &str) -> bool {
		self.content_type.eq_ignore_ascii_case(content_type)
	}

	pub fn is_image(&self) -> bool {
		self.content_type.starts_with("image/")
	}

	pub fn is_text(&self) -> bool {
		self.content_type.starts_with("text/")
	}

	pub fn is_pdf(&self) -> bool {
		self.content_type == "application/pdf"
	}

	pub fn as_text(&self) -> Result<&str, std::str::Utf8Error> {
		std::str::from_utf8(&self.data)
	}

	pub async fn save_to<P: AsRef<Path>>(&self, directory: P) -> Result<PathBuf, MurError> {
		self.save_to_with_name(directory, &self.sanitized_filename)
			.await
	}

	pub async fn save_to_with_name<P: AsRef<Path>>(
		&self,
		directory: P,
		filename: &str,
	) -> Result<PathBuf, MurError> {
		let path = directory.as_ref().join(filename);

		if let Some(parent) = path.parent() {
			tokio::fs::create_dir_all(parent)
				.await
				.map_err(|e| MurError::Internal(format!("Failed to create directory: {}", e)))?;
		}

		tokio::fs::write(&path, &self.data)
			.await
			.map_err(|e| MurError::Internal(format!("Failed to write file: {}", e)))?;

		Ok(path)
	}

	pub fn save_to_sync<P: AsRef<Path>>(&self, directory: P) -> Result<PathBuf, MurError> {
		let path = directory.as_ref().join(&self.sanitized_filename);

		if let Some(parent) = path.parent() {
			std::fs::create_dir_all(parent)
				.map_err(|e| MurError::Internal(format!("Failed to create directory: {}", e)))?;
		}

		let mut file = std::fs::File::create(&path)
			.map_err(|e| MurError::Internal(format!("Failed to create file: {}", e)))?;

		file.write_all(&self.data)
			.map_err(|e| MurError::Internal(format!("Failed to write file: {}", e)))?;

		Ok(path)
	}

	pub fn unique_filename(&self) -> String {
		let timestamp = std::time::SystemTime::now()
			.duration_since(std::time::UNIX_EPOCH)
			.map(|d| d.as_millis())
			.unwrap_or(0);
		let random: u32 = MurMultipartUtils::rand_u32();

		match &self.extension {
			Some(ext) => format!("{}_{}.{}", timestamp, random, ext),
			None => format!("{}_{}", timestamp, random),
		}
	}
}