mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 03:11:32 +00:00
split files into upload, download
This commit is contained in:
parent
b0d4bdb8ff
commit
55b8aced9d
12 changed files with 277 additions and 267 deletions
|
@ -72,6 +72,9 @@
|
||||||
# Otherwise we have to use crop, or use max_width/max_height which throws error
|
# Otherwise we have to use crop, or use max_width/max_height which throws error
|
||||||
# if image is larger.
|
# if image is larger.
|
||||||
max_banner_size: 512
|
max_banner_size: 512
|
||||||
|
# Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and
|
||||||
|
# banners can still be uploaded.
|
||||||
|
image_upload_disabled: false
|
||||||
}
|
}
|
||||||
# Email sending configuration. All options except login/password are mandatory
|
# Email sending configuration. All options except login/password are mandatory
|
||||||
email: {
|
email: {
|
||||||
|
|
|
@ -76,5 +76,6 @@ pub async fn leave_admin(
|
||||||
blocked_urls,
|
blocked_urls,
|
||||||
tagline,
|
tagline,
|
||||||
my_user: None,
|
my_user: None,
|
||||||
|
image_upload_disabled: context.settings().pictrs()?.image_upload_disabled,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1128,12 +1128,7 @@ async fn proxy_image_link_internal(
|
||||||
/// Rewrite a link to go through `/api/v4/image_proxy` endpoint. This is only for remote urls and
|
/// Rewrite a link to go through `/api/v4/image_proxy` endpoint. This is only for remote urls and
|
||||||
/// if image_proxy setting is enabled.
|
/// if image_proxy setting is enabled.
|
||||||
pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult<DbUrl> {
|
pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult<DbUrl> {
|
||||||
proxy_image_link_internal(
|
proxy_image_link_internal(link, context.settings().pictrs()?.image_mode(), context).await
|
||||||
link,
|
|
||||||
context.settings().pictrs()?.image_mode(),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn proxy_image_link_opt_api(
|
pub async fn proxy_image_link_opt_api(
|
||||||
|
|
|
@ -69,6 +69,6 @@ async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
|
||||||
tagline,
|
tagline,
|
||||||
oauth_providers: Some(oauth_providers),
|
oauth_providers: Some(oauth_providers),
|
||||||
admin_oauth_providers: Some(admin_oauth_providers),
|
admin_oauth_providers: Some(admin_oauth_providers),
|
||||||
image_upload_disabled: context.settings().pictrs()?.disable_image_upload,
|
image_upload_disabled: context.settings().pictrs()?.image_upload_disabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
113
crates/routes/src/images/download.rs
Normal file
113
crates/routes/src/images/download.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use super::utils::{adapt_request, convert_header, file_type};
|
||||||
|
use actix_web::{
|
||||||
|
body::{BodyStream, BoxBody},
|
||||||
|
http::StatusCode,
|
||||||
|
web::{Data, *},
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
Responder,
|
||||||
|
};
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
image::{ImageGetParams, ImageProxyParams},
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::source::{images::RemoteImage, local_site::LocalSite};
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub async fn get_image(
|
||||||
|
filename: Path<String>,
|
||||||
|
Query(params): Query<ImageGetParams>,
|
||||||
|
req: HttpRequest,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
local_user_view: Option<LocalUserView>,
|
||||||
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
// block access to images if instance is private and unauthorized, public
|
||||||
|
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||||
|
if local_site.private_instance && local_user_view.is_none() {
|
||||||
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
|
}
|
||||||
|
let name = &filename.into_inner();
|
||||||
|
|
||||||
|
// If there are no query params, the URL is original
|
||||||
|
let pictrs_url = context.settings().pictrs()?.url;
|
||||||
|
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
||||||
|
format!("{}image/original/{}", pictrs_url, name)
|
||||||
|
} else {
|
||||||
|
let file_type = file_type(params.file_type, name);
|
||||||
|
let mut url = format!("{}image/process.{}?src={}", pictrs_url, file_type, name);
|
||||||
|
|
||||||
|
if let Some(size) = params.max_size {
|
||||||
|
url = format!("{url}&thumbnail={size}",);
|
||||||
|
}
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
|
do_get_image(processed_url, req).await
|
||||||
|
}
|
||||||
|
pub async fn image_proxy(
|
||||||
|
Query(params): Query<ImageProxyParams>,
|
||||||
|
req: HttpRequest,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<Either<HttpResponse<()>, HttpResponse<BoxBody>>> {
|
||||||
|
let url = Url::parse(¶ms.url)?;
|
||||||
|
|
||||||
|
// Check that url corresponds to a federated image so that this can't be abused as a proxy
|
||||||
|
// for arbitrary purposes.
|
||||||
|
RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
|
||||||
|
|
||||||
|
let pictrs_config = context.settings().pictrs()?;
|
||||||
|
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
||||||
|
format!("{}image/original?proxy={}", pictrs_config.url, params.url)
|
||||||
|
} else {
|
||||||
|
let file_type = file_type(params.file_type, url.as_str());
|
||||||
|
let mut url = format!(
|
||||||
|
"{}image/process.{}?proxy={}",
|
||||||
|
pictrs_config.url, file_type, url
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(size) = params.max_size {
|
||||||
|
url = format!("{url}&thumbnail={size}",);
|
||||||
|
}
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
|
let bypass_proxy = pictrs_config
|
||||||
|
.proxy_bypass_domains
|
||||||
|
.iter()
|
||||||
|
.any(|s| url.domain().is_some_and(|d| d == s));
|
||||||
|
if bypass_proxy {
|
||||||
|
// Bypass proxy and redirect user to original image
|
||||||
|
Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req)))
|
||||||
|
} else {
|
||||||
|
// Proxy the image data through Lemmy
|
||||||
|
Ok(Either::Right(do_get_image(processed_url, req).await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn do_get_image(url: String, req: HttpRequest) -> LemmyResult<HttpResponse> {
|
||||||
|
let mut client_req = adapt_request(&req, url);
|
||||||
|
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = client_req.send().await?;
|
||||||
|
|
||||||
|
if res.status() == http::StatusCode::NOT_FOUND {
|
||||||
|
return Ok(HttpResponse::NotFound().finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client_res = HttpResponse::build(StatusCode::from_u16(res.status().as_u16())?);
|
||||||
|
|
||||||
|
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
||||||
|
client_res.insert_header(convert_header(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(client_res.body(BodyStream::new(res.bytes_stream())))
|
||||||
|
}
|
|
@ -1,73 +1,14 @@
|
||||||
use actix_web::{body::BoxBody, web::*, HttpRequest, HttpResponse, Responder};
|
use actix_web::web::*;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{context::LemmyContext, image::DeleteImageParams, SuccessResponse};
|
||||||
context::LemmyContext,
|
use lemmy_db_schema::source::images::LocalImage;
|
||||||
image::{DeleteImageParams, ImageGetParams, ImageProxyParams, UploadImageResponse},
|
|
||||||
LemmyErrorType,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
images::{LocalImage, RemoteImage},
|
|
||||||
local_site::LocalSite,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
use lemmy_utils::error::LemmyResult;
|
use lemmy_utils::error::LemmyResult;
|
||||||
use url::Url;
|
use utils::PICTRS_CLIENT;
|
||||||
use utils::{do_get_image, do_upload_image, file_type, UploadType, PICTRS_CLIENT};
|
|
||||||
|
|
||||||
pub mod person;
|
pub mod download;
|
||||||
|
pub mod upload;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub async fn upload_image(
|
|
||||||
req: HttpRequest,
|
|
||||||
body: Payload,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<UploadImageResponse>> {
|
|
||||||
if context.settings().pictrs()?.disable_image_upload {
|
|
||||||
return Err(LemmyErrorType::ImageUploadDisabled.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = do_upload_image(req, body, UploadType::Other, &local_user_view, &context).await?;
|
|
||||||
|
|
||||||
let image_url = image.image_url(&context.settings().get_protocol_and_hostname())?;
|
|
||||||
Ok(Json(UploadImageResponse {
|
|
||||||
image_url,
|
|
||||||
filename: image.file,
|
|
||||||
delete_token: image.delete_token,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_image(
|
|
||||||
filename: Path<String>,
|
|
||||||
Query(params): Query<ImageGetParams>,
|
|
||||||
req: HttpRequest,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: Option<LocalUserView>,
|
|
||||||
) -> LemmyResult<HttpResponse> {
|
|
||||||
// block access to images if instance is private and unauthorized, public
|
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
|
||||||
if local_site.private_instance && local_user_view.is_none() {
|
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
|
||||||
}
|
|
||||||
let name = &filename.into_inner();
|
|
||||||
|
|
||||||
// If there are no query params, the URL is original
|
|
||||||
let pictrs_url = context.settings().pictrs()?.url;
|
|
||||||
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
|
||||||
format!("{}image/original/{}", pictrs_url, name)
|
|
||||||
} else {
|
|
||||||
let file_type = file_type(params.file_type, name);
|
|
||||||
let mut url = format!("{}image/process.{}?src={}", pictrs_url, file_type, name);
|
|
||||||
|
|
||||||
if let Some(size) = params.max_size {
|
|
||||||
url = format!("{url}&thumbnail={size}",);
|
|
||||||
}
|
|
||||||
url
|
|
||||||
};
|
|
||||||
|
|
||||||
do_get_image(processed_url, req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_image(
|
pub async fn delete_image(
|
||||||
data: Json<DeleteImageParams>,
|
data: Json<DeleteImageParams>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
|
@ -95,43 +36,3 @@ pub async fn pictrs_health(context: Data<LemmyContext>) -> LemmyResult<Json<Succ
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
Ok(Json(SuccessResponse::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn image_proxy(
|
|
||||||
Query(params): Query<ImageProxyParams>,
|
|
||||||
req: HttpRequest,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Either<HttpResponse<()>, HttpResponse<BoxBody>>> {
|
|
||||||
let url = Url::parse(¶ms.url)?;
|
|
||||||
|
|
||||||
// Check that url corresponds to a federated image so that this can't be abused as a proxy
|
|
||||||
// for arbitrary purposes.
|
|
||||||
RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
|
|
||||||
|
|
||||||
let pictrs_config = context.settings().pictrs()?;
|
|
||||||
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
|
||||||
format!("{}image/original?proxy={}", pictrs_config.url, params.url)
|
|
||||||
} else {
|
|
||||||
let file_type = file_type(params.file_type, url.as_str());
|
|
||||||
let mut url = format!(
|
|
||||||
"{}image/process.{}?proxy={}",
|
|
||||||
pictrs_config.url, file_type, url
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(size) = params.max_size {
|
|
||||||
url = format!("{url}&thumbnail={size}",);
|
|
||||||
}
|
|
||||||
url
|
|
||||||
};
|
|
||||||
|
|
||||||
let bypass_proxy = pictrs_config
|
|
||||||
.proxy_bypass_domains
|
|
||||||
.iter()
|
|
||||||
.any(|s| url.domain().is_some_and(|d| d == s));
|
|
||||||
if bypass_proxy {
|
|
||||||
// Bypass proxy and redirect user to original image
|
|
||||||
Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req)))
|
|
||||||
} else {
|
|
||||||
// Proxy the image data through Lemmy
|
|
||||||
Ok(Either::Right(do_get_image(processed_url, req).await?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
use super::utils::{delete_old_image, do_upload_image, UploadType};
|
|
||||||
use actix_web::{self, web::*, HttpRequest};
|
|
||||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::person::{Person, PersonUpdateForm},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub async fn upload_avatar(
|
|
||||||
req: HttpRequest,
|
|
||||||
body: Payload,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
let image = do_upload_image(req, body, UploadType::Avatar, &local_user_view, &context).await?;
|
|
||||||
|
|
||||||
delete_old_image(&local_user_view.person.avatar, &context).await?;
|
|
||||||
|
|
||||||
let avatar = format!(
|
|
||||||
"{}/api/v4/image/{}",
|
|
||||||
context.settings().get_protocol_and_hostname(),
|
|
||||||
image.file
|
|
||||||
);
|
|
||||||
let avatar = Some(Some(Url::parse(&avatar)?.into()));
|
|
||||||
let person_form = PersonUpdateForm {
|
|
||||||
avatar,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?;
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
132
crates/routes/src/images/upload.rs
Normal file
132
crates/routes/src/images/upload.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use super::utils::{adapt_request, delete_old_image, make_send};
|
||||||
|
use actix_web::{self, web::*, HttpRequest};
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
image::UploadImageResponse,
|
||||||
|
request::{PictrsFile, PictrsResponse},
|
||||||
|
LemmyErrorType,
|
||||||
|
SuccessResponse,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{
|
||||||
|
images::{LocalImage, LocalImageForm},
|
||||||
|
person::{Person, PersonUpdateForm},
|
||||||
|
},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
use reqwest::Body;
|
||||||
|
use std::time::Duration;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub async fn upload_image(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: Payload,
|
||||||
|
local_user_view: LocalUserView,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<Json<UploadImageResponse>> {
|
||||||
|
if context.settings().pictrs()?.image_upload_disabled {
|
||||||
|
return Err(LemmyErrorType::ImageUploadDisabled.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = do_upload_image(req, body, UploadType::Other, &local_user_view, &context).await?;
|
||||||
|
|
||||||
|
let image_url = image.image_url(&context.settings().get_protocol_and_hostname())?;
|
||||||
|
Ok(Json(UploadImageResponse {
|
||||||
|
image_url,
|
||||||
|
filename: image.file,
|
||||||
|
delete_token: image.delete_token,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_avatar(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: Payload,
|
||||||
|
local_user_view: LocalUserView,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<Json<SuccessResponse>> {
|
||||||
|
let image = do_upload_image(req, body, UploadType::Avatar, &local_user_view, &context).await?;
|
||||||
|
|
||||||
|
delete_old_image(&local_user_view.person.avatar, &context).await?;
|
||||||
|
|
||||||
|
let avatar = format!(
|
||||||
|
"{}/api/v4/image/{}",
|
||||||
|
context.settings().get_protocol_and_hostname(),
|
||||||
|
image.file
|
||||||
|
);
|
||||||
|
let avatar = Some(Some(Url::parse(&avatar)?.into()));
|
||||||
|
let person_form = PersonUpdateForm {
|
||||||
|
avatar,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?;
|
||||||
|
|
||||||
|
Ok(Json(SuccessResponse::default()))
|
||||||
|
}
|
||||||
|
pub enum UploadType {
|
||||||
|
Avatar,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn do_upload_image(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: Payload,
|
||||||
|
upload_type: UploadType,
|
||||||
|
local_user_view: &LocalUserView,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<PictrsFile> {
|
||||||
|
let pictrs_config = context.settings().pictrs()?;
|
||||||
|
let image_url = format!("{}image", pictrs_config.url);
|
||||||
|
|
||||||
|
let mut client_req = adapt_request(&req, image_url);
|
||||||
|
|
||||||
|
client_req = match upload_type {
|
||||||
|
UploadType::Avatar => {
|
||||||
|
let max_size = context.settings().pictrs()?.max_avatar_size.to_string();
|
||||||
|
client_req.query(&[
|
||||||
|
("resize", max_size.as_ref()),
|
||||||
|
("allow_animation", "false"),
|
||||||
|
("allow_video", "false"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
// TODO: same as above but using `max_banner_size`
|
||||||
|
// UploadType::Banner => {}
|
||||||
|
_ => client_req,
|
||||||
|
};
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string())
|
||||||
|
};
|
||||||
|
let res = client_req
|
||||||
|
.timeout(Duration::from_secs(pictrs_config.upload_timeout))
|
||||||
|
.body(Body::wrap_stream(make_send(body)))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
let mut images = res.json::<PictrsResponse>().await?;
|
||||||
|
for image in &images.files {
|
||||||
|
// Pictrs allows uploading multiple images in a single request. Lemmy doesnt need this,
|
||||||
|
// but still a user may upload multiple and so we need to store all links in db for
|
||||||
|
// to allow deletion via web ui.
|
||||||
|
let form = LocalImageForm {
|
||||||
|
local_user_id: Some(local_user_view.local_user.id),
|
||||||
|
pictrs_alias: image.file.to_string(),
|
||||||
|
pictrs_delete_token: image.delete_token.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||||
|
let thumbnail_url = image.image_url(&protocol_and_hostname)?;
|
||||||
|
|
||||||
|
// Also store the details for the image
|
||||||
|
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
||||||
|
LocalImage::create(&mut context.pool(), &form, &details_form).await?;
|
||||||
|
}
|
||||||
|
let image = images
|
||||||
|
.files
|
||||||
|
.pop()
|
||||||
|
.ok_or(LemmyErrorType::InvalidImageUpload)?;
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
|
@ -1,31 +1,22 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::BodyStream,
|
|
||||||
http::{
|
http::{
|
||||||
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
||||||
Method,
|
Method,
|
||||||
StatusCode,
|
|
||||||
},
|
},
|
||||||
web::{Data, Payload},
|
web::Data,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
|
||||||
};
|
};
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use http::HeaderValue;
|
use http::HeaderValue;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
request::{client_builder, delete_image_from_pictrs, PictrsFile, PictrsResponse},
|
request::{client_builder, delete_image_from_pictrs},
|
||||||
LemmyErrorType,
|
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{newtypes::DbUrl, source::images::LocalImage};
|
||||||
newtypes::DbUrl,
|
|
||||||
source::images::{LocalImage, LocalImageForm},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::{error::LemmyResult, settings::SETTINGS, REQWEST_TIMEOUT};
|
use lemmy_utils::{error::LemmyResult, settings::SETTINGS, REQWEST_TIMEOUT};
|
||||||
use reqwest::Body;
|
|
||||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
||||||
use reqwest_tracing::TracingMiddleware;
|
use reqwest_tracing::TracingMiddleware;
|
||||||
use std::{sync::LazyLock, time::Duration};
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// Pictrs cannot use proxy
|
// Pictrs cannot use proxy
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
@ -40,7 +31,7 @@ pub(super) static PICTRS_CLIENT: LazyLock<ClientWithMiddleware> = LazyLock::new(
|
||||||
.build()
|
.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
fn adapt_request(request: &HttpRequest, url: String) -> RequestBuilder {
|
pub(super) fn adapt_request(request: &HttpRequest, url: String) -> RequestBuilder {
|
||||||
// remove accept-encoding header so that pictrs doesn't compress the response
|
// remove accept-encoding header so that pictrs doesn't compress the response
|
||||||
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
|
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
|
||||||
|
|
||||||
|
@ -61,7 +52,7 @@ fn adapt_request(request: &HttpRequest, url: String) -> RequestBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static
|
pub(super) fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static
|
||||||
where
|
where
|
||||||
S: Stream + Unpin + 'static,
|
S: Stream + Unpin + 'static,
|
||||||
S::Item: Send,
|
S::Item: Send,
|
||||||
|
@ -109,106 +100,13 @@ pub(super) fn convert_method(method: &Method) -> http::Method {
|
||||||
http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted")
|
http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_header<'a>(name: &'a http::HeaderName, value: &'a HeaderValue) -> (&'a str, &'a [u8]) {
|
pub(super) fn convert_header<'a>(
|
||||||
|
name: &'a http::HeaderName,
|
||||||
|
value: &'a HeaderValue,
|
||||||
|
) -> (&'a str, &'a [u8]) {
|
||||||
(name.as_str(), value.as_bytes())
|
(name.as_str(), value.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) enum UploadType {
|
|
||||||
Avatar,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn do_upload_image(
|
|
||||||
req: HttpRequest,
|
|
||||||
body: Payload,
|
|
||||||
upload_type: UploadType,
|
|
||||||
local_user_view: &LocalUserView,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<PictrsFile> {
|
|
||||||
let pictrs_config = context.settings().pictrs()?;
|
|
||||||
let image_url = format!("{}image", pictrs_config.url);
|
|
||||||
|
|
||||||
let mut client_req = adapt_request(&req, image_url);
|
|
||||||
|
|
||||||
client_req = match upload_type {
|
|
||||||
UploadType::Avatar => {
|
|
||||||
let max_size = context
|
|
||||||
.settings()
|
|
||||||
.pictrs()?
|
|
||||||
.max_avatar_size
|
|
||||||
.to_string();
|
|
||||||
client_req.query(&[
|
|
||||||
("resize", max_size.as_ref()),
|
|
||||||
("allow_animation", "false"),
|
|
||||||
("allow_video", "false"),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
// TODO: same as above but using `max_banner_size`
|
|
||||||
// UploadType::Banner => {}
|
|
||||||
_ => client_req,
|
|
||||||
};
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string())
|
|
||||||
};
|
|
||||||
let res = client_req
|
|
||||||
.timeout(Duration::from_secs(pictrs_config.upload_timeout))
|
|
||||||
.body(Body::wrap_stream(make_send(body)))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?;
|
|
||||||
|
|
||||||
let mut images = res.json::<PictrsResponse>().await?;
|
|
||||||
for image in &images.files {
|
|
||||||
// Pictrs allows uploading multiple images in a single request. Lemmy doesnt need this,
|
|
||||||
// but still a user may upload multiple and so we need to store all links in db for
|
|
||||||
// to allow deletion via web ui.
|
|
||||||
let form = LocalImageForm {
|
|
||||||
local_user_id: Some(local_user_view.local_user.id),
|
|
||||||
pictrs_alias: image.file.to_string(),
|
|
||||||
pictrs_delete_token: image.delete_token.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
|
||||||
let thumbnail_url = image.image_url(&protocol_and_hostname)?;
|
|
||||||
|
|
||||||
// Also store the details for the image
|
|
||||||
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
|
||||||
LocalImage::create(&mut context.pool(), &form, &details_form).await?;
|
|
||||||
}
|
|
||||||
let image = images
|
|
||||||
.files
|
|
||||||
.pop()
|
|
||||||
.ok_or(LemmyErrorType::InvalidImageUpload)?;
|
|
||||||
|
|
||||||
Ok(image)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn do_get_image(url: String, req: HttpRequest) -> LemmyResult<HttpResponse> {
|
|
||||||
let mut client_req = adapt_request(&req, url);
|
|
||||||
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = client_req.send().await?;
|
|
||||||
|
|
||||||
if res.status() == http::StatusCode::NOT_FOUND {
|
|
||||||
return Ok(HttpResponse::NotFound().finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut client_res = HttpResponse::build(StatusCode::from_u16(res.status().as_u16())?);
|
|
||||||
|
|
||||||
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
|
||||||
client_res.insert_header(convert_header(name, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(client_res.body(BodyStream::new(res.bytes_stream())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When adding a new avatar, banner or similar image, delete the old one.
|
/// When adding a new avatar, banner or similar image, delete the old one.
|
||||||
pub(super) async fn delete_old_image(
|
pub(super) async fn delete_old_image(
|
||||||
old_image: &Option<DbUrl>,
|
old_image: &Option<DbUrl>,
|
||||||
|
|
|
@ -120,7 +120,7 @@ pub struct PictrsConfig {
|
||||||
/// Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and
|
/// Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and
|
||||||
/// banners can still be uploaded.
|
/// banners can still be uploaded.
|
||||||
#[default(false)]
|
#[default(false)]
|
||||||
pub disable_image_upload: bool,
|
pub image_upload_disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)]
|
||||||
|
|
|
@ -134,7 +134,12 @@ use lemmy_apub::api::{
|
||||||
search::search,
|
search::search,
|
||||||
user_settings_backup::{export_settings, import_settings},
|
user_settings_backup::{export_settings, import_settings},
|
||||||
};
|
};
|
||||||
use lemmy_routes::images::{delete_image, get_image, image_proxy, pictrs_health, upload_image};
|
use lemmy_routes::images::{
|
||||||
|
delete_image,
|
||||||
|
download::{get_image, image_proxy},
|
||||||
|
pictrs_health,
|
||||||
|
upload::upload_image,
|
||||||
|
};
|
||||||
use lemmy_utils::rate_limit::RateLimitCell;
|
use lemmy_utils::rate_limit::RateLimitCell;
|
||||||
|
|
||||||
// Deprecated, use api v4 instead.
|
// Deprecated, use api v4 instead.
|
||||||
|
|
|
@ -161,11 +161,9 @@ use lemmy_apub::api::{
|
||||||
};
|
};
|
||||||
use lemmy_routes::images::{
|
use lemmy_routes::images::{
|
||||||
delete_image,
|
delete_image,
|
||||||
get_image,
|
download::{get_image, image_proxy},
|
||||||
image_proxy,
|
|
||||||
person::upload_avatar,
|
|
||||||
pictrs_health,
|
pictrs_health,
|
||||||
upload_image,
|
upload::{upload_avatar, upload_image},
|
||||||
};
|
};
|
||||||
use lemmy_utils::rate_limit::RateLimitCell;
|
use lemmy_utils::rate_limit::RateLimitCell;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue