max / makenotwork
8 files changed,
+71 insertions,
-2 deletions
| @@ -210,6 +210,16 @@ pub async fn update_version_file( | |||
| 210 | 210 | Ok(version) | |
| 211 | 211 | } | |
| 212 | 212 | ||
| 213 | + | /// Delete a version by ID. | |
| 214 | + | #[tracing::instrument(skip_all)] | |
| 215 | + | pub async fn delete_version(pool: &PgPool, version_id: VersionId) -> Result<()> { | |
| 216 | + | sqlx::query("DELETE FROM versions WHERE id = $1") | |
| 217 | + | .bind(version_id) | |
| 218 | + | .execute(pool) | |
| 219 | + | .await?; | |
| 220 | + | Ok(()) | |
| 221 | + | } | |
| 222 | + | ||
| 213 | 223 | /// Sum all version file sizes for a given item (for storage decrement on item delete). | |
| 214 | 224 | #[tracing::instrument(skip_all)] | |
| 215 | 225 | pub async fn sum_file_sizes_for_item(pool: &PgPool, item_id: super::ItemId) -> Result<i64> { |
| @@ -17,4 +17,4 @@ pub(super) use refund::refund_transaction; | |||
| 17 | 17 | pub(super) use sections::{create_section, delete_section, list_sections, reorder_sections, update_section}; | |
| 18 | 18 | pub(super) use tags::{add_tag, remove_tag, set_primary_tag}; | |
| 19 | 19 | pub(super) use crud::update_item_text; | |
| 20 | - | pub(super) use versions::{create_version, list_versions}; | |
| 20 | + | pub(super) use versions::{create_version, delete_version, list_versions}; |
| @@ -81,6 +81,41 @@ pub(in crate::routes::api) async fn create_version( | |||
| 81 | 81 | })) | |
| 82 | 82 | } | |
| 83 | 83 | ||
| 84 | + | /// Delete a version from an owned item. Enqueues S3 deletion if file exists. | |
| 85 | + | #[tracing::instrument(skip_all, name = "items::delete_version")] | |
| 86 | + | pub(in crate::routes::api) async fn delete_version( | |
| 87 | + | State(state): State<AppState>, | |
| 88 | + | AuthUser(user): AuthUser, | |
| 89 | + | Path((item_id, version_id)): Path<(ItemId, VersionId)>, | |
| 90 | + | ) -> Result<impl IntoResponse> { | |
| 91 | + | user.check_not_suspended()?; | |
| 92 | + | verify_item_ownership(&state, item_id, user.id).await?; | |
| 93 | + | ||
| 94 | + | let version = db::versions::get_version_by_id(&state.db, version_id) | |
| 95 | + | .await? | |
| 96 | + | .ok_or(AppError::NotFound)?; | |
| 97 | + | ||
| 98 | + | if version.item_id != item_id { | |
| 99 | + | return Err(AppError::NotFound); | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | if let Some(ref s3_key) = version.s3_key { | |
| 103 | + | if let Err(e) = db::pending_s3_deletions::enqueue_deletions( | |
| 104 | + | &state.db, | |
| 105 | + | &[(s3_key.clone(), "main".to_string())], | |
| 106 | + | "version_delete", | |
| 107 | + | ) | |
| 108 | + | .await | |
| 109 | + | { | |
| 110 | + | tracing::warn!(error = ?e, "failed to enqueue S3 deletion for version"); | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | db::versions::delete_version(&state.db, version_id).await?; | |
| 115 | + | ||
| 116 | + | Ok(axum::http::StatusCode::OK) | |
| 117 | + | } | |
| 118 | + | ||
| 84 | 119 | /// List all versions for a given item (public items only; drafts return 404). | |
| 85 | 120 | #[tracing::instrument(skip_all, name = "items::list_versions")] | |
| 86 | 121 | pub(in crate::routes::api) async fn list_versions( |
| @@ -249,6 +249,7 @@ pub fn api_routes() -> Router<AppState> { | |||
| 249 | 249 | .route("/api/items/{id}/text", put(items::update_item_text)) | |
| 250 | 250 | // Version routes | |
| 251 | 251 | .route("/api/items/{id}/versions", post(items::create_version)) | |
| 252 | + | .route("/api/items/{id}/versions/{version_id}", delete(items::delete_version)) | |
| 252 | 253 | // Custom link routes | |
| 253 | 254 | .route("/api/links", post(links::create_link)) | |
| 254 | 255 | .route("/api/links/{id}", put(links::update_link)) |
| @@ -446,6 +446,7 @@ impl Version { | |||
| 446 | 446 | }, | |
| 447 | 447 | is_current: v.is_current, | |
| 448 | 448 | has_file, | |
| 449 | + | file_name: v.file_name.clone(), | |
| 449 | 450 | } | |
| 450 | 451 | } | |
| 451 | 452 | } |
| @@ -731,6 +731,7 @@ pub struct Version { | |||
| 731 | 731 | pub status: String, | |
| 732 | 732 | pub is_current: bool, | |
| 733 | 733 | pub has_file: bool, | |
| 734 | + | pub file_name: Option<String>, | |
| 734 | 735 | } | |
| 735 | 736 | ||
| 736 | 737 | /// Row data for displaying a license key in the creator dashboard. |
| @@ -213,6 +213,25 @@ | |||
| 213 | 213 | }); | |
| 214 | 214 | }); | |
| 215 | 215 | ||
| 216 | + | document.querySelectorAll('.delete-version-btn').forEach(function(btn) { | |
| 217 | + | btn.addEventListener('click', function() { | |
| 218 | + | if (!confirm('Delete this version?')) return; | |
| 219 | + | var versionId = btn.dataset.versionId; | |
| 220 | + | fetch('/api/items/' + itemId + '/versions/' + versionId, { | |
| 221 | + | method: 'DELETE', | |
| 222 | + | headers: csrfHeaders() | |
| 223 | + | }) | |
| 224 | + | .then(function(res) { | |
| 225 | + | if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) { | |
| 226 | + | throw new Error(d.error || 'Failed to delete version'); | |
| 227 | + | }); | |
| 228 | + | var row = btn.closest('tr'); | |
| 229 | + | if (row) row.remove(); | |
| 230 | + | }) | |
| 231 | + | .catch(function(err) { showToast(err.message || 'Failed to delete version'); }); | |
| 232 | + | }); | |
| 233 | + | }); | |
| 234 | + | ||
| 216 | 235 | document.getElementById('cancel-version-upload-btn').addEventListener('click', function() { | |
| 217 | 236 | uploader.cancel(); | |
| 218 | 237 | resetVersionUpload(); |
| @@ -25,7 +25,7 @@ | |||
| 25 | 25 | <tr> | |
| 26 | 26 | <td><span class="badge{% if version.is_current %} current{% endif %}">v{{ version.number }}</span></td> | |
| 27 | 27 | <td>{{ version.uploaded_date }}</td> | |
| 28 | - | <td>{{ version.file_count }}</td> | |
| 28 | + | <td>{% if version.has_file %}{% match version.file_name %}{% when Some with (name) %}{{ name }}{% when None %}1 file{% endmatch %}{% else %}No file{% endif %}</td> | |
| 29 | 29 | <td>{{ version.size }}</td> | |
| 30 | 30 | <td>{{ version.downloads }}</td> | |
| 31 | 31 | <td>{{ version.status }}</td> | |
| @@ -37,6 +37,8 @@ | |||
| 37 | 37 | <button class="secondary upload-to-version-btn" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;" | |
| 38 | 38 | data-version-id="{{ version.id }}">Upload File</button> | |
| 39 | 39 | {% endif %} | |
| 40 | + | <button class="secondary delete-version-btn" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;" | |
| 41 | + | data-version-id="{{ version.id }}">Delete</button> | |
| 40 | 42 | </td> | |
| 41 | 43 | </tr> | |
| 42 | 44 | {% endfor %} |