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 if crate::util::is_account_migrated(&state.db, &auth.did)
69 .await
70 .unwrap_or(false)
71 {
72 return ApiError::AccountMigrated.into_response();
73 }
74
75 let did = auth.did;
76 let user_id = auth.user_id;
77 let current_root_cid = auth.current_root_cid;
78 let controller_did = auth.controller_did;
79
80 if let Some(swap_commit) = &input.swap_commit
81 && Cid::from_str(swap_commit).ok() != Some(current_root_cid)
82 {
83 return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
84 }
85 let tracking_store = TrackingBlockStore::new(state.block_store.clone());
86 let commit_bytes = match tracking_store.get(¤t_root_cid).await {
87 Ok(Some(b)) => b,
88 _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
89 };
90 let commit = match Commit::from_cbor(&commit_bytes) {
91 Ok(c) => c,
92 _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
93 };
94 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
95 let key = format!("{}/{}", input.collection, input.rkey);
96 if let Some(swap_record_str) = &input.swap_record {
97 let expected_cid = Cid::from_str(swap_record_str).ok();
98 let actual_cid = mst.get(&key).await.ok().flatten();
99 if expected_cid != actual_cid {
100 return ApiError::InvalidSwap(Some(
101 "Record has been modified or does not exist".into(),
102 ))
103 .into_response();
104 }
105 }
106 let prev_record_cid = mst.get(&key).await.ok().flatten();
107 if prev_record_cid.is_none() {
108 return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response();
109 }
110 let new_mst = match mst.delete(&key).await {
111 Ok(m) => m,
112 Err(e) => {
113 error!("Failed to delete from MST: {:?}", e);
114 return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e)))
115 .into_response();
116 }
117 };
118 let new_mst_root = match new_mst.persist().await {
119 Ok(c) => c,
120 Err(e) => {
121 error!("Failed to persist MST: {:?}", e);
122 return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
123 }
124 };
125 let collection_for_audit = input.collection.to_string();
126 let rkey_for_audit = input.rkey.to_string();
127 let op = RecordOp::Delete {
128 collection: input.collection.to_string(),
129 rkey: rkey_for_audit.clone(),
130 prev: prev_record_cid,
131 };
132 let mut new_mst_blocks = std::collections::BTreeMap::new();
133 let mut old_mst_blocks = std::collections::BTreeMap::new();
134 if new_mst
135 .blocks_for_path(&key, &mut new_mst_blocks)
136 .await
137 .is_err()
138 {
139 return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
140 .into_response();
141 }
142 if mst
143 .blocks_for_path(&key, &mut old_mst_blocks)
144 .await
145 .is_err()
146 {
147 return ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
148 .into_response();
149 }
150 let mut relevant_blocks = new_mst_blocks.clone();
151 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone())));
152 let written_cids: Vec<Cid> = tracking_store
153 .get_all_relevant_cids()
154 .into_iter()
155 .chain(relevant_blocks.keys().copied())
156 .collect::<std::collections::HashSet<_>>()
157 .into_iter()
158 .collect();
159 let written_cids_str: Vec<String> = written_cids.iter().map(|c| c.to_string()).collect();
160 let obsolete_cids: Vec<Cid> = std::iter::once(current_root_cid)
161 .chain(
162 old_mst_blocks
163 .keys()
164 .filter(|cid| !new_mst_blocks.contains_key(*cid))
165 .copied(),
166 )
167 .chain(prev_record_cid)
168 .collect();
169 let commit_result = match commit_and_log(
170 &state,
171 CommitParams {
172 did: &did,
173 user_id,
174 current_root_cid: Some(current_root_cid),
175 prev_data_cid: Some(commit.data),
176 new_mst_root,
177 ops: vec![op],
178 blocks_cids: &written_cids_str,
179 blobs: &[],
180 obsolete_cids,
181 },
182 )
183 .await
184 {
185 Ok(res) => res,
186 Err(e) => return ApiError::InternalError(Some(e)).into_response(),
187 };
188
189 if let Some(ref controller) = controller_did {
190 let _ = delegation::log_delegation_action(
191 &state.db,
192 &did,
193 controller,
194 Some(controller),
195 DelegationActionType::RepoWrite,
196 Some(json!({
197 "action": "delete",
198 "collection": collection_for_audit,
199 "rkey": rkey_for_audit
200 })),
201 None,
202 None,
203 )
204 .await;
205 }
206
207 (
208 StatusCode::OK,
209 Json(DeleteRecordOutput {
210 commit: Some(CommitInfo {
211 cid: commit_result.commit_cid.to_string(),
212 rev: commit_result.rev,
213 }),
214 }),
215 )
216 .into_response()
217}