//! 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, Path(file_id): Path, ) -> 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 = 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, 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, Path(file_id): Path, Json(req): Json, ) -> 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() } Err(RepositoryError::Validation(msg)) => ( StatusCode::BAD_REQUEST, Json(ApiError::new("VALIDATION", &msg)), ) .into_response(), } }