axum-image-endpoint 0.1.1

Image Endpoint to upload big images and receive rescaled ones
Documentation
use std::{
    fs::{File, create_dir_all, remove_dir_all, remove_file},
    io::Write,
    path::Path as FsPath,
};

use axum::{
    Router,
    extract::DefaultBodyLimit,
    response::{IntoResponse, Redirect},
    routing::get,
};
use tower_http::limit::RequestBodyLimitLayer;

use crate::{
    error::ImageEndpointError,
    extractors::{Folder, FolderAndFileName},
    helpers::create_final_image,
    image_app_options::ImageAppOptions,
    multipart::ImageMultipart,
};

use crate::error::Result;

/// .
///
/// # Errors
///
/// This function will return an error if the setup failed for the image app (like creating folders)
#[allow(clippy::needless_pass_by_value)]
pub fn image_app(
    image_app_options: ImageAppOptions,
) -> std::result::Result<Router, ImageEndpointError> {
    image_app_options.create_image_folders()?;
    let state = image_app_options.image_app_state();
    let api_route = image_app_options.api_route();
    let cache_layer = image_app_options.cache_layer()?;
    let serve_dir = image_app_options.serve_dir()?;

    Ok(Router::new()
        .route(
            &api_route,
            get(get_image).post(post_image).delete(delete_image),
        )
        .layer(cache_layer)
        .nest_service(&image_app_options.img_route, serve_dir)
        .layer(DefaultBodyLimit::disable())
        .layer(RequestBodyLimitLayer::new(
            image_app_options.request_body_limit,
        ))
        .with_state(state))
}

async fn post_image(folder: Folder, image_multipart: ImageMultipart) -> Result<impl IntoResponse> {
    create_dir_all(folder.original_subfolder())?;

    // Write original file but no modified files
    let file_path = &folder.original_subfolder_file_path(&image_multipart.image_name);

    if FsPath::exists(file_path) {
        Err(ImageEndpointError::ImageAlreadyFound)
    } else {
        let mut file = File::create(file_path)?;
        file.write_all(&image_multipart.image)?;

        Ok(())
    }
}

async fn get_image(folder_and_filename: FolderAndFileName) -> Result<impl IntoResponse> {
    let original_filename = folder_and_filename.original_filename();
    let file_exists = folder_and_filename.does_original_file_exist();

    if file_exists && folder_and_filename.no_options_set() {
        // send out original image path
        let redirect_to = folder_and_filename.original_redirect_path();
        Ok(Redirect::temporary(&redirect_to))
    } else if file_exists {
        let hash = folder_and_filename.hash();
        let filepath = folder_and_filename.modified_filepath(&hash);

        if !FsPath::exists(&filepath) {
            create_final_image(
                &original_filename,
                filepath,
                folder_and_filename.get_image_options(),
            )?;
        }

        let redirect_to = folder_and_filename.modified_redirect_path(&hash);
        Ok(Redirect::temporary(&redirect_to))
    } else {
        Err(ImageEndpointError::ImageNotFound(original_filename))
    }
}

async fn delete_image(folder_and_filename: FolderAndFileName) -> Result<impl IntoResponse> {
    let hash = folder_and_filename.hash();
    let modified_folder = folder_and_filename.modified_folder(&hash);

    if modified_folder.exists() {
        remove_dir_all(&modified_folder)?;
    }

    let original_filename = folder_and_filename.original_filename();
    remove_file(&original_filename)?;

    Ok("")
}