summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/versions.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/versions.rs')
-rw-r--r--makima/src/server/handlers/versions.rs207
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()
+ }
+ }
+}