rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
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"));
    }
}