use std::path::PathBuf;
use bytes::Bytes;
#[derive(Debug, Default, Clone, Copy)]
pub struct Upload;
#[derive(Debug)]
pub struct UploadedFile {
pub filename: String,
pub content_type: Option<String>,
path: PathBuf,
}
impl UploadedFile {
pub(crate) fn new(filename: String, content_type: Option<String>, path: PathBuf) -> Self {
Self {
filename,
content_type,
path,
}
}
pub fn filename(&self) -> &str {
&self.filename
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub async fn into_reader(self) -> std::io::Result<tokio::fs::File> {
tokio::fs::File::open(&self.path).await
}
pub async fn into_bytes(self) -> std::io::Result<Bytes> {
let data = tokio::fs::read(&self.path).await?;
Ok(Bytes::from(data))
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
}
impl Drop for UploadedFile {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_file(&self.path) {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::warn!(
error = %e,
path = %self.path.display(),
"failed to remove uploaded temp file"
);
}
}
}
}
pub async fn spool_field(
field: axum::extract::multipart::Field<'_>,
) -> Result<UploadedFile, super::FormError> {
use futures_util::TryStreamExt;
use tokio_util::io::StreamReader;
let filename = field
.file_name()
.map(str::to_string)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "upload.bin".into());
let content_type = field.content_type().map(str::to_string);
let tmp = tempfile::NamedTempFile::new().map_err(|e| super::FormError::Spool(e.to_string()))?;
let path = tmp
.into_temp_path()
.keep()
.map_err(|e| super::FormError::Spool(e.to_string()))?;
let stream = field.map_err(std::io::Error::other);
let mut reader = StreamReader::new(stream);
let mut file = tokio::fs::File::create(&path)
.await
.map_err(|e| super::FormError::Spool(e.to_string()))?;
tokio::io::copy(&mut reader, &mut file)
.await
.map_err(|e| super::FormError::Spool(e.to_string()))?;
Ok(UploadedFile::new(filename, content_type, path))
}