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