axum-image-endpoint 0.1.1

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

use axum::{
    RequestExt, RequestPartsExt,
    extract::{FromRequest, FromRequestParts, Multipart, Path, Query, Request},
};

use crate::{
    error::ImageEndpointError,
    get_image_options::{FolderName, GetImageOptions},
    helpers::get_image_path_hash,
    image_app_options::{MODIFIED, ORIGINAL},
    image_app_state::ImageAppState,
    multipart::ImageMultipart,
};

#[derive(Debug, Clone)]
pub struct Folder {
    folder: String,
    state: Arc<ImageAppState>,
}

impl Folder {
    pub fn original_subfolder(&self) -> PathBuf {
        self.state.original_image_path.join(self.folder.as_str())
    }

    pub fn original_subfolder_file_path(&self, image_name: &str) -> PathBuf {
        self.original_subfolder().join(image_name)
    }
}

#[derive(Debug, Clone)]
pub struct FolderAndFileName {
    folder: String,
    filename: String,
    axum_path: String,
    get_image_options: GetImageOptions,
    state: Arc<ImageAppState>,
}

impl FolderAndFileName {
    pub fn get_image_options(&self) -> &GetImageOptions {
        &self.get_image_options
    }

    pub fn original_filename(&self) -> PathBuf {
        self.state
            .original_image_path
            .join(&self.folder)
            .join(&self.filename)
    }

    pub fn modified_filepath(&self, hash: &str) -> PathBuf {
        let foldername = self.modified_folder(hash);
        let filepath = foldername.join(&self.filename);

        filepath
    }

    pub fn modified_folder(&self, hash: &str) -> PathBuf {
        let foldername = self
            .state
            .modified_image_path
            .join(hash)
            .join(self.get_image_options.folder_name());

        foldername
    }

    pub fn hash(&self) -> String {
        let hash = get_image_path_hash(&self.axum_path);

        hash
    }

    pub fn modified_redirect_path(&self, hash: &str) -> String {
        let redirect_to = format!(
            "{}{}/{}/{}/{}",
            &self.state.image_route,
            MODIFIED,
            hash,
            self.get_image_options.folder_name(),
            self.filename
        );

        redirect_to
    }

    pub fn does_original_file_exist(&self) -> bool {
        let original_filename = self.original_filename();

        std::path::Path::exists(&original_filename)
    }

    pub fn no_options_set(&self) -> bool {
        self.get_image_options.no_options_set()
    }

    pub(crate) fn original_redirect_path(&self) -> String {
        let redirect_to = format!(
            "{}{}/{}/{}",
            &self.state.image_route, ORIGINAL, self.folder, self.filename
        );

        redirect_to
    }
}

impl FromRequestParts<Arc<ImageAppState>> for Folder {
    type Rejection = ImageEndpointError;

    async fn from_request_parts(
        parts: &mut http::request::Parts,
        state: &Arc<ImageAppState>,
    ) -> Result<Self, Self::Rejection> {
        let Path(path) = parts
            .extract::<Path<String>>()
            .await
            .map_err(|_| ImageEndpointError::Generic("Error while extracting the path".into()))?;

        let folder = Folder {
            folder: path,
            state: Arc::clone(state),
        };

        Ok(folder)
    }
}

impl FromRequestParts<Arc<ImageAppState>> for FolderAndFileName {
    type Rejection = ImageEndpointError;

    async fn from_request_parts(
        parts: &mut http::request::Parts,
        state: &Arc<ImageAppState>,
    ) -> Result<Self, Self::Rejection> {
        let Path(path) = parts
            .extract::<Path<String>>()
            .await
            .map_err(|_| ImageEndpointError::Generic("Error while extracting the path".into()))?;

        let Query(get_image_options) =
            parts
                .extract::<Query<GetImageOptions>>()
                .await
                .map_err(|_| {
                    ImageEndpointError::Generic("Was not able to extract query params".into())
                })?;

        let parts = path.rsplit_once('/');

        match parts {
            Some((folder, file)) => Ok(FolderAndFileName {
                folder: folder.into(),
                filename: file.into(),
                axum_path: path,
                state: state.clone(),
                get_image_options,
            }),
            None => Err(ImageEndpointError::Generic(
                "Was not able to read folder and filenamd from the path".into(),
            )),
        }
    }
}

impl<S> FromRequest<S> for ImageMultipart
where
    S: Send + Sync,
{
    type Rejection = ImageEndpointError;

    async fn from_request(req: Request, _state: &S) -> std::result::Result<Self, Self::Rejection> {
        let multipart: Multipart = req.extract::<Multipart, _>().await?;

        let image_multipart = ImageMultipart::try_from_multipart(multipart).await?;

        Ok(image_multipart)
    }
}