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}