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}