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