]> Untitled Git - lemmy.git/blob - crates/routes/src/images.rs
Simplify config using macros (#1686)
[lemmy.git] / crates / routes / src / images.rs
1 use actix_web::{body::BodyStream, http::StatusCode, web::Data, *};
2 use anyhow::anyhow;
3 use awc::Client;
4 use lemmy_utils::{claims::Claims, rate_limit::RateLimit, settings::structs::Settings, LemmyError};
5 use serde::{Deserialize, Serialize};
6 use std::time::Duration;
7
8 pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
9   let client = Client::builder()
10     .header("User-Agent", "pict-rs-frontend, v0.1.0")
11     .timeout(Duration::from_secs(30))
12     .finish();
13
14   cfg
15     .app_data(Data::new(client))
16     .service(
17       web::resource("/pictrs/image")
18         .wrap(rate_limit.image())
19         .route(web::post().to(upload)),
20     )
21     // This has optional query params: /image/{filename}?format=jpg&thumbnail=256
22     .service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
23     .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
24 }
25
26 #[derive(Debug, Serialize, Deserialize)]
27 struct Image {
28   file: String,
29   delete_token: String,
30 }
31
32 #[derive(Debug, Serialize, Deserialize)]
33 struct Images {
34   msg: String,
35   files: Option<Vec<Image>>,
36 }
37
38 #[derive(Deserialize)]
39 struct PictrsParams {
40   format: Option<String>,
41   thumbnail: Option<String>,
42 }
43
44 async fn upload(
45   req: HttpRequest,
46   body: web::Payload,
47   client: web::Data<Client>,
48 ) -> Result<HttpResponse, Error> {
49   // TODO: check rate limit here
50   let jwt = req
51     .cookie("jwt")
52     .expect("No auth header for picture upload");
53
54   if Claims::decode(jwt.value()).is_err() {
55     return Ok(HttpResponse::Unauthorized().finish());
56   };
57
58   let mut client_req = client.request_from(format!("{}/image", pictrs_url()?), req.head());
59
60   if let Some(addr) = req.head().peer_addr {
61     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
62   };
63
64   let mut res = client_req
65     .send_stream(body)
66     .await
67     .map_err(error::ErrorBadRequest)?;
68
69   let images = res.json::<Images>().await.map_err(error::ErrorBadRequest)?;
70
71   Ok(HttpResponse::build(res.status()).json(images))
72 }
73
74 async fn full_res(
75   filename: web::Path<String>,
76   web::Query(params): web::Query<PictrsParams>,
77   req: HttpRequest,
78   client: web::Data<Client>,
79 ) -> Result<HttpResponse, Error> {
80   let name = &filename.into_inner();
81
82   // If there are no query params, the URL is original
83   let url = if params.format.is_none() && params.thumbnail.is_none() {
84     format!("{}/image/original/{}", pictrs_url()?, name,)
85   } else {
86     // Use jpg as a default when none is given
87     let format = params.format.unwrap_or_else(|| "jpg".to_string());
88
89     let mut url = format!("{}/image/process.{}?src={}", pictrs_url()?, format, name,);
90
91     if let Some(size) = params.thumbnail {
92       url = format!("{}&thumbnail={}", url, size,);
93     }
94     url
95   };
96
97   image(url, req, client).await
98 }
99
100 async fn image(
101   url: String,
102   req: HttpRequest,
103   client: web::Data<Client>,
104 ) -> Result<HttpResponse, Error> {
105   let mut client_req = client.request_from(url, req.head());
106
107   if let Some(addr) = req.head().peer_addr {
108     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
109   };
110
111   let res = client_req
112     .no_decompress()
113     .send()
114     .await
115     .map_err(error::ErrorBadRequest)?;
116
117   if res.status() == StatusCode::NOT_FOUND {
118     return Ok(HttpResponse::NotFound().finish());
119   }
120
121   let mut client_res = HttpResponse::build(res.status());
122
123   for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
124     client_res.insert_header((name.clone(), value.clone()));
125   }
126
127   Ok(client_res.body(BodyStream::new(res)))
128 }
129
130 async fn delete(
131   components: web::Path<(String, String)>,
132   req: HttpRequest,
133   client: web::Data<Client>,
134 ) -> Result<HttpResponse, Error> {
135   let (token, file) = components.into_inner();
136
137   let url = format!("{}/image/delete/{}/{}", pictrs_url()?, &token, &file);
138
139   let mut client_req = client.request_from(url, req.head());
140
141   if let Some(addr) = req.head().peer_addr {
142     client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string()))
143   };
144
145   let res = client_req
146     .no_decompress()
147     .send()
148     .await
149     .map_err(error::ErrorBadRequest)?;
150
151   Ok(HttpResponse::build(res.status()).body(BodyStream::new(res)))
152 }
153
154 fn pictrs_url() -> Result<String, LemmyError> {
155   Settings::get()
156     .pictrs_url
157     .ok_or_else(|| anyhow!("images_disabled").into())
158 }