Something went wrong. Try again.
forked from tangled.org/core
Something went wrong. Try again.
5.4 kB · 171 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172From d67b6f339b0ce3725d8daf173e1bd047de7eb9d5 Mon Sep 17 00:00:00 2001From: dawn <dawn@tangled.org>Date: Mon, 2 Nov 2026 00:34:58 +0300Subject: [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.rsnew file mode 100644index 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.rsindex 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.rsnew file mode 100644index 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.rsindex 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