From d67b6f339b0ce3725d8daf173e1bd047de7eb9d5 Mon Sep 17 00:00:00 2001 From: dawn 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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DeletePathsResponse { + /// The paths that were in the cache and have been deleted. + pub deleted: Vec, +} 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, + Extension(req_state): Extension, + Json(payload): Json, +) -> ServerResult> { + 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 = 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