this repo has no description
1use crate::api::error::ApiError;
2use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown};
3use crate::state::AppState;
4use crate::sync::car::encode_car_header;
5use crate::sync::util::assert_repo_availability;
6use axum::{
7 Json,
8 extract::{Query, State},
9 http::{HeaderMap, StatusCode},
10 response::{IntoResponse, Response},
11};
12use cid::Cid;
13use ipld_core::ipld::Ipld;
14use jacquard_repo::storage::BlockStore;
15use serde::{Deserialize, Serialize};
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 ApiError::InvalidRequest("did is required".into()).into_response();
52 }
53 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
54 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
55 Ok(a) => a,
56 Err(e) => return e.into_response(),
57 };
58 match account.repo_root_cid {
59 Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(),
60 None => {
61 ApiError::RepoNotFound(Some(format!("Could not find root for DID: {}", did)))
62 .into_response()
63 }
64 }
65}
66
67#[derive(Deserialize)]
68pub struct GetCheckoutParams {
69 pub did: String,
70}
71
72pub async fn get_checkout(
73 State(state): State<AppState>,
74 headers: HeaderMap,
75 Query(params): Query<GetCheckoutParams>,
76) -> Response {
77 let did = params.did.trim();
78 if did.is_empty() {
79 return ApiError::InvalidRequest("did is required".into()).into_response();
80 }
81 let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
82 let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
83 Ok(a) => a,
84 Err(e) => return e.into_response(),
85 };
86 let Some(head_str) = account.repo_root_cid else {
87 return ApiError::RepoNotFound(Some("Repo not initialized".into())).into_response();
88 };
89 let Ok(head_cid) = Cid::from_str(&head_str) else {
90 return ApiError::InternalError(Some("Invalid head CID".into())).into_response();
91 };
92 let Ok(mut car_bytes) = encode_car_header(&head_cid) else {
93 return ApiError::InternalError(Some("Failed to encode CAR header".into())).into_response();
94 };
95 let mut stack = vec![head_cid];
96 let mut visited = std::collections::HashSet::new();
97 let mut remaining = MAX_REPO_BLOCKS_TRAVERSAL;
98 while let Some(cid) = stack.pop() {
99 if visited.contains(&cid) {
100 continue;
101 }
102 visited.insert(cid);
103 if remaining == 0 {
104 break;
105 }
106 remaining -= 1;
107 if let Ok(Some(block)) = state.block_store.get(&cid).await {
108 let cid_bytes = cid.to_bytes();
109 let total_len = cid_bytes.len() + block.len();
110 let mut writer = Vec::new();
111 crate::sync::car::write_varint(&mut writer, total_len as u64)
112 .expect("Writing to Vec<u8> should never fail");
113 writer
114 .write_all(&cid_bytes)
115 .expect("Writing to Vec<u8> should never fail");
116 writer
117 .write_all(&block)
118 .expect("Writing to Vec<u8> should never fail");
119 car_bytes.extend_from_slice(&writer);
120 if let Ok(value) = serde_ipld_dagcbor::from_slice::<Ipld>(&block) {
121 extract_links_ipld(&value, &mut stack);
122 }
123 }
124 }
125 (
126 StatusCode::OK,
127 [(axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car")],
128 car_bytes,
129 )
130 .into_response()
131}
132
133fn extract_links_ipld(value: &Ipld, stack: &mut Vec<Cid>) {
134 match value {
135 Ipld::Link(cid) => {
136 stack.push(*cid);
137 }
138 Ipld::Map(map) => {
139 for v in map.values() {
140 extract_links_ipld(v, stack);
141 }
142 }
143 Ipld::List(arr) => {
144 for v in arr {
145 extract_links_ipld(v, stack);
146 }
147 }
148 _ => {}
149 }
150}