this repo has no description
1use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown};
2use crate::state::AppState;
3use crate::sync::car::encode_car_header;
4use crate::sync::util::assert_repo_availability;
5use axum::{
6 Json,
7 extract::{Query, State},
8 http::{HeaderMap, StatusCode},
9 response::{IntoResponse, Response},
10};
11use cid::Cid;
12use ipld_core::ipld::Ipld;
13use jacquard_repo::storage::BlockStore;
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use std::io::Write;
17use std::str::FromStr;
18
19const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000;
20
21async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool {
22 let token = match extract_bearer_token_from_header(
23 headers.get("Authorization").and_then(|h| h.to_str().ok()),
24 ) {
25 Some(t) => t,
26 None => return false,
27 };
28 match validate_bearer_token_allow_takendown(&state.db, &token).await {
29 Ok(auth_user) => auth_user.is_admin || auth_user.did == did,
30 Err(_) => false,
31 }
32}
33
34#[derive(Deserialize)]
35pub struct GetHeadParams {
36 pub did: String,
37}
38
39#[derive(Serialize)]
40pub struct GetHeadOutput {
41 pub root: String,
42}
43
44pub async fn get_head(
45 State(state): State<AppState>,
46 headers: HeaderMap,
47 Query(params): Query<GetHeadParams>,
48) -> Response {
49 let did = params.did.trim();
50 if did.is_empty() {
51 return (
52 StatusCode::BAD_REQUEST,
53 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
54 )
55 .into_response();
56 }
57 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
58 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
59 Ok(a) => a,
60 Err(e) => return e.into_response(),
61 };
62 match account.repo_root_cid {
63 Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(),
64 None => (
65 StatusCode::BAD_REQUEST,
66 Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})),
67 )
68 .into_response(),
69 }
70}
71
72#[derive(Deserialize)]
73pub struct GetCheckoutParams {
74 pub did: String,
75}
76
77pub async fn get_checkout(
78 State(state): State<AppState>,
79 headers: HeaderMap,
80 Query(params): Query<GetCheckoutParams>,
81) -> Response {
82 let did = params.did.trim();
83 if did.is_empty() {
84 return (
85 StatusCode::BAD_REQUEST,
86 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
87 )
88 .into_response();
89 }
90 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
91 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
92 Ok(a) => a,
93 Err(e) => return e.into_response(),
94 };
95 let head_str = match account.repo_root_cid {
96 Some(r) => r,
97 None => {
98 return (
99 StatusCode::BAD_REQUEST,
100 Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})),
101 )
102 .into_response();
103 }
104 };
105 let head_cid = match Cid::from_str(&head_str) {
106 Ok(c) => c,
107 Err(_) => {
108 return (
109 StatusCode::INTERNAL_SERVER_ERROR,
110 Json(json!({"error": "InternalError", "message": "Invalid head CID"})),
111 )
112 .into_response();
113 }
114 };
115 let mut car_bytes = match encode_car_header(&head_cid) {
116 Ok(h) => h,
117 Err(e) => {
118 return (
119 StatusCode::INTERNAL_SERVER_ERROR,
120 Json(json!({"error": "InternalError", "message": format!("Failed to encode CAR header: {}", e)})),
121 )
122 .into_response();
123 }
124 };
125 let mut stack = vec![head_cid];
126 let mut visited = std::collections::HashSet::new();
127 let mut remaining = MAX_REPO_BLOCKS_TRAVERSAL;
128 while let Some(cid) = stack.pop() {
129 if visited.contains(&cid) {
130 continue;
131 }
132 visited.insert(cid);
133 if remaining == 0 {
134 break;
135 }
136 remaining -= 1;
137 if let Ok(Some(block)) = state.block_store.get(&cid).await {
138 let cid_bytes = cid.to_bytes();
139 let total_len = cid_bytes.len() + block.len();
140 let mut writer = Vec::new();
141 crate::sync::car::write_varint(&mut writer, total_len as u64)
142 .expect("Writing to Vec<u8> should never fail");
143 writer
144 .write_all(&cid_bytes)
145 .expect("Writing to Vec<u8> should never fail");
146 writer
147 .write_all(&block)
148 .expect("Writing to Vec<u8> should never fail");
149 car_bytes.extend_from_slice(&writer);
150 if let Ok(value) = serde_ipld_dagcbor::from_slice::<Ipld>(&block) {
151 extract_links_ipld(&value, &mut stack);
152 }
153 }
154 }
155 (
156 StatusCode::OK,
157 [(axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car")],
158 car_bytes,
159 )
160 .into_response()
161}
162
163fn extract_links_ipld(value: &Ipld, stack: &mut Vec<Cid>) {
164 match value {
165 Ipld::Link(cid) => {
166 stack.push(*cid);
167 }
168 Ipld::Map(map) => {
169 for v in map.values() {
170 extract_links_ipld(v, stack);
171 }
172 }
173 Ipld::List(arr) => {
174 for v in arr {
175 extract_links_ipld(v, stack);
176 }
177 }
178 _ => {}
179 }
180}