this repo has no description
1use crate::api::error::ApiError;
2use crate::state::AppState;
3use crate::sync::util::assert_repo_availability;
4use axum::{
5 Json,
6 body::Body,
7 extract::{Query, State},
8 http::StatusCode,
9 http::header,
10 response::{IntoResponse, Response},
11};
12use serde::{Deserialize, Serialize};
13use tracing::error;
14
15#[derive(Deserialize)]
16pub struct GetBlobParams {
17 pub did: String,
18 pub cid: String,
19}
20
21pub async fn get_blob(
22 State(state): State<AppState>,
23 Query(params): Query<GetBlobParams>,
24) -> Response {
25 let did = params.did.trim();
26 let cid = params.cid.trim();
27 if did.is_empty() {
28 return ApiError::InvalidRequest("did is required".into()).into_response();
29 }
30 if cid.is_empty() {
31 return ApiError::InvalidRequest("cid is required".into()).into_response();
32 }
33
34 let _account = match assert_repo_availability(&state.db, did, false).await {
35 Ok(a) => a,
36 Err(e) => return e.into_response(),
37 };
38
39 let blob_result = sqlx::query!(
40 "SELECT storage_key, mime_type, size_bytes FROM blobs WHERE cid = $1",
41 cid
42 )
43 .fetch_optional(&state.db)
44 .await;
45 match blob_result {
46 Ok(Some(row)) => {
47 let storage_key = &row.storage_key;
48 let mime_type = &row.mime_type;
49 let size_bytes = row.size_bytes;
50 match state.blob_store.get(storage_key).await {
51 Ok(data) => Response::builder()
52 .status(StatusCode::OK)
53 .header(header::CONTENT_TYPE, mime_type)
54 .header(header::CONTENT_LENGTH, size_bytes.to_string())
55 .header("x-content-type-options", "nosniff")
56 .header("content-security-policy", "default-src 'none'; sandbox")
57 .body(Body::from(data))
58 .unwrap(),
59 Err(e) => {
60 error!("Failed to fetch blob from storage: {:?}", e);
61 ApiError::BlobNotFound(Some("Blob not found in storage".into())).into_response()
62 }
63 }
64 }
65 Ok(None) => ApiError::BlobNotFound(Some("Blob not found".into())).into_response(),
66 Err(e) => {
67 error!("DB error in get_blob: {:?}", e);
68 ApiError::InternalError(Some("Database error".into())).into_response()
69 }
70 }
71}
72
73#[derive(Deserialize)]
74pub struct ListBlobsParams {
75 pub did: String,
76 pub since: Option<String>,
77 pub limit: Option<i64>,
78 pub cursor: Option<String>,
79}
80
81#[derive(Serialize)]
82pub struct ListBlobsOutput {
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub cursor: Option<String>,
85 pub cids: Vec<String>,
86}
87
88pub async fn list_blobs(
89 State(state): State<AppState>,
90 Query(params): Query<ListBlobsParams>,
91) -> Response {
92 let did = params.did.trim();
93 if did.is_empty() {
94 return ApiError::InvalidRequest("did is required".into()).into_response();
95 }
96
97 let account = match assert_repo_availability(&state.db, did, false).await {
98 Ok(a) => a,
99 Err(e) => return e.into_response(),
100 };
101
102 let limit = params.limit.unwrap_or(500).clamp(1, 1000);
103 let cursor_cid = params.cursor.as_deref().unwrap_or("");
104 let user_id = account.user_id;
105
106 let cids_result: Result<Vec<String>, sqlx::Error> = if let Some(since) = ¶ms.since {
107 sqlx::query_scalar!(
108 r#"
109 SELECT DISTINCT unnest(blobs) as "cid!"
110 FROM repo_seq
111 WHERE did = $1 AND rev > $2 AND blobs IS NOT NULL
112 "#,
113 did,
114 since
115 )
116 .fetch_all(&state.db)
117 .await
118 .map(|mut cids| {
119 cids.sort();
120 cids.into_iter()
121 .filter(|c| c.as_str() > cursor_cid)
122 .take((limit + 1) as usize)
123 .collect()
124 })
125 } else {
126 sqlx::query!(
127 r#"
128 SELECT cid FROM blobs
129 WHERE created_by_user = $1 AND cid > $2
130 ORDER BY cid ASC
131 LIMIT $3
132 "#,
133 user_id,
134 cursor_cid,
135 limit + 1
136 )
137 .fetch_all(&state.db)
138 .await
139 .map(|rows| rows.into_iter().map(|r| r.cid).collect())
140 };
141 match cids_result {
142 Ok(cids) => {
143 let has_more = cids.len() as i64 > limit;
144 let cids: Vec<String> = cids.into_iter().take(limit as usize).collect();
145 let next_cursor = if has_more { cids.last().cloned() } else { None };
146 (
147 StatusCode::OK,
148 Json(ListBlobsOutput {
149 cursor: next_cursor,
150 cids,
151 }),
152 )
153 .into_response()
154 }
155 Err(e) => {
156 error!("DB error in list_blobs: {:?}", e);
157 ApiError::InternalError(Some("Database error".into())).into_response()
158 }
159 }
160}