this repo has no description
1use crate::state::AppState;
2use crate::sync::car::encode_car_header;
3use axum::{
4 Json,
5 extract::{Query, State},
6 http::StatusCode,
7 response::{IntoResponse, Response},
8};
9use cid::Cid;
10use ipld_core::ipld::Ipld;
11use jacquard_repo::storage::BlockStore;
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use std::io::Write;
15use std::str::FromStr;
16use tracing::error;
17
18const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000;
19
20#[derive(Deserialize)]
21pub struct GetHeadParams {
22 pub did: String,
23}
24
25#[derive(Serialize)]
26pub struct GetHeadOutput {
27 pub root: String,
28}
29
30pub async fn get_head(
31 State(state): State<AppState>,
32 Query(params): Query<GetHeadParams>,
33) -> Response {
34 let did = params.did.trim();
35
36 if did.is_empty() {
37 return (
38 StatusCode::BAD_REQUEST,
39 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
40 )
41 .into_response();
42 }
43
44 let result = sqlx::query!(
45 r#"
46 SELECT r.repo_root_cid
47 FROM repos r
48 JOIN users u ON r.user_id = u.id
49 WHERE u.did = $1
50 "#,
51 did
52 )
53 .fetch_optional(&state.db)
54 .await;
55
56 match result {
57 Ok(Some(row)) => (StatusCode::OK, Json(GetHeadOutput { root: row.repo_root_cid })).into_response(),
58 Ok(None) => (
59 StatusCode::BAD_REQUEST,
60 Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})),
61 )
62 .into_response(),
63 Err(e) => {
64 error!("DB error in get_head: {:?}", e);
65 (
66 StatusCode::INTERNAL_SERVER_ERROR,
67 Json(json!({"error": "InternalError"})),
68 )
69 .into_response()
70 }
71 }
72}
73
74#[derive(Deserialize)]
75pub struct GetCheckoutParams {
76 pub did: String,
77}
78
79pub async fn get_checkout(
80 State(state): State<AppState>,
81 Query(params): Query<GetCheckoutParams>,
82) -> Response {
83 let did = params.did.trim();
84
85 if did.is_empty() {
86 return (
87 StatusCode::BAD_REQUEST,
88 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
89 )
90 .into_response();
91 }
92
93 let repo_row = sqlx::query!(
94 r#"
95 SELECT r.repo_root_cid
96 FROM repos r
97 JOIN users u ON u.id = r.user_id
98 WHERE u.did = $1
99 "#,
100 did
101 )
102 .fetch_optional(&state.db)
103 .await
104 .unwrap_or(None);
105
106 let head_str = match repo_row {
107 Some(r) => r.repo_root_cid,
108 None => {
109 let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
110 .fetch_optional(&state.db)
111 .await
112 .unwrap_or(None);
113
114 if user_exists.is_none() {
115 return (
116 StatusCode::NOT_FOUND,
117 Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),
118 )
119 .into_response();
120 } else {
121 return (
122 StatusCode::NOT_FOUND,
123 Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})),
124 )
125 .into_response();
126 }
127 }
128 };
129
130 let head_cid = match Cid::from_str(&head_str) {
131 Ok(c) => c,
132 Err(_) => {
133 return (
134 StatusCode::INTERNAL_SERVER_ERROR,
135 Json(json!({"error": "InternalError", "message": "Invalid head CID"})),
136 )
137 .into_response();
138 }
139 };
140
141 let mut car_bytes = match encode_car_header(&head_cid) {
142 Ok(h) => h,
143 Err(e) => {
144 return (
145 StatusCode::INTERNAL_SERVER_ERROR,
146 Json(json!({"error": "InternalError", "message": format!("Failed to encode CAR header: {}", e)})),
147 )
148 .into_response();
149 }
150 };
151
152 let mut stack = vec![head_cid];
153 let mut visited = std::collections::HashSet::new();
154 let mut remaining = MAX_REPO_BLOCKS_TRAVERSAL;
155
156 while let Some(cid) = stack.pop() {
157 if visited.contains(&cid) {
158 continue;
159 }
160 visited.insert(cid);
161 if remaining == 0 {
162 break;
163 }
164 remaining -= 1;
165
166 if let Ok(Some(block)) = state.block_store.get(&cid).await {
167 let cid_bytes = cid.to_bytes();
168 let total_len = cid_bytes.len() + block.len();
169 let mut writer = Vec::new();
170 crate::sync::car::write_varint(&mut writer, total_len as u64)
171 .expect("Writing to Vec<u8> should never fail");
172 writer.write_all(&cid_bytes)
173 .expect("Writing to Vec<u8> should never fail");
174 writer.write_all(&block)
175 .expect("Writing to Vec<u8> should never fail");
176 car_bytes.extend_from_slice(&writer);
177
178 if let Ok(value) = serde_ipld_dagcbor::from_slice::<Ipld>(&block) {
179 extract_links_ipld(&value, &mut stack);
180 }
181 }
182 }
183
184 (
185 StatusCode::OK,
186 [(axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car")],
187 car_bytes,
188 )
189 .into_response()
190}
191
192fn extract_links_ipld(value: &Ipld, stack: &mut Vec<Cid>) {
193 match value {
194 Ipld::Link(cid) => {
195 stack.push(*cid);
196 }
197 Ipld::Map(map) => {
198 for v in map.values() {
199 extract_links_ipld(v, stack);
200 }
201 }
202 Ipld::List(arr) => {
203 for v in arr {
204 extract_links_ipld(v, stack);
205 }
206 }
207 _ => {}
208 }
209}