use crate::{
crypto,
encoding::{base64, hex},
error::Error,
};
use bytes::Bytes;
use etag::EntityTag;
use md5::{Digest, Md5};
use mime::Mime;
use multer::{Field, Multipart};
use std::{
convert::AsRef,
fs::{self, OpenOptions},
io::{self, Write},
path::Path,
};
#[derive(Debug, Clone, Default)]
pub struct NamedFile {
field_name: Option<String>,
file_name: Option<String>,
content_type: Option<Mime>,
bytes: Bytes,
}
impl NamedFile {
#[inline]
pub fn new(file_name: impl Into<String>) -> Self {
Self {
field_name: None,
file_name: Some(file_name.into()),
content_type: None,
bytes: Bytes::new(),
}
}
#[inline]
pub fn set_field_name(&mut self, field_name: impl Into<String>) {
self.field_name = Some(field_name.into());
}
#[inline]
pub fn set_file_name(&mut self, file_name: impl Into<String>) {
self.file_name = Some(file_name.into());
}
#[inline]
pub fn set_content_type(&mut self, content_type: Mime) {
self.content_type = Some(content_type);
}
#[inline]
pub fn set_bytes(&mut self, bytes: impl Into<Bytes>) {
self.bytes = bytes.into();
}
#[inline]
pub fn field_name(&self) -> Option<&str> {
self.field_name.as_deref()
}
#[inline]
pub fn file_name(&self) -> Option<&str> {
self.file_name.as_deref()
}
#[inline]
pub fn content_type(&self) -> Option<&Mime> {
self.content_type.as_ref()
}
#[inline]
pub fn file_size(&self) -> usize {
self.bytes.len()
}
#[inline]
pub fn bytes(&self) -> Bytes {
self.bytes.clone()
}
#[inline]
pub fn checksum(&self) -> Bytes {
let checksum = crypto::digest(self.as_ref());
Vec::from(checksum).into()
}
#[inline]
pub fn etag(&self) -> EntityTag {
EntityTag::from_data(self.as_ref())
}
pub fn content_md5(&self) -> String {
let mut hasher = Md5::new();
hasher.update(self.as_ref());
base64::encode(hasher.finalize())
}
#[inline]
pub fn to_hex_string(&self) -> String {
hex::encode(self.as_ref())
}
#[inline]
pub fn to_base64_string(&self) -> String {
base64::encode(self.as_ref())
}
#[inline]
pub fn read_hex_string(&mut self, data: &str) -> Result<(), Error> {
let bytes = hex::decode(data)?;
self.bytes = bytes.into();
Ok(())
}
#[inline]
pub fn read_base64_string(&mut self, data: &str) -> Result<(), Error> {
let bytes = base64::decode(data)?;
self.bytes = bytes.into();
Ok(())
}
#[inline]
pub fn read_from_local(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
let bytes = fs::read(path)?;
self.bytes = bytes.into();
Ok(())
}
#[inline]
pub fn write(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
fs::write(path, self.as_ref())
}
#[inline]
pub fn append(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
let mut file = OpenOptions::new().append(true).open(path)?;
file.write_all(self.as_ref())
}
#[inline]
pub fn encrypt_with(&mut self, key: impl AsRef<[u8]>) -> Result<(), Error> {
let suffix = ".encrypted";
let bytes = crypto::encrypt(self.as_ref(), key.as_ref())?;
if let Some(ref mut file_name) = self.file_name {
if !file_name.ends_with(suffix) {
file_name.push_str(suffix);
}
}
self.bytes = bytes.into();
Ok(())
}
#[inline]
pub fn decrypt_with(&mut self, key: impl AsRef<[u8]>) -> Result<(), Error> {
let suffix = ".encrypted";
let bytes = crypto::decrypt(self.as_ref(), key.as_ref())?;
if let Some(ref mut file_name) = self.file_name {
if file_name.ends_with(suffix) {
file_name.truncate(file_name.len() - suffix.len());
}
}
self.bytes = bytes.into();
Ok(())
}
#[inline]
pub fn rename_file_stem(&mut self, file_stem: &str) -> Result<(), Error> {
let file_name = if let Some(ext) = self
.file_name
.as_ref()
.and_then(|s| Path::new(s).extension())
{
let ext = ext.to_string_lossy();
format!("{file_stem}.{ext}")
} else {
file_stem.to_owned()
};
self.file_name = Some(file_name);
Ok(())
}
pub fn try_from_local(path: impl AsRef<Path>) -> Result<Self, io::Error> {
let path = path.as_ref();
let bytes = fs::read(path)?;
let file_name = path.file_name().map(|s| s.to_string_lossy().into_owned());
let content_type = file_name.as_ref().and_then(|s| {
let file_name = s.strip_suffix(".encrypted").unwrap_or(s);
mime_guess::from_path(file_name).first()
});
Ok(Self {
field_name: None,
file_name,
content_type,
bytes: bytes.into(),
})
}
pub async fn try_from_multipart_field(field: Field<'_>) -> Result<Self, multer::Error> {
let field_name = field.name().map(|s| s.to_owned());
let file_name = field.file_name().map(|s| s.to_owned());
let content_type = field.content_type().cloned().or_else(|| {
file_name
.as_ref()
.and_then(|s| mime_guess::from_path(s).first())
});
let bytes = field.bytes().await?;
Ok(Self {
field_name,
file_name,
content_type,
bytes,
})
}
pub async fn try_from_multipart(mut multipart: Multipart<'_>) -> Result<Self, multer::Error> {
while let Some(field) = multipart.next_field().await? {
if field.file_name().is_some() {
let file = NamedFile::try_from_multipart_field(field).await?;
return Ok(file);
}
}
Err(multer::Error::IncompleteFieldData { field_name: None })
}
pub async fn try_collect_from_multipart(
mut multipart: Multipart<'_>,
) -> Result<Vec<Self>, multer::Error> {
let mut files = Vec::new();
while let Some(field) = multipart.next_field().await? {
if field.file_name().is_some() {
let file = NamedFile::try_from_multipart_field(field).await?;
files.push(file);
}
}
Ok(files)
}
}
impl AsRef<[u8]> for NamedFile {
#[inline]
fn as_ref(&self) -> &[u8] {
self.bytes.as_ref()
}
}
impl From<NamedFile> for Bytes {
#[inline]
fn from(file: NamedFile) -> Self {
file.bytes
}
}
impl<'a> From<&'a NamedFile> for Bytes {
#[inline]
fn from(file: &'a NamedFile) -> Self {
file.bytes()
}
}