~james/core

core/nix/pkgs/attic-server/delete-paths.patch -rw-r--r-- 5.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
From d67b6f339b0ce3725d8daf173e1bd047de7eb9d5 Mon Sep 17 00:00:00 2001
From: dawn <dawn@tangled.org>
Date: Mon, 2 Nov 2026 00:34:58 +0300
Subject: [PATCH] server: add delete-paths endpoint

---
 attic/src/api/v1/delete_paths.rs  | 25 +++++++++
 attic/src/api/v1/mod.rs           |  1 +
 server/src/api/v1/delete_paths.rs | 90 +++++++++++++++++++++++++++++++
 server/src/api/v1/mod.rs          |  2 +
 4 files changed, 118 insertions(+)
 create mode 100644 attic/src/api/v1/delete_paths.rs
 create mode 100644 server/src/api/v1/delete_paths.rs

diff --git a/attic/src/api/v1/delete_paths.rs b/attic/src/api/v1/delete_paths.rs
new file mode 100644
index 0000000..ce2a27d
--- /dev/null
+++ b/attic/src/api/v1/delete_paths.rs
@@ -0,0 +1,25 @@
+//! delete-paths v1
+//!
+//! `POST /_api/v1/delete-paths`
+//!
+//! Requires "delete" permission.
+
+use serde::{Deserialize, Serialize};
+
+use crate::cache::CacheName;
+use crate::nix_store::StorePathHash;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DeletePathsRequest {
+    /// The name of the cache.
+    pub cache: CacheName,
+
+    /// The list of store paths to delete.
+    pub store_path_hashes: Vec<StorePathHash>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DeletePathsResponse {
+    /// The paths that were in the cache and have been deleted.
+    pub deleted: Vec<StorePathHash>,
+}
diff --git a/attic/src/api/v1/mod.rs b/attic/src/api/v1/mod.rs
index 6c0cc4e..eee2036 100644
--- a/attic/src/api/v1/mod.rs
+++ b/attic/src/api/v1/mod.rs
@@ -1,3 +1,4 @@
 pub mod cache_config;
+pub mod delete_paths;
 pub mod get_missing_paths;
 pub mod upload_path;
diff --git a/server/src/api/v1/delete_paths.rs b/server/src/api/v1/delete_paths.rs
new file mode 100644
index 0000000..118d3b8
--- /dev/null
+++ b/server/src/api/v1/delete_paths.rs
@@ -0,0 +1,90 @@
+use std::collections::HashSet;
+
+use anyhow::anyhow;
+use axum::extract::{Extension, Json};
+use sea_orm::entity::prelude::*;
+use sea_orm::TransactionTrait;
+use tracing::instrument;
+
+use crate::database::entity::object::{self, Entity as Object};
+use crate::error::{ErrorKind, ServerError, ServerResult};
+use crate::{RequestState, State};
+use attic::api::v1::delete_paths::{DeletePathsRequest, DeletePathsResponse};
+use attic::nix_store::StorePathHash;
+
+/// The most store paths one request may name.
+const MAX_STORE_PATHS: usize = 10000;
+
+/// Deletes store paths from a cache.
+///
+/// Requires "delete" permission. Only the objects are removed, the
+/// garbage collector reaps the NARs and chunks left without one.
+#[instrument(skip_all, fields(payload))]
+pub(crate) async fn delete_paths(
+    Extension(state): Extension<State>,
+    Extension(req_state): Extension<RequestState>,
+    Json(payload): Json<DeletePathsRequest>,
+) -> ServerResult<Json<DeletePathsResponse>> {
+    let database = state.database().await?;
+    let cache = req_state
+        .auth
+        .auth_cache(database, &payload.cache, |cache, permission| {
+            permission.require_delete()?;
+            Ok(cache)
+        })
+        .await?;
+
+    if payload.store_path_hashes.len() > MAX_STORE_PATHS {
+        return Err(ErrorKind::RequestError(anyhow!(
+            "At most {} store paths can be deleted per request",
+            MAX_STORE_PATHS
+        ))
+        .into());
+    }
+
+    if payload.store_path_hashes.is_empty() {
+        return Ok(Json(DeletePathsResponse { deleted: vec![] }));
+    }
+
+    let requested_hashes: HashSet<String> = payload
+        .store_path_hashes
+        .iter()
+        .map(|h| h.as_str().to_owned())
+        .collect();
+
+    let txn = database
+        .begin()
+        .await
+        .map_err(ServerError::database_error)?;
+
+    let found = Object::find()
+        .filter(object::Column::CacheId.eq(cache.id))
+        .filter(object::Column::StorePathHash.is_in(requested_hashes))
+        .all(&txn)
+        .await
+        .map_err(ServerError::database_error)?;
+
+    if found.is_empty() {
+        return Ok(Json(DeletePathsResponse { deleted: vec![] }));
+    }
+
+    // Delete by ID so an object uploaded after the select is neither
+    // deleted nor missing from the response.
+    Object::delete_many()
+        .filter(object::Column::Id.is_in(found.iter().map(|o| o.id)))
+        .exec(&txn)
+        .await
+        .map_err(ServerError::database_error)?;
+
+    txn.commit().await.map_err(ServerError::database_error)?;
+
+    // Safety: All found hashes come from validated `StorePathHash`es.
+    // No need to pay the cost of checking again
+    #[allow(unsafe_code)]
+    let deleted = found
+        .into_iter()
+        .map(|o| unsafe { StorePathHash::new_unchecked(o.store_path_hash) })
+        .collect();
+
+    Ok(Json(DeletePathsResponse { deleted }))
+}
diff --git a/server/src/api/v1/mod.rs b/server/src/api/v1/mod.rs
index bc34fd7..6be575c 100644
--- a/server/src/api/v1/mod.rs
+++ b/server/src/api/v1/mod.rs
@@ -1,4 +1,5 @@
 mod cache_config;
+mod delete_paths;
 mod get_missing_paths;
 mod upload_path;
 
@@ -13,6 +14,7 @@ pub(crate) fn get_router() -> Router {
             "/_api/v1/get-missing-paths",
             post(get_missing_paths::get_missing_paths),
         )
+        .route("/_api/v1/delete-paths", post(delete_paths::delete_paths))
         .route("/_api/v1/upload-path", put(upload_path::upload_path))
         .route(
             "/:cache/attic-cache-info",
-- 
2.55.0