diff options
Diffstat (limited to 'crates/recluse/src/controller_backend.rs')
| -rw-r--r-- | crates/recluse/src/controller_backend.rs | 111 | +111 −0 |
1 files changed, 111 insertions, 0 deletions
diff --git a/crates/recluse/src/controller_backend.rs b/crates/recluse/src/controller_backend.rs new file mode 100644 --- /dev/null +++ b/crates/recluse/src/controller_backend.rs @@ -0,0 +1,111 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::sync::Arc; + +use axum::{Router, body, extract, http, response, routing}; +use tracing::error; + +use crate::proxy; +use crate::storage; +use repos::{Backend, BackendError, BackendSpec, ResolvedFile}; + +/// Generic controller for backend HTTP handling. +pub struct BackendController<S: BackendSpec> { + backend: Arc<Backend<S>>, + storage: Arc<storage::StorageService>, + upstream: Arc<proxy::ProxyService>, +} + +impl<S: BackendSpec> BackendController<S> { + pub fn new( + backend: Arc<Backend<S>>, + storage: Arc<storage::StorageService>, + upstream: Arc<proxy::ProxyService>, + ) -> Self { + Self { + backend, + storage, + upstream, + } + } + + pub fn router(self: Arc<Self>) -> Router { + Router::new() + .route("/{filename}", routing::get(Self::handle)) + .with_state(self) + } + + async fn handle( + extract::State(controller): extract::State<Arc<Self>>, + extract::Path(filename): extract::Path<String>, + ) -> Result<response::Response, http::StatusCode> { + let (url, mime) = match controller.backend.resolve_file(&filename).await { + Ok(ResolvedFile::Content { data, mime }) => { + return Ok(Self::build_response(http::StatusCode::OK, data, mime)); + } + Ok(ResolvedFile::Upstream { url, mime }) => (url, mime), + Err(BackendError::NotFound) => { + error!(backend = S::ID, filename, "file not found"); + return Err(http::StatusCode::NOT_FOUND); + } + Err(e) => { + error!(backend = S::ID, filename, "error resolving file: {e}"); + return Err(http::StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + match controller.storage.get(S::ID, &filename).await { + Ok(Some(entry)) => { + return Ok(Self::build_response( + http::StatusCode::OK, + entry.file_bytes.0, + mime, + )); + } + Ok(None) => {} + Err(err) => { + error!( + backend = S::ID, + filename, "failed to get file from storage: {err}" + ); + return Err(http::StatusCode::INTERNAL_SERVER_ERROR); + } + } + + let entry = controller + .upstream + .fetch(proxy::DownloadRequest { url }) + .await?; + + match controller.storage.put(S::ID, &filename, &entry.bytes).await { + Ok(()) => {} + Err(err) => { + error!( + backend = S::ID, + filename, "failed to put file to storage: {err}" + ); + return Err(http::StatusCode::INTERNAL_SERVER_ERROR); + } + } + + Ok(Self::build_response( + http::StatusCode::OK, + entry.bytes, + mime, + )) + } + + fn build_response( + status: http::StatusCode, + bytes: bytes::Bytes, + mime: repos::ContentType, + ) -> response::Response { + response::Response::builder() + .status(status) + .header(http::header::CONTENT_TYPE, mime.as_str()) + .header(http::header::CONTENT_LENGTH, bytes.len()) + .body(body::Body::from(bytes)) + .unwrap() + } +} |
