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 (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response();
131 }
132 let new_mst = match mst.delete(&key).await {
133 Ok(m) => m,
134 Err(e) => {
135 error!("Failed to delete from MST: {:?}", e);
136 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response();
137 }
138 };
139 let new_mst_root = match new_mst.persist().await {
140 Ok(c) => c,
141 Err(e) => {
142 error!("Failed to persist MST: {:?}", e);
143 return (
144 StatusCode::INTERNAL_SERVER_ERROR,
145 Json(json!({"error": "InternalError", "message": "Failed to persist MST"})),
146 )
147 .into_response();
148 }
149 };
150 let collection_for_audit = input.collection.clone();
151 let rkey_for_audit = input.rkey.clone();
152 let op = RecordOp::Delete {
153 collection: input.collection,
154 rkey: input.rkey,
155 prev: prev_record_cid,
156 };
157 let mut relevant_blocks = std::collections::BTreeMap::new();
158 if new_mst
159 .blocks_for_path(&key, &mut relevant_blocks)
160 .await
161 .is_err()
162 {
163 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response();
164 }
165 if mst
166 .blocks_for_path(&key, &mut relevant_blocks)
167 .await
168 .is_err()
169 {
170 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response();
171 }
172 let mut written_cids = tracking_store.get_all_relevant_cids();
173 for cid in relevant_blocks.keys() {
174 if !written_cids.contains(cid) {
175 written_cids.push(*cid);
176 }
177 }
178 let written_cids_str = written_cids
179 .iter()
180 .map(|c| c.to_string())
181 .collect::<Vec<_>>();
182 let commit_result = match commit_and_log(
183 &state,
184 CommitParams {
185 did: &did,
186 user_id,
187 current_root_cid: Some(current_root_cid),
188 prev_data_cid: Some(commit.data),
189 new_mst_root,
190 ops: vec![op],
191 blocks_cids: &written_cids_str,
192 blobs: &[],
193 },
194 )
195 .await
196 {
197 Ok(res) => res,
198 Err(e) => {
199 return (
200 StatusCode::INTERNAL_SERVER_ERROR,
201 Json(json!({"error": "InternalError", "message": e})),
202 )
203 .into_response();
204 }
205 };
206
207 if let Some(ref controller) = controller_did {
208 let _ = delegation::log_delegation_action(
209 &state.db,
210 &did,
211 controller,
212 Some(controller),
213 DelegationActionType::RepoWrite,
214 Some(json!({
215 "action": "delete",
216 "collection": collection_for_audit,
217 "rkey": rkey_for_audit
218 })),
219 None,
220 None,
221 )
222 .await;
223 }
224
225 (
226 StatusCode::OK,
227 Json(DeleteRecordOutput {
228 commit: Some(CommitInfo {
229 cid: commit_result.commit_cid.to_string(),
230 rev: commit_result.rev,
231 }),
232 }),
233 )
234 .into_response()
235}