use std::time::Duration; use crate::pict::{Details, Extension, Images, Upload, Uploads}; use actix_web::{ body::BodyStream, http::StatusCode, web, HttpRequest, HttpResponse, ResponseError, }; use awc::Client; use url::Url; pub(crate) static VALID_SIZES: &[u16] = &[80, 160, 320, 640, 1080, 2160]; pub(crate) struct Connection { upstream: Url, client: Client, } impl std::fmt::Debug for Connection { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Connection") .field("upstream", &self.upstream.as_str()) .field("client", &"Client") .finish() } } impl Connection { pub(crate) fn new(upstream: Url, client: Client) -> Self { Connection { upstream, client } } #[tracing::instrument(skip_all)] pub(crate) async fn claim(&self, upload: Upload) -> Result { let mut attempts = 0; const CLAIM_ATTEMPT_LIMIT: usize = 10; loop { match self.client.get(self.claim_url(&upload)).send().await { Ok(mut res) => { match res.status() { StatusCode::OK => { return res.json::().await.map_err(|_| UploadError::Json); } StatusCode::UNPROCESSABLE_ENTITY => { let images = res.json::().await.map_err(|_| UploadError::Json)?; tracing::warn!("{}", images.msg()); return Err(UploadError::Status); } StatusCode::NO_CONTENT => { // continue } _ => { return Err(UploadError::Status); } } } Err(_) => { attempts += 1; if attempts > CLAIM_ATTEMPT_LIMIT { return Err(UploadError::Status); } tokio::time::sleep(Duration::from_secs(1)).await; // continue } } } } pub(crate) async fn thumbnail( &self, size: u16, file: &str, extension: Extension, req: &HttpRequest, ) -> Result { if !VALID_SIZES.contains(&size) { return Err(SizeError(size).into()); } self.proxy(self.thumbnail_url(size, file, extension), req) .await } pub(crate) async fn image( &self, file: &str, req: &HttpRequest, ) -> Result { self.proxy(self.image_url(file), req).await } pub(crate) async fn details(&self, file: &str) -> Result { let mut response = self .client .get(self.details_url(file)) .send() .await .map_err(|_| UploadError::Request)?; if !response.status().is_success() { return Err(UploadError::Status); } response.json().await.map_err(|_| UploadError::Json) } pub(crate) async fn upload( &self, req: &HttpRequest, body: web::Payload, ) -> Result { let client_request = self.client.request_from(self.upload_url(), req.head()); let mut client_request = if let Some(addr) = req.head().peer_addr { client_request.append_header(("X-Forwarded-For", addr.to_string())) } else { client_request }; client_request.headers_mut().remove("Accept-Encoding"); let mut res = client_request .send_stream(body) .await .map_err(|_| UploadError::Request)?; let uploads = res.json::().await.map_err(|_| UploadError::Json)?; Ok(uploads) } pub(crate) async fn delete(&self, file: &str, token: &str) -> Result<(), UploadError> { let res = self .client .delete(self.delete_url(file, token)) .send() .await .map_err(|_| UploadError::Request)?; if !res.status().is_success() { return Err(UploadError::Status); } Ok(()) } fn claim_url(&self, upload: &Upload) -> String { let mut url = self.upstream.clone(); url.set_path("/image/backgrounded/claim"); url.set_query(Some(&format!("upload_id={}", upload.id()))); url.to_string() } fn upload_url(&self) -> String { let mut url = self.upstream.clone(); url.set_path("/image/backgrounded"); url.to_string() } fn thumbnail_url(&self, size: u16, file: &str, extension: Extension) -> String { let mut url = self.upstream.clone(); url.set_path(&format!("/image/process.{extension}")); url.set_query(Some(&format!("src={file}&resize={size}"))); url.to_string() } fn image_url(&self, file: &str) -> String { let mut url = self.upstream.clone(); url.set_path(&format!("/image/original/{file}")); url.to_string() } fn details_url(&self, file: &str) -> String { let mut url = self.upstream.clone(); url.set_path(&format!("/image/details/original/{file}")); url.to_string() } fn delete_url(&self, file: &str, token: &str) -> String { let mut url = self.upstream.clone(); url.set_path(&format!("/image/delete/{token}/{file}")); url.to_string() } async fn proxy( &self, url: String, req: &HttpRequest, ) -> Result { let client_request = self.client.request_from(url, req.head()); let client_request = if let Some(addr) = req.head().peer_addr { client_request.append_header(("X-Forwarded-For", addr.to_string())) } else { client_request }; let res = client_request .no_decompress() .send() .await .map_err(|_| UploadError::Request)?; let mut client_res = HttpResponse::build(res.status()); for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") { client_res.append_header((name.clone(), value.clone())); } Ok(client_res.body(BodyStream::new(res))) } } #[derive(Clone, Debug, thiserror::Error)] pub(crate) enum UploadError { #[error("There was an error uploading the image")] Request, #[error("There was an error parsing the image response")] Json, #[error("Request returned bad HTTP status")] Status, } impl ResponseError for UploadError { fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) .content_type(mime::TEXT_PLAIN.essence_str()) .body(self.to_string()) } } #[derive(Clone, Debug, thiserror::Error)] #[error("The requested size is invalid, {0}")] struct SizeError(u16); impl ResponseError for SizeError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) .content_type(mime::TEXT_PLAIN.essence_str()) .body(self.to_string()) } }