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}