summaryrefslogblamecommitdiff
path: root/makima/src/server/handlers/files.rs
blob: 746d66b8c4c3f66b5b2e4ec01e60be443ad54f12 (plain) (tree)





































































































































































































































                                                                                                 
//! HTTP handlers for file CRUD operations.

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use uuid::Uuid;

use crate::db::models::{CreateFileRequest, FileListResponse, FileSummary, UpdateFileRequest};
use crate::db::repository;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;

/// List all files for the current owner.
#[utoipa::path(
    get,
    path = "/api/v1/files",
    responses(
        (status = 200, description = "List of files", body = FileListResponse),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    tag = "Files"
)]
pub async fn list_files(State(state): State<SharedState>) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::list_files(pool).await {
        Ok(files) => {
            let summaries: Vec<FileSummary> = files.into_iter().map(FileSummary::from).collect();
            let total = summaries.len() as i64;
            Json(FileListResponse {
                files: summaries,
                total,
            })
            .into_response()
        }
        Err(e) => {
            tracing::error!("Failed to list files: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Get a single file by ID.
#[utoipa::path(
    get,
    path = "/api/v1/files/{id}",
    params(
        ("id" = Uuid, Path, description = "File ID")
    ),
    responses(
        (status = 200, description = "File details", body = crate::db::models::File),
        (status = 404, description = "File not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    tag = "Files"
)]
pub async fn get_file(
    State(state): State<SharedState>,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::get_file(pool, id).await {
        Ok(Some(file)) => Json(file).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "File not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to get file {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Create a new file.
#[utoipa::path(
    post,
    path = "/api/v1/files",
    request_body = CreateFileRequest,
    responses(
        (status = 201, description = "File created", body = crate::db::models::File),
        (status = 400, description = "Invalid request", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    tag = "Files"
)]
pub async fn create_file(
    State(state): State<SharedState>,
    Json(req): Json<CreateFileRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::create_file(pool, req).await {
        Ok(file) => (StatusCode::CREATED, Json(file)).into_response(),
        Err(e) => {
            tracing::error!("Failed to create file: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Update an existing file.
#[utoipa::path(
    put,
    path = "/api/v1/files/{id}",
    params(
        ("id" = Uuid, Path, description = "File ID")
    ),
    request_body = UpdateFileRequest,
    responses(
        (status = 200, description = "File updated", body = crate::db::models::File),
        (status = 404, description = "File not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    tag = "Files"
)]
pub async fn update_file(
    State(state): State<SharedState>,
    Path(id): Path<Uuid>,
    Json(req): Json<UpdateFileRequest>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::update_file(pool, id, req).await {
        Ok(Some(file)) => Json(file).into_response(),
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "File not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to update file {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a file.
#[utoipa::path(
    delete,
    path = "/api/v1/files/{id}",
    params(
        ("id" = Uuid, Path, description = "File ID")
    ),
    responses(
        (status = 204, description = "File deleted"),
        (status = 404, description = "File not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    tag = "Files"
)]
pub async fn delete_file(
    State(state): State<SharedState>,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::delete_file(pool, id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "File not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to delete file {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}