Something went wrong. Try again.
Paper Mario atproto mod hosting site starhaven.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132//! Proxies blobs from an author's own PDS.//!//! `GET /blob/{did}/{cid}` fetches a blob so pages can link to it without//! knowing which PDS the author's data lives on.//!//! `POST /blob` uploads a blob to the logged-in user's PDS and returns its//! blob ref as JSON. The caller still has to write it into a record.
use axum::body::Bytes;use axum::extract::{Path, State};use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};use axum::http::HeaderMap;use axum::response::{IntoResponse, Response};use axum::routing::{get, post};use axum::{Json, Router};use serde::Serialize;
use crate::atproto::actor::resolve_document;use crate::atproto::pds;use crate::error::AppError;use crate::oauth::session::SessionCookie;use crate::state::AppState;
pub fn router() -> Router<AppState> { Router::new() .route("/blob/{did}/{cid}", get(handle_get_blob)) .route("/blob", post(handle_post_blob))}
/// A blob is addressed by its own content hash, so once fetched it can never/// change under the same URL.const CACHE_FOREVER: &str = "public, max-age=31536000, immutable";
async fn handle_get_blob( Path((did, cid)): Path<(String, String)>, State(state): State<AppState>,) -> Result<Response, AppError> { let document = resolve_document(&state, &did) .await .map_err(AppError::Internal)?; let pds_endpoint = document .pds_endpoints() .first() .ok_or(AppError::NotFound)? .to_string();
let url = format!("{pds_endpoint}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}"); let response = state .http_client .get(&url) .send() .await .map_err(|err| AppError::Internal(err.into()))?;
if !response.status().is_success() { return Err(AppError::NotFound); } let content_type = response .headers() .get(CONTENT_TYPE) .cloned() .unwrap_or_else(|| { "application/octet-stream" .parse() .expect("valid header value") }); let bytes = response .bytes() .await .map_err(|err| AppError::Internal(err.into()))?;
Ok(( [ (CONTENT_TYPE, content_type), ( CACHE_CONTROL, CACHE_FOREVER.parse().expect("valid header value"), ), // Prevent other origins from embedding these blobs. ( "cross-origin-resource-policy" .parse() .expect("valid header name"), "same-origin".parse().expect("valid header value"), ), ], bytes, ) .into_response())}
const MAX_BLOB_BYTES: usize = 10 * 1024 * 1024;
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct BlobRef { pub cid: String, pub mime_type: String, pub size: u64,}
async fn handle_post_blob( State(state): State<AppState>, session: SessionCookie, headers: HeaderMap, bytes: Bytes,) -> Result<Json<BlobRef>, AppError> { let mime_type = headers .get(CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .unwrap_or("application/octet-stream");
if bytes.is_empty() { return Err(AppError::BadRequest("The file is empty".to_string())); } if bytes.len() > MAX_BLOB_BYTES { return Err(AppError::BadRequest( "The file is too large (10MB limit)".to_string(), )); }
let blob = pds::upload_blob(&state, &session, bytes, mime_type) .await .map_err(AppError::Internal)?;
Ok(Json(BlobRef { cid: blob.inner.ref_.link, mime_type: blob.inner.mime_type, size: blob.inner.size, }))}