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