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