diff options
Diffstat (limited to 'makima/src/server/handlers/versions.rs')
| -rw-r--r-- | makima/src/server/handlers/versions.rs | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/makima/src/server/handlers/versions.rs b/makima/src/server/handlers/versions.rs new file mode 100644 index 0000000..15118d6 --- /dev/null +++ b/makima/src/server/handlers/versions.rs @@ -0,0 +1,207 @@ +//! HTTP handlers for file version history operations. + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use uuid::Uuid; + +use crate::db::models::{FileVersionListResponse, FileVersionSummary, RestoreVersionRequest}; +use crate::db::repository::{self, RepositoryError}; +use crate::server::messages::ApiError; +use crate::server::state::{FileUpdateNotification, SharedState}; + +/// List all versions of a file. +#[utoipa::path( + get, + path = "/api/v1/files/{id}/versions", + params( + ("id" = Uuid, Path, description = "File ID") + ), + responses( + (status = 200, description = "List of file versions", body = FileVersionListResponse), + (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 = "Versions" +)] +pub async fn list_versions( + State(state): State<SharedState>, + Path(file_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(); + }; + + // Check if file exists + match repository::get_file(pool, file_id).await { + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "File not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to check file {}: {}", file_id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + Ok(Some(_)) => {} + } + + match repository::list_file_versions(pool, file_id).await { + Ok(versions) => { + let summaries: Vec<FileVersionSummary> = + versions.into_iter().map(FileVersionSummary::from).collect(); + let total = summaries.len() as i64; + Json(FileVersionListResponse { + versions: summaries, + total, + }) + .into_response() + } + Err(e) => { + tracing::error!("Failed to list versions for file {}: {}", file_id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Get a specific version of a file. +#[utoipa::path( + get, + path = "/api/v1/files/{id}/versions/{version}", + params( + ("id" = Uuid, Path, description = "File ID"), + ("version" = i32, Path, description = "Version number") + ), + responses( + (status = 200, description = "Version details", body = crate::db::models::FileVersion), + (status = 404, description = "Version not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + tag = "Versions" +)] +pub async fn get_version( + State(state): State<SharedState>, + Path((file_id, version)): Path<(Uuid, i32)>, +) -> 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_version(pool, file_id, version).await { + Ok(Some(version)) => Json(version).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Version not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to get version {} for file {}: {}", version, file_id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Restore a file to a previous version. +#[utoipa::path( + post, + path = "/api/v1/files/{id}/versions/restore", + params( + ("id" = Uuid, Path, description = "File ID") + ), + request_body = RestoreVersionRequest, + responses( + (status = 200, description = "File restored to previous version", body = crate::db::models::File), + (status = 404, description = "File or version not found", body = ApiError), + (status = 409, description = "Version conflict", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + tag = "Versions" +)] +pub async fn restore_version( + State(state): State<SharedState>, + Path(file_id): Path<Uuid>, + Json(req): Json<RestoreVersionRequest>, +) -> 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::restore_file_version(pool, file_id, req.target_version, req.current_version).await { + Ok(Some(file)) => { + // Broadcast update notification + state.broadcast_file_update(FileUpdateNotification { + file_id, + version: file.version, + updated_fields: vec!["body".to_string(), "summary".to_string()], + updated_by: "system".to_string(), + }); + Json(file).into_response() + } + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "File or version not found")), + ) + .into_response(), + Err(RepositoryError::VersionConflict { expected, actual }) => { + tracing::info!( + "Version conflict on file {} restore: expected {}, actual {}", + file_id, + expected, + actual + ); + ( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "code": "VERSION_CONFLICT", + "message": format!( + "File was modified by another user. Expected version {}, actual version {}", + expected, actual + ), + "expectedVersion": expected, + "actualVersion": actual, + })), + ) + .into_response() + } + Err(RepositoryError::Database(e)) => { + tracing::error!("Failed to restore file {} to version {}: {}", file_id, req.target_version, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} |
