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}