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