Skip to main content

max / makenotwork

4.1 KB · 140 lines History Blame Raw
1 //! Version management handlers for items.
2
3 use axum::{
4 extract::{Path, State},
5 response::IntoResponse,
6 Json,
7 };
8 use serde::{Deserialize, Serialize};
9
10 use crate::{
11 auth::AuthUser,
12 db::{self, ItemId, VersionId},
13 error::{AppError, Result},
14 types::ListResponse,
15 validation,
16 AppState,
17 };
18
19 use super::super::verify_item_ownership;
20
21 /// JSON input for creating a new version of an item.
22 #[derive(Debug, Deserialize)]
23 pub struct CreateVersionRequest {
24 pub version_number: String,
25 pub changelog: Option<String>,
26 pub file_url: Option<String>,
27 pub file_size_bytes: Option<i64>,
28 pub file_name: Option<String>,
29 pub label: Option<String>,
30 }
31
32 /// JSON response representing an item version.
33 #[derive(Debug, Serialize)]
34 pub struct VersionResponse {
35 pub id: VersionId,
36 pub item_id: ItemId,
37 pub version_number: String,
38 pub changelog: Option<String>,
39 pub file_url: Option<String>,
40 pub download_count: i32,
41 pub is_current: bool,
42 }
43
44 /// Create a new version for an owned item.
45 #[tracing::instrument(skip_all, name = "items::create_version")]
46 pub(in crate::routes::api) async fn create_version(
47 State(state): State<AppState>,
48 AuthUser(user): AuthUser,
49 Path(item_id): Path<ItemId>,
50 Json(req): Json<CreateVersionRequest>,
51 ) -> Result<impl IntoResponse> {
52 user.check_not_suspended()?;
53 // Validate input
54 validation::validate_version_number(&req.version_number)?;
55 if let Some(ref changelog) = req.changelog {
56 validation::validate_changelog(changelog)?;
57 }
58
59 let (item, _) = verify_item_ownership(&state, item_id, user.id).await?;
60
61 let version = db::versions::create_version(
62 &state.db,
63 item_id,
64 &req.version_number,
65 req.changelog.as_deref(),
66 req.file_url.as_deref(),
67 req.file_size_bytes,
68 req.file_name.as_deref(),
69 req.label.as_deref(),
70 )
71 .await?;
72
73 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
74
75 Ok(Json(VersionResponse {
76 id: version.id,
77 item_id: version.item_id,
78 version_number: version.version_number,
79 changelog: version.changelog,
80 file_url: version.file_url,
81 download_count: version.download_count,
82 is_current: version.is_current,
83 }))
84 }
85
86 /// Delete a version from an owned item. Enqueues S3 deletion if file exists.
87 #[tracing::instrument(skip_all, name = "items::delete_version")]
88 pub(in crate::routes::api) async fn delete_version(
89 State(state): State<AppState>,
90 AuthUser(user): AuthUser,
91 Path((item_id, version_id)): Path<(ItemId, VersionId)>,
92 ) -> Result<impl IntoResponse> {
93 user.check_not_suspended()?;
94 verify_item_ownership(&state, item_id, user.id).await?;
95
96 let version = db::versions::get_version_by_id(&state.db, version_id)
97 .await?
98 .ok_or(AppError::NotFound)?;
99
100 if version.item_id != item_id {
101 return Err(AppError::NotFound);
102 }
103
104 // delete_version handles storage decrement + S3 enqueue atomically
105 let _ = version;
106 db::versions::delete_version(&state.db, version_id).await?;
107
108 Ok(axum::http::StatusCode::OK)
109 }
110
111 /// List all versions for a given item (public items only; drafts return 404).
112 #[tracing::instrument(skip_all, name = "items::list_versions")]
113 pub(in crate::routes::api) async fn list_versions(
114 State(state): State<AppState>,
115 Path(item_id): Path<ItemId>,
116 ) -> Result<impl IntoResponse> {
117 let item = db::items::get_item_by_id(&state.db, item_id)
118 .await?
119 .ok_or(AppError::NotFound)?;
120 if !item.is_public {
121 return Err(AppError::NotFound);
122 }
123 let versions = db::versions::get_versions_by_item(&state.db, item_id).await?;
124
125 let data: Vec<VersionResponse> = versions
126 .into_iter()
127 .map(|v| VersionResponse {
128 id: v.id,
129 item_id: v.item_id,
130 version_number: v.version_number,
131 changelog: v.changelog,
132 file_url: v.file_url,
133 download_count: v.download_count,
134 is_current: v.is_current,
135 })
136 .collect();
137
138 Ok(Json(ListResponse { data }))
139 }
140