use std::fs;
use std::path::PathBuf;
use super::storage::{FileSystemStorage, Storage, StorageError};
use super::uploadedfile::{InMemoryUploadedFile, TemporaryUploadedFile};
pub trait UploadHandler {
type Output;
fn receive(
&mut self,
name: &str,
content_type: &str,
data: &[u8],
) -> Result<Self::Output, UploadHandlerError>;
}
#[derive(Debug, thiserror::Error)]
pub enum UploadHandlerError {
#[error("storage error: {0}")]
Storage(#[from] StorageError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Default)]
pub struct MemoryUploadHandler;
impl UploadHandler for MemoryUploadHandler {
type Output = InMemoryUploadedFile;
fn receive(
&mut self,
name: &str,
content_type: &str,
data: &[u8],
) -> Result<Self::Output, UploadHandlerError> {
Ok(InMemoryUploadedFile::new(name, content_type, data.to_vec()))
}
}
pub struct TemporaryFileUploadHandler {
pub temp_dir: PathBuf,
}
impl UploadHandler for TemporaryFileUploadHandler {
type Output = TemporaryUploadedFile;
fn receive(
&mut self,
name: &str,
content_type: &str,
data: &[u8],
) -> Result<Self::Output, UploadHandlerError> {
fs::create_dir_all(&self.temp_dir)?;
let storage = FileSystemStorage {
location: self.temp_dir.clone(),
base_url: String::new(),
};
let saved_name = storage.save(name, data)?;
let path = storage.path(&saved_name);
Ok(TemporaryUploadedFile::new(
path,
saved_name,
content_type,
data.len() as u64,
))
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::{MemoryUploadHandler, TemporaryFileUploadHandler, UploadHandler};
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestDir {
path: PathBuf,
}
impl TestDir {
fn new() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos();
let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rjango-upload-handler-test-{}-{nanos}-{counter}",
process::id()
));
fs::create_dir_all(&path).expect("create test directory");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
#[test]
fn memory_upload_handler_returns_in_memory_file() {
let mut handler = MemoryUploadHandler;
let uploaded = handler
.receive("avatar.png", "image/png", &[1, 2, 3])
.expect("receive in-memory upload")
.into_uploaded_file();
assert_eq!(uploaded.name, "avatar.png");
assert_eq!(uploaded.content_type, "image/png");
assert_eq!(uploaded.size, 3);
assert_eq!(uploaded.content, vec![1, 2, 3]);
}
#[test]
fn temporary_file_upload_handler_persists_content() {
let dir = TestDir::new();
let mut handler = TemporaryFileUploadHandler {
temp_dir: dir.path().to_path_buf(),
};
let uploaded = handler
.receive("uploads/report.txt", "text/plain", b"report")
.expect("receive temporary upload");
assert_eq!(uploaded.name, "uploads/report.txt");
assert_eq!(uploaded.size, 6);
assert_eq!(uploaded.read().expect("read upload"), b"report");
}
#[test]
fn temporary_file_upload_handler_uses_available_name_for_collisions() {
let dir = TestDir::new();
let mut handler = TemporaryFileUploadHandler {
temp_dir: dir.path().to_path_buf(),
};
let first = handler
.receive("duplicate.txt", "text/plain", b"first")
.expect("receive first upload");
let second = handler
.receive("duplicate.txt", "text/plain", b"second")
.expect("receive second upload");
assert_eq!(first.name, "duplicate.txt");
assert_eq!(second.name, "duplicate_1.txt");
assert_eq!(second.read().expect("read second upload"), b"second");
}
#[test]
fn temporary_file_upload_handler_creates_missing_directories() {
let dir = TestDir::new();
let nested_dir = dir.path().join("nested").join("temp");
let mut handler = TemporaryFileUploadHandler {
temp_dir: nested_dir.clone(),
};
let uploaded = handler
.receive("note.txt", "text/plain", b"ok")
.expect("receive upload into nested dir");
assert!(nested_dir.exists());
assert_eq!(uploaded.path, nested_dir.join("note.txt"));
}
}