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