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