this repo has no description
1use crate::api::error::ApiError;
2use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log};
3use crate::api::repo::record::write::{CommitInfo, prepare_repo_write};
4use crate::delegation::{self, DelegationActionType};
5use crate::repo::tracking::TrackingBlockStore;
6use crate::state::AppState;
7use crate::types::{AtIdentifier, Nsid, Rkey};
8use axum::{
9 Json,
10 extract::State,
11 http::{HeaderMap, StatusCode},
12 response::{IntoResponse, Response},
13};
14use cid::Cid;
15use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
16use serde::{Deserialize, Serialize};
17use serde_json::json;
18use std::str::FromStr;
19use std::sync::Arc;
20use tracing::error;
21
22#[derive(Deserialize)]
23pub struct DeleteRecordInput {
24 pub repo: AtIdentifier,
25 pub collection: Nsid,
26 pub rkey: Rkey,
27 #[serde(rename = "swapRecord")]
28 pub swap_record: Option<String>,
29 #[serde(rename = "swapCommit")]
30 pub swap_commit: Option<String>,
31}
32
33#[derive(Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct DeleteRecordOutput {
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub commit: Option<CommitInfo>,
38}
39
40pub async fn delete_record(
41 State(state): State<AppState>,
42 headers: HeaderMap,
43 axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
44 Json(input): Json<DeleteRecordInput>,
45) -> Response {
46 let auth = match prepare_repo_write(
47 &state,
48 &headers,
49 &input.repo,
50 "POST",
51 &crate::util::build_full_url(&uri.to_string()),
52 )
53 .await
54 {
55 Ok(res) => res,
56 Err(err_res) => return err_res,
57 };
58
59 if let Err(e) = crate::auth::scope_check::check_repo_scope(
60 auth.is_oauth,
61 auth.scope.as_deref(),
62 crate::oauth::RepoAction::Delete,
63 &input.collection,
64 ) {
65 return e;
66 }
67
68 let did = auth.did;
69 let user_id = auth.user_id;
70 let current_root_cid = auth.current_root_cid;
71 let controller_did = auth.controller_did;
72
73 if let Some(swap_commit) = &input.swap_commit
74 && Cid::from_str(swap_commit).ok() != Some(current_root_cid)
75 {
76 return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
77 }
78 let tracking_store = TrackingBlockStore::new(state.block_store.clone());
79 let commit_bytes = match tracking_store.get(¤t_root_cid).await {
80 Ok(Some(b)) => b,
81 _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
82 };
83 let commit = match Commit::from_cbor(&commit_bytes) {
84 Ok(c) => c,
85 _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
86 };
87 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
88 let key = format!("{}/{}", input.collection, input.rkey);
89 if let Some(swap_record_str) = &input.swap_record {
90 let expected_cid = Cid::from_str(swap_record_str).ok();
91 let actual_cid = mst.get(&key).await.ok().flatten();
92 if expected_cid != actual_cid {
93 return ApiError::InvalidSwap(Some(
94 "Record has been modified or does not exist".into(),
95 ))
96 .into_response();
97 }
98 }
99 let prev_record_cid = mst.get(&key).await.ok().flatten();
100 if prev_record_cid.is_none() {
101 return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response();
102 }
103 let new_mst = match mst.delete(&key).await {
104 Ok(m) => m,
105 Err(e) => {
106 error!("Failed to delete from MST: {:?}", e);
107 return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e)))
108 .into_response();
109 }
110 };
111 let new_mst_root = match new_mst.persist().await {
112 Ok(c) => c,
113 Err(e) => {
114 error!("Failed to persist MST: {:?}", e);
115 return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
116 }
117 };
118 let collection_for_audit = input.collection.to_string();
119 let rkey_for_audit = input.rkey.to_string();
120 let op = RecordOp::Delete {
121 collection: input.collection.clone(),
122 rkey: input.rkey.clone(),
123 prev: prev_record_cid,
124 };
125 let mut new_mst_blocks = std::collections::BTreeMap::new();
126 let mut old_mst_blocks = std::collections::BTreeMap::new();
127 if new_mst
128 .blocks_for_path(&key, &mut new_mst_blocks)
129 .await
130 .is_err()
131 {
132 return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
133 .into_response();
134 }
135 if mst
136 .blocks_for_path(&key, &mut old_mst_blocks)
137 .await
138 .is_err()
139 {
140 return ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
141 .into_response();
142 }
143 let mut relevant_blocks = new_mst_blocks.clone();
144 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone())));
145 let written_cids: Vec<Cid> = tracking_store
146 .get_all_relevant_cids()
147 .into_iter()
148 .chain(relevant_blocks.keys().copied())
149 .collect::<std::collections::HashSet<_>>()
150 .into_iter()
151 .collect();
152 let written_cids_str: Vec<String> = written_cids.iter().map(|c| c.to_string()).collect();
153 let obsolete_cids: Vec<Cid> = std::iter::once(current_root_cid)
154 .chain(
155 old_mst_blocks
156 .keys()
157 .filter(|cid| !new_mst_blocks.contains_key(*cid))
158 .copied(),
159 )
160 .chain(prev_record_cid)
161 .collect();
162 let commit_result = match commit_and_log(
163 &state,
164 CommitParams {
165 did: &did,
166 user_id,
167 current_root_cid: Some(current_root_cid),
168 prev_data_cid: Some(commit.data),
169 new_mst_root,
170 ops: vec![op],
171 blocks_cids: &written_cids_str,
172 blobs: &[],
173 obsolete_cids,
174 },
175 )
176 .await
177 {
178 Ok(res) => res,
179 Err(e) if e.contains("ConcurrentModification") => {
180 return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
181 }
182 Err(e) => return ApiError::InternalError(Some(e)).into_response(),
183 };
184
185 if let Some(ref controller) = controller_did {
186 let _ = delegation::log_delegation_action(
187 &state.db,
188 &did,
189 controller,
190 Some(controller),
191 DelegationActionType::RepoWrite,
192 Some(json!({
193 "action": "delete",
194 "collection": collection_for_audit,
195 "rkey": rkey_for_audit
196 })),
197 None,
198 None,
199 )
200 .await;
201 }
202
203 (
204 StatusCode::OK,
205 Json(DeleteRecordOutput {
206 commit: Some(CommitInfo {
207 cid: commit_result.commit_cid.to_string(),
208 rev: commit_result.rev,
209 }),
210 }),
211 )
212 .into_response()
213}