axum-image-endpoint 0.1.1

Image Endpoint to upload big images and receive rescaled ones
Documentation
use std::{
    fs::create_dir_all,
    path::{Path, PathBuf},
    sync::Arc,
    time::Duration,
};

use http::{HeaderValue, header::CACHE_CONTROL};
use tower::ServiceBuilder;
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer};

use crate::{
    cache_control::CacheControlValue, error::ImageEndpointError, image_app_state::ImageAppState,
};

pub const ORIGINAL: &str = "original";
pub const MODIFIED: &str = "modified";

#[derive(Debug)]
pub struct ImageAppOptions {
    pub(crate) request_body_limit: usize,
    pub(crate) api_route: String,
    pub(crate) img_route: String,
    pub(crate) image_path: PathBuf,
    pub(crate) cache_time: Duration,
}

impl ImageAppOptions {
    /// Creates a new [`ImageAppOptions`].
    /// `api_route` is where the frontend should hit the api
    /// `img_route` is where the api does redirect to
    /// `image_path` is where the files get stored to disk
    #[must_use]
    pub fn new(
        request_body_limit: usize,
        api_route: String,
        img_route: String,
        image_path: PathBuf,
        cache_time: Duration,
    ) -> Self {
        Self {
            request_body_limit,
            api_route,
            img_route,
            image_path,
            cache_time,
        }
    }
}

impl ImageAppOptions {
    pub(crate) fn original_image_path(&self) -> PathBuf {
        Path::join(&self.image_path, ORIGINAL)
    }

    pub(crate) fn modified_image_path(&self) -> PathBuf {
        Path::join(&self.image_path, MODIFIED)
    }

    pub(crate) fn api_route(&self) -> String {
        format!("{}{}", &self.api_route, "{*image_path}")
    }

    pub(crate) fn image_app_state(&self) -> Arc<ImageAppState> {
        Arc::new(ImageAppState::from(self))
    }

    pub(crate) fn serve_dir(
        &self,
    ) -> Result<tower_http::set_header::SetResponseHeader<ServeDir, HeaderValue>, ImageEndpointError>
    {
        let serve_dir = ServeDir::new(&self.image_path);

        let cache_layer = self.cache_layer()?;
        let serve_dir = ServiceBuilder::new().layer(cache_layer).service(serve_dir);

        Ok(serve_dir)
    }

    pub(crate) fn cache_layer(
        &self,
    ) -> Result<SetResponseHeaderLayer<HeaderValue>, ImageEndpointError> {
        let cache_control = CacheControlValue::new()
            .max_age(self.cache_time)
            .must_revalidate()
            .public();

        let cache_layer = SetResponseHeaderLayer::if_not_present(
            CACHE_CONTROL,
            HeaderValue::from_str(&cache_control.to_string()).map_err(|_| {
                ImageEndpointError::Generic("Was not able to parse cache control header".into())
            })?,
        );

        Ok(cache_layer)
    }

    pub(crate) fn create_image_folders(&self) -> Result<(), ImageEndpointError> {
        // first create the originals folder
        create_dir_all(self.original_image_path())?;
        // create the folder for all the modified files
        create_dir_all(self.modified_image_path())?;

        Ok(())
    }
}

impl Default for ImageAppOptions {
    fn default() -> Self {
        let image_path = PathBuf::from("./public/api/img/");

        Self {
            request_body_limit: 10 * 1024 * 1024, // 10 MB
            api_route: "/api/img/".into(),
            image_path,
            img_route: "/public/api/img/".into(),
            cache_time: Duration::from_secs(60),
        }
    }
}