mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-23 12:51:18 +00:00
Adding an image_details table to store image dimensions.
- Adds an image_details table, which stores the height, width, and content_type for local and remote images. - For LocalImages, this information already comes back with the upload. - For RemoteImages, it calls the pictrs details endpoint. - Fixed some issues with proxying non-image urls. - Fixes #3328 - Also fixes #4703
This commit is contained in:
parent
b152be7951
commit
ee7df0dc35
15 changed files with 332 additions and 128 deletions
|
@ -15,7 +15,7 @@ export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queu
|
||||||
|
|
||||||
# pictrs setup
|
# pictrs setup
|
||||||
if [ ! -f "api_tests/pict-rs" ]; then
|
if [ ! -f "api_tests/pict-rs" ]; then
|
||||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
|
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs
|
||||||
chmod +x api_tests/pict-rs
|
chmod +x api_tests/pict-rs
|
||||||
fi
|
fi
|
||||||
./api_tests/pict-rs \
|
./api_tests/pict-rs \
|
||||||
|
|
|
@ -29,7 +29,6 @@ import {
|
||||||
unfollows,
|
unfollows,
|
||||||
getPost,
|
getPost,
|
||||||
waitUntil,
|
waitUntil,
|
||||||
randomString,
|
|
||||||
createPostWithThumbnail,
|
createPostWithThumbnail,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
const downloadFileSync = require("download-file-sync");
|
const downloadFileSync = require("download-file-sync");
|
||||||
|
|
|
@ -3,14 +3,15 @@ use crate::{
|
||||||
lemmy_db_schema::traits::Crud,
|
lemmy_db_schema::traits::Crud,
|
||||||
post::{LinkMetadata, OpenGraphData},
|
post::{LinkMetadata, OpenGraphData},
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
send_activity::{ActivityChannel, SendActivityData},
|
||||||
utils::{local_site_opt_to_sensitive, proxy_image_link, proxy_image_link_opt_apub},
|
utils::{local_site_opt_to_sensitive, proxy_image_link},
|
||||||
};
|
};
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use encoding_rs::{Encoding, UTF_8};
|
use encoding_rs::{Encoding, UTF_8};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::DbUrl,
|
newtypes::DbUrl,
|
||||||
source::{
|
source::{
|
||||||
images::{LocalImage, LocalImageForm},
|
images::{ImageDetails, ImageDetailsForm, LocalImage, LocalImageForm},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
post::{Post, PostUpdateForm},
|
post::{Post, PostUpdateForm},
|
||||||
},
|
},
|
||||||
|
@ -25,7 +26,7 @@ use lemmy_utils::{
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
|
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
@ -95,38 +96,48 @@ pub fn generate_post_link_metadata(
|
||||||
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
|
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
|
||||||
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
|
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
|
||||||
|
|
||||||
// Use custom thumbnail if available and its not an image post
|
let thumbnail_url = if is_image_post {
|
||||||
let thumbnail_url = if !is_image_post && custom_thumbnail.is_some() {
|
if allow_generate_thumbnail {
|
||||||
custom_thumbnail
|
match post.url {
|
||||||
}
|
Some(url) => generate_pictrs_thumbnail(&url, &context)
|
||||||
// Use federated thumbnail if available
|
.await
|
||||||
else if federated_thumbnail.is_some() {
|
.ok()
|
||||||
federated_thumbnail
|
.map(Into::into),
|
||||||
}
|
None => None,
|
||||||
// Generate local thumbnail if allowed
|
}
|
||||||
else if allow_generate_thumbnail {
|
} else {
|
||||||
match post
|
None
|
||||||
.url
|
}
|
||||||
.filter(|_| is_image_post)
|
} else {
|
||||||
.or(metadata.opengraph_data.image)
|
// Use custom thumbnail if available and its not an image post
|
||||||
{
|
if let Some(custom_thumbnail) = custom_thumbnail {
|
||||||
Some(url) => generate_pictrs_thumbnail(&url, &context).await.ok(),
|
proxy_image_link(custom_thumbnail, &context).await.ok()
|
||||||
None => None,
|
}
|
||||||
|
// Use federated thumbnail if available
|
||||||
|
else if let Some(federated_thumbnail) = federated_thumbnail {
|
||||||
|
proxy_image_link(federated_thumbnail, &context).await.ok()
|
||||||
|
}
|
||||||
|
// Generate local thumbnail if allowed
|
||||||
|
else if allow_generate_thumbnail {
|
||||||
|
match metadata.opengraph_data.image {
|
||||||
|
Some(url) => generate_pictrs_thumbnail(&url, &context)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(Into::into),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise use opengraph preview image directly
|
||||||
|
else {
|
||||||
|
metadata.opengraph_data.image
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Otherwise use opengraph preview image directly
|
|
||||||
else {
|
|
||||||
metadata.opengraph_data.image.map(Into::into)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Proxy the image fetch if necessary
|
|
||||||
let proxied_thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?;
|
|
||||||
|
|
||||||
let form = PostUpdateForm {
|
let form = PostUpdateForm {
|
||||||
embed_title: Some(metadata.opengraph_data.title),
|
embed_title: Some(metadata.opengraph_data.title),
|
||||||
embed_description: Some(metadata.opengraph_data.description),
|
embed_description: Some(metadata.opengraph_data.description),
|
||||||
embed_video_url: Some(metadata.opengraph_data.embed_video_url),
|
embed_video_url: Some(metadata.opengraph_data.embed_video_url),
|
||||||
thumbnail_url: Some(proxied_thumbnail_url),
|
thumbnail_url: Some(thumbnail_url),
|
||||||
url_content_type: Some(metadata.content_type),
|
url_content_type: Some(metadata.content_type),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -142,17 +153,6 @@ pub fn generate_post_link_metadata(
|
||||||
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> {
|
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> {
|
||||||
let html = String::from_utf8_lossy(html_bytes);
|
let html = String::from_utf8_lossy(html_bytes);
|
||||||
|
|
||||||
// Make sure the first line is doctype html
|
|
||||||
let first_line = html
|
|
||||||
.trim_start()
|
|
||||||
.lines()
|
|
||||||
.next()
|
|
||||||
.ok_or(LemmyErrorType::NoLinesInHtml)?
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
if !first_line.starts_with("<!doctype html") {
|
|
||||||
Err(LemmyErrorType::SiteMetadataPageIsNotDoctypeHtml)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut page = HTML::from_string(html.to_string(), None)?;
|
let mut page = HTML::from_string(html.to_string(), None)?;
|
||||||
|
|
||||||
|
@ -201,19 +201,50 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraph
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct PictrsResponse {
|
pub struct PictrsResponse {
|
||||||
files: Vec<PictrsFile>,
|
pub files: Option<Vec<PictrsFile>>,
|
||||||
msg: String,
|
pub msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct PictrsFile {
|
pub struct PictrsFile {
|
||||||
file: String,
|
pub file: String,
|
||||||
delete_token: String,
|
pub delete_token: String,
|
||||||
|
pub details: PictrsFileDetails,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
impl PictrsFile {
|
||||||
|
pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
|
||||||
|
Url::parse(&format!(
|
||||||
|
"{protocol_and_hostname}/pictrs/image/{}",
|
||||||
|
self.file
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct PictrsFileDetails {
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub content_type: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PictrsFileDetails {
|
||||||
|
/// Builds the image form. This should always use the thumbnail_url,
|
||||||
|
/// Because the post_view joins to it
|
||||||
|
pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm {
|
||||||
|
ImageDetailsForm {
|
||||||
|
link: thumbnail_url.clone().into(),
|
||||||
|
width: self.width.into(),
|
||||||
|
height: self.height.into(),
|
||||||
|
content_type: self.content_type.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct PictrsPurgeResponse {
|
struct PictrsPurgeResponse {
|
||||||
msg: String,
|
msg: String,
|
||||||
}
|
}
|
||||||
|
@ -295,35 +326,78 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
|
||||||
encode(image_url.as_str())
|
encode(image_url.as_str())
|
||||||
);
|
);
|
||||||
|
|
||||||
let response = context
|
let res: PictrsResponse = context
|
||||||
.client()
|
.client()
|
||||||
.get(&fetch_url)
|
.get(&fetch_url)
|
||||||
.timeout(REQWEST_TIMEOUT)
|
.timeout(REQWEST_TIMEOUT)
|
||||||
.send()
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response: PictrsResponse = response.json().await?;
|
if let Some(image) = res.files.unwrap_or_default().first() {
|
||||||
|
let form = LocalImageForm {
|
||||||
|
// This is none because its an internal request.
|
||||||
|
// IE, a local user shouldn't get to delete the thumbnails for their link posts
|
||||||
|
local_user_id: None,
|
||||||
|
pictrs_alias: image.file.clone(),
|
||||||
|
pictrs_delete_token: image.delete_token.clone(),
|
||||||
|
};
|
||||||
|
LocalImage::create(&mut context.pool(), &form).await?;
|
||||||
|
|
||||||
|
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||||
|
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
||||||
|
|
||||||
|
// Also store the details for the image
|
||||||
|
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
||||||
|
ImageDetails::create(&mut context.pool(), &details_form).await?;
|
||||||
|
|
||||||
if response.msg == "ok" {
|
|
||||||
let thumbnail_url = Url::parse(&format!(
|
|
||||||
"{}/pictrs/image/{}",
|
|
||||||
context.settings().get_protocol_and_hostname(),
|
|
||||||
response.files.first().expect("missing pictrs file").file
|
|
||||||
))?;
|
|
||||||
for uploaded_image in response.files {
|
|
||||||
let form = LocalImageForm {
|
|
||||||
local_user_id: None,
|
|
||||||
pictrs_alias: uploaded_image.file.to_string(),
|
|
||||||
pictrs_delete_token: uploaded_image.delete_token.to_string(),
|
|
||||||
};
|
|
||||||
LocalImage::create(&mut context.pool(), &form).await?;
|
|
||||||
}
|
|
||||||
Ok(thumbnail_url)
|
Ok(thumbnail_url)
|
||||||
} else {
|
} else {
|
||||||
Err(LemmyErrorType::PictrsResponseError(response.msg))?
|
Err(LemmyErrorType::PictrsResponseError(res.msg))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the image details for pictrs proxied images
|
||||||
|
///
|
||||||
|
/// We don't need to check for image mode, as that's already been done
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn fetch_pictrs_proxied_image_details(
|
||||||
|
image_url: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> LemmyResult<PictrsFileDetails> {
|
||||||
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
|
|
||||||
|
// Pictrs needs you to fetch the proxied image before you can fetch the details
|
||||||
|
let proxy_url = format!(
|
||||||
|
"{}image/original?proxy={}",
|
||||||
|
context.settings().pictrs_config()?.url,
|
||||||
|
encode(image_url.as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = context.client().get(&proxy_url).send().await?.status();
|
||||||
|
if !res.is_success() {
|
||||||
|
Err(LemmyErrorType::NotAnImageType)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let details_url = format!(
|
||||||
|
"{}image/details/original?proxy={}",
|
||||||
|
pictrs_config.url,
|
||||||
|
encode(image_url.as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = context
|
||||||
|
.client()
|
||||||
|
.get(&details_url)
|
||||||
|
.timeout(REQWEST_TIMEOUT)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: get rid of this by reading content type from db
|
// TODO: get rid of this by reading content type from db
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> {
|
async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
request::{delete_image_from_pictrs, purge_image_from_pictrs},
|
request::{
|
||||||
|
delete_image_from_pictrs,
|
||||||
|
fetch_pictrs_proxied_image_details,
|
||||||
|
purge_image_from_pictrs,
|
||||||
|
},
|
||||||
site::{FederatedInstances, InstanceWithFederationState},
|
site::{FederatedInstances, InstanceWithFederationState},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Days, Local, TimeZone, Utc};
|
use chrono::{DateTime, Days, Local, TimeZone, Utc};
|
||||||
|
@ -12,7 +16,7 @@ use lemmy_db_schema::{
|
||||||
community::{Community, CommunityModerator, CommunityUpdateForm},
|
community::{Community, CommunityModerator, CommunityUpdateForm},
|
||||||
community_block::CommunityBlock,
|
community_block::CommunityBlock,
|
||||||
email_verification::{EmailVerification, EmailVerificationForm},
|
email_verification::{EmailVerification, EmailVerificationForm},
|
||||||
images::RemoteImage,
|
images::{ImageDetails, RemoteImage},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
instance_block::InstanceBlock,
|
instance_block::InstanceBlock,
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
|
@ -931,7 +935,20 @@ pub async fn process_markdown(
|
||||||
|
|
||||||
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
|
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
|
||||||
let (text, links) = markdown_rewrite_image_links(text);
|
let (text, links) = markdown_rewrite_image_links(text);
|
||||||
RemoteImage::create(&mut context.pool(), links).await?;
|
|
||||||
|
// Create images and image detail rows
|
||||||
|
for link in links {
|
||||||
|
RemoteImage::create(&mut context.pool(), &link).await?;
|
||||||
|
|
||||||
|
// Insert image details for the remote image
|
||||||
|
let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
|
||||||
|
if let Ok(details) = details_res {
|
||||||
|
let proxied =
|
||||||
|
build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
||||||
|
let details_form = details.build_image_details_form(&proxied);
|
||||||
|
ImageDetails::create(&mut context.pool(), &details_form).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(text)
|
Ok(text)
|
||||||
} else {
|
} else {
|
||||||
Ok(text)
|
Ok(text)
|
||||||
|
@ -965,13 +982,19 @@ async fn proxy_image_link_internal(
|
||||||
if link.domain() == Some(&context.settings().hostname) {
|
if link.domain() == Some(&context.settings().hostname) {
|
||||||
Ok(link.into())
|
Ok(link.into())
|
||||||
} else if image_mode == PictrsImageMode::ProxyAllImages {
|
} else if image_mode == PictrsImageMode::ProxyAllImages {
|
||||||
let proxied = format!(
|
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
||||||
"{}/api/v3/image_proxy?url={}",
|
|
||||||
context.settings().get_protocol_and_hostname(),
|
RemoteImage::create(&mut context.pool(), &link).await?;
|
||||||
encode(link.as_str())
|
|
||||||
);
|
// This should fail softly, since pictrs might not even be running
|
||||||
RemoteImage::create(&mut context.pool(), vec![link]).await?;
|
let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
|
||||||
Ok(Url::parse(&proxied)?.into())
|
|
||||||
|
if let Ok(details) = details_res {
|
||||||
|
let details_form = details.build_image_details_form(&proxied);
|
||||||
|
ImageDetails::create(&mut context.pool(), &details_form).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proxied.into())
|
||||||
} else {
|
} else {
|
||||||
Ok(link.into())
|
Ok(link.into())
|
||||||
}
|
}
|
||||||
|
@ -1025,6 +1048,17 @@ pub async fn proxy_image_link_opt_apub(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_proxied_image_url(
|
||||||
|
link: &Url,
|
||||||
|
protocol_and_hostname: &str,
|
||||||
|
) -> Result<Url, url::ParseError> {
|
||||||
|
Url::parse(&format!(
|
||||||
|
"{}/api/v3/image_proxy?url={}",
|
||||||
|
protocol_and_hostname,
|
||||||
|
encode(link.as_str())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
#[allow(clippy::indexing_slicing)]
|
#[allow(clippy::indexing_slicing)]
|
||||||
|
|
|
@ -14,7 +14,6 @@ use lemmy_api_common::{
|
||||||
local_site_to_slur_regex,
|
local_site_to_slur_regex,
|
||||||
mark_post_as_read,
|
mark_post_as_read,
|
||||||
process_markdown_opt,
|
process_markdown_opt,
|
||||||
proxy_image_link_opt_apub,
|
|
||||||
EndpointType,
|
EndpointType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -75,7 +74,6 @@ pub async fn create_post(
|
||||||
is_url_blocked(&url, &url_blocklist)?;
|
is_url_blocked(&url, &url_blocklist)?;
|
||||||
check_url_scheme(&url)?;
|
check_url_scheme(&url)?;
|
||||||
check_url_scheme(&custom_thumbnail)?;
|
check_url_scheme(&custom_thumbnail)?;
|
||||||
let url = proxy_image_link_opt_apub(url, &context).await?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
check_community_user_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
@ -125,7 +123,7 @@ pub async fn create_post(
|
||||||
|
|
||||||
let post_form = PostInsertForm::builder()
|
let post_form = PostInsertForm::builder()
|
||||||
.name(data.name.trim().to_string())
|
.name(data.name.trim().to_string())
|
||||||
.url(url)
|
.url(url.map(Into::into))
|
||||||
.body(body)
|
.body(body)
|
||||||
.alt_text(data.alt_text.clone())
|
.alt_text(data.alt_text.clone())
|
||||||
.community_id(data.community_id)
|
.community_id(data.community_id)
|
||||||
|
|
|
@ -11,7 +11,6 @@ use lemmy_api_common::{
|
||||||
get_url_blocklist,
|
get_url_blocklist,
|
||||||
local_site_to_slur_regex,
|
local_site_to_slur_regex,
|
||||||
process_markdown_opt,
|
process_markdown_opt,
|
||||||
proxy_image_link_opt_apub,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -86,10 +85,6 @@ pub async fn update_post(
|
||||||
Err(LemmyErrorType::NoPostEditAllowed)?
|
Err(LemmyErrorType::NoPostEditAllowed)?
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = match url {
|
|
||||||
Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?),
|
|
||||||
_ => Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let language_id = data.language_id;
|
let language_id = data.language_id;
|
||||||
CommunityLanguage::is_allowed_community_language(
|
CommunityLanguage::is_allowed_community_language(
|
||||||
|
@ -101,7 +96,7 @@ pub async fn update_post(
|
||||||
|
|
||||||
let post_form = PostUpdateForm {
|
let post_form = PostUpdateForm {
|
||||||
name: data.name.clone(),
|
name: data.name.clone(),
|
||||||
url,
|
url: Some(url.map(Into::into)),
|
||||||
body: diesel_option_overwrite(body),
|
body: diesel_option_overwrite(body),
|
||||||
alt_text: diesel_option_overwrite(data.alt_text.clone()),
|
alt_text: diesel_option_overwrite(data.alt_text.clone()),
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
newtypes::DbUrl,
|
newtypes::DbUrl,
|
||||||
schema::{local_image, remote_image},
|
schema::{image_details, local_image, remote_image},
|
||||||
source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm},
|
source::images::{
|
||||||
|
ImageDetails,
|
||||||
|
ImageDetailsForm,
|
||||||
|
LocalImage,
|
||||||
|
LocalImageForm,
|
||||||
|
RemoteImage,
|
||||||
|
RemoteImageForm,
|
||||||
|
},
|
||||||
utils::{get_conn, DbPool},
|
utils::{get_conn, DbPool},
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
|
@ -39,14 +46,13 @@ impl LocalImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteImage {
|
impl RemoteImage {
|
||||||
pub async fn create(pool: &mut DbPool<'_>, links: Vec<Url>) -> Result<usize, Error> {
|
pub async fn create(pool: &mut DbPool<'_>, link_: &Url) -> Result<usize, Error> {
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
let forms = links
|
let form = RemoteImageForm {
|
||||||
.into_iter()
|
link: link_.clone().into(),
|
||||||
.map(|url| RemoteImageForm { link: url.into() })
|
};
|
||||||
.collect::<Vec<_>>();
|
|
||||||
insert_into(remote_image::table)
|
insert_into(remote_image::table)
|
||||||
.values(forms)
|
.values(form)
|
||||||
.on_conflict_do_nothing()
|
.on_conflict_do_nothing()
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await
|
.await
|
||||||
|
@ -67,3 +73,13 @@ impl RemoteImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ImageDetails {
|
||||||
|
pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result<Self, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
insert_into(image_details::table)
|
||||||
|
.values(form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -309,6 +309,16 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
image_details (link) {
|
||||||
|
link -> Text,
|
||||||
|
width -> Int4,
|
||||||
|
height -> Int4,
|
||||||
|
content_type -> Text,
|
||||||
|
published -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
instance (id) {
|
instance (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
@ -849,8 +859,7 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
remote_image (id) {
|
remote_image (link) {
|
||||||
id -> Int4,
|
|
||||||
link -> Text,
|
link -> Text,
|
||||||
published -> Timestamptz,
|
published -> Timestamptz,
|
||||||
}
|
}
|
||||||
|
@ -1055,6 +1064,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
federation_allowlist,
|
federation_allowlist,
|
||||||
federation_blocklist,
|
federation_blocklist,
|
||||||
federation_queue_state,
|
federation_queue_state,
|
||||||
|
image_details,
|
||||||
instance,
|
instance,
|
||||||
instance_block,
|
instance_block,
|
||||||
language,
|
language,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::newtypes::{DbUrl, LocalUserId};
|
use crate::newtypes::{DbUrl, LocalUserId};
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use crate::schema::{local_image, remote_image};
|
use crate::schema::{image_details, local_image, remote_image};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
|
@ -22,7 +22,7 @@ use typed_builder::TypedBuilder;
|
||||||
diesel(belongs_to(crate::source::local_user::LocalUser))
|
diesel(belongs_to(crate::source::local_user::LocalUser))
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))]
|
#[cfg_attr(feature = "full", diesel(primary_key(pictrs_alias)))]
|
||||||
pub struct LocalImage {
|
pub struct LocalImage {
|
||||||
pub local_user_id: Option<LocalUserId>,
|
pub local_user_id: Option<LocalUserId>,
|
||||||
pub pictrs_alias: String,
|
pub pictrs_alias: String,
|
||||||
|
@ -41,11 +41,14 @@ pub struct LocalImageForm {
|
||||||
|
|
||||||
/// Stores all images which are hosted on remote domains. When attempting to proxy an image, it
|
/// Stores all images which are hosted on remote domains. When attempting to proxy an image, it
|
||||||
/// is checked against this table to avoid Lemmy being used as a general purpose proxy.
|
/// is checked against this table to avoid Lemmy being used as a general purpose proxy.
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[skip_serializing_none]
|
||||||
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
|
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
#[cfg_attr(feature = "full", diesel(table_name = remote_image))]
|
#[cfg_attr(feature = "full", diesel(table_name = remote_image))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(primary_key(link)))]
|
||||||
pub struct RemoteImage {
|
pub struct RemoteImage {
|
||||||
pub id: i32,
|
|
||||||
pub link: DbUrl,
|
pub link: DbUrl,
|
||||||
pub published: DateTime<Utc>,
|
pub published: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
@ -56,3 +59,28 @@ pub struct RemoteImage {
|
||||||
pub struct RemoteImageForm {
|
pub struct RemoteImageForm {
|
||||||
pub link: DbUrl,
|
pub link: DbUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = image_details))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(primary_key(link)))]
|
||||||
|
pub struct ImageDetails {
|
||||||
|
pub link: DbUrl,
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
|
pub content_type: String,
|
||||||
|
pub published: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, TypedBuilder)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = image_details))]
|
||||||
|
pub struct ImageDetailsForm {
|
||||||
|
pub link: DbUrl,
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
|
pub content_type: String,
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_db_schema::{
|
||||||
community_follower,
|
community_follower,
|
||||||
community_moderator,
|
community_moderator,
|
||||||
community_person_ban,
|
community_person_ban,
|
||||||
|
image_details,
|
||||||
instance_block,
|
instance_block,
|
||||||
local_user,
|
local_user,
|
||||||
local_user_language,
|
local_user_language,
|
||||||
|
@ -237,10 +238,12 @@ fn queries<'a>() -> Queries<
|
||||||
.inner_join(person::table)
|
.inner_join(person::table)
|
||||||
.inner_join(community::table)
|
.inner_join(community::table)
|
||||||
.inner_join(post::table)
|
.inner_join(post::table)
|
||||||
|
.left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())))
|
||||||
.select((
|
.select((
|
||||||
post::all_columns,
|
post::all_columns,
|
||||||
person::all_columns,
|
person::all_columns,
|
||||||
community::all_columns,
|
community::all_columns,
|
||||||
|
image_details::all_columns.nullable(),
|
||||||
is_creator_banned_from_community,
|
is_creator_banned_from_community,
|
||||||
is_local_user_banned_from_community_selection,
|
is_local_user_banned_from_community_selection,
|
||||||
creator_is_moderator,
|
creator_is_moderator,
|
||||||
|
@ -1666,6 +1669,7 @@ mod tests {
|
||||||
public_key: inserted_person.public_key.clone(),
|
public_key: inserted_person.public_key.clone(),
|
||||||
last_refreshed_at: inserted_person.last_refreshed_at,
|
last_refreshed_at: inserted_person.last_refreshed_at,
|
||||||
},
|
},
|
||||||
|
image_details: None,
|
||||||
creator_banned_from_community: false,
|
creator_banned_from_community: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
creator_is_moderator: false,
|
creator_is_moderator: false,
|
||||||
|
|
|
@ -8,7 +8,7 @@ use lemmy_db_schema::{
|
||||||
community::Community,
|
community::Community,
|
||||||
custom_emoji::CustomEmoji,
|
custom_emoji::CustomEmoji,
|
||||||
custom_emoji_keyword::CustomEmojiKeyword,
|
custom_emoji_keyword::CustomEmojiKeyword,
|
||||||
images::LocalImage,
|
images::{ImageDetails, LocalImage},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
local_site_rate_limit::LocalSiteRateLimit,
|
local_site_rate_limit::LocalSiteRateLimit,
|
||||||
local_user::LocalUser,
|
local_user::LocalUser,
|
||||||
|
@ -130,6 +130,7 @@ pub struct PostView {
|
||||||
pub post: Post,
|
pub post: Post,
|
||||||
pub creator: Person,
|
pub creator: Person,
|
||||||
pub community: Community,
|
pub community: Community,
|
||||||
|
pub image_details: Option<ImageDetails>,
|
||||||
pub creator_banned_from_community: bool,
|
pub creator_banned_from_community: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_is_moderator: bool,
|
pub creator_is_moderator: bool,
|
||||||
|
@ -216,6 +217,7 @@ pub struct VoteView {
|
||||||
pub score: i16,
|
pub score: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(TS, Queryable))]
|
#[cfg_attr(feature = "full", derive(TS, Queryable))]
|
||||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::BodyStream,
|
body::BodyStream,
|
||||||
error,
|
|
||||||
http::{
|
http::{
|
||||||
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
web,
|
web,
|
||||||
web::Query,
|
web::Query,
|
||||||
Error,
|
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
};
|
};
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::{context::LemmyContext, request::PictrsFileDetails};
|
||||||
use lemmy_db_schema::source::{
|
use lemmy_db_schema::source::{
|
||||||
images::{LocalImage, LocalImageForm, RemoteImage},
|
images::{LocalImage, LocalImageForm, RemoteImage},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
|
@ -40,6 +38,7 @@ pub fn config(
|
||||||
)
|
)
|
||||||
// This has optional query params: /image/{filename}?format=jpg&thumbnail=256
|
// This has optional query params: /image/{filename}?format=jpg&thumbnail=256
|
||||||
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
|
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
|
||||||
|
.service(web::resource("/pictrs/image/details/original").route(web::get().to(details)))
|
||||||
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
|
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,11 +55,17 @@ struct Images {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PictrsParams {
|
struct PictrsGetParams {
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
thumbnail: Option<i32>,
|
thumbnail: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PictrsDetailsParams {
|
||||||
|
alias: Option<String>,
|
||||||
|
proxy: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn adapt_request(
|
fn adapt_request(
|
||||||
request: &HttpRequest,
|
request: &HttpRequest,
|
||||||
client: &ClientWithMiddleware,
|
client: &ClientWithMiddleware,
|
||||||
|
@ -92,7 +97,7 @@ async fn upload(
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
client: web::Data<ClientWithMiddleware>,
|
client: web::Data<ClientWithMiddleware>,
|
||||||
context: web::Data<LemmyContext>,
|
context: web::Data<LemmyContext>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
// TODO: check rate limit here
|
// TODO: check rate limit here
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
let image_url = format!("{}image", pictrs_config.url);
|
let image_url = format!("{}image", pictrs_config.url);
|
||||||
|
@ -106,11 +111,10 @@ async fn upload(
|
||||||
.timeout(Duration::from_secs(pictrs_config.upload_timeout))
|
.timeout(Duration::from_secs(pictrs_config.upload_timeout))
|
||||||
.body(Body::wrap_stream(make_send(body)))
|
.body(Body::wrap_stream(make_send(body)))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await?;
|
||||||
.map_err(error::ErrorBadRequest)?;
|
|
||||||
|
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
let images = res.json::<Images>().await.map_err(error::ErrorBadRequest)?;
|
let images = res.json::<Images>().await?;
|
||||||
if let Some(images) = &images.files {
|
if let Some(images) = &images.files {
|
||||||
for uploaded_image in images {
|
for uploaded_image in images {
|
||||||
let form = LocalImageForm {
|
let form = LocalImageForm {
|
||||||
|
@ -118,9 +122,7 @@ async fn upload(
|
||||||
pictrs_alias: uploaded_image.file.to_string(),
|
pictrs_alias: uploaded_image.file.to_string(),
|
||||||
pictrs_delete_token: uploaded_image.delete_token.to_string(),
|
pictrs_delete_token: uploaded_image.delete_token.to_string(),
|
||||||
};
|
};
|
||||||
LocalImage::create(&mut context.pool(), &form)
|
LocalImage::create(&mut context.pool(), &form).await?;
|
||||||
.await
|
|
||||||
.map_err(error::ErrorBadRequest)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,16 +131,14 @@ async fn upload(
|
||||||
|
|
||||||
async fn full_res(
|
async fn full_res(
|
||||||
filename: web::Path<String>,
|
filename: web::Path<String>,
|
||||||
web::Query(params): web::Query<PictrsParams>,
|
web::Query(params): web::Query<PictrsGetParams>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
client: web::Data<ClientWithMiddleware>,
|
client: web::Data<ClientWithMiddleware>,
|
||||||
context: web::Data<LemmyContext>,
|
context: web::Data<LemmyContext>,
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
// block access to images if instance is private and unauthorized, public
|
// block access to images if instance is private and unauthorized, public
|
||||||
let local_site = LocalSite::read(&mut context.pool())
|
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||||
.await
|
|
||||||
.map_err(error::ErrorBadRequest)?;
|
|
||||||
if local_site.private_instance && local_user_view.is_none() {
|
if local_site.private_instance && local_user_view.is_none() {
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ async fn image(
|
||||||
url: String,
|
url: String,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
client: &ClientWithMiddleware,
|
client: &ClientWithMiddleware,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let mut client_req = adapt_request(&req, client, url);
|
let mut client_req = adapt_request(&req, client, url);
|
||||||
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
@ -180,7 +180,7 @@ async fn image(
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = client_req.send().await.map_err(error::ErrorBadRequest)?;
|
let res = client_req.send().await?;
|
||||||
|
|
||||||
if res.status() == StatusCode::NOT_FOUND {
|
if res.status() == StatusCode::NOT_FOUND {
|
||||||
return Ok(HttpResponse::NotFound().finish());
|
return Ok(HttpResponse::NotFound().finish());
|
||||||
|
@ -202,7 +202,7 @@ async fn delete(
|
||||||
context: web::Data<LemmyContext>,
|
context: web::Data<LemmyContext>,
|
||||||
// require login
|
// require login
|
||||||
_local_user_view: LocalUserView,
|
_local_user_view: LocalUserView,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let (token, file) = components.into_inner();
|
let (token, file) = components.into_inner();
|
||||||
|
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
|
@ -214,11 +214,9 @@ async fn delete(
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = client_req.send().await.map_err(error::ErrorBadRequest)?;
|
let res = client_req.send().await?;
|
||||||
|
|
||||||
LocalImage::delete_by_alias(&mut context.pool(), &file)
|
LocalImage::delete_by_alias(&mut context.pool(), &file).await?;
|
||||||
.await
|
|
||||||
.map_err(error::ErrorBadRequest)?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream())))
|
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream())))
|
||||||
}
|
}
|
||||||
|
@ -228,8 +226,33 @@ pub struct ImageProxyParams {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn details(
|
||||||
|
web::Query(params): web::Query<PictrsDetailsParams>,
|
||||||
|
client: web::Data<ClientWithMiddleware>,
|
||||||
|
context: web::Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
|
|
||||||
|
let mut url = format!("{}image/details/original", pictrs_config.url);
|
||||||
|
if let Some(alias) = params.alias {
|
||||||
|
url = format!("{url}?alias={alias}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(proxy) = params.proxy {
|
||||||
|
url = format!("{url}?proxy={proxy}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.get(url).send().await?;
|
||||||
|
let status = res.status();
|
||||||
|
let json = res.json::<PictrsFileDetails>().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::build(status).json(json))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn image_proxy(
|
pub async fn image_proxy(
|
||||||
Query(params): Query<ImageProxyParams>,
|
Query(params): Query<ImageProxyParams>,
|
||||||
|
req: HttpRequest,
|
||||||
|
client: web::Data<ClientWithMiddleware>,
|
||||||
context: web::Data<LemmyContext>,
|
context: web::Data<LemmyContext>,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let url = Url::parse(&decode(¶ms.url)?)?;
|
let url = Url::parse(&decode(¶ms.url)?)?;
|
||||||
|
@ -240,9 +263,8 @@ pub async fn image_proxy(
|
||||||
|
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url);
|
let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url);
|
||||||
let image_response = context.client().get(url).send().await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().streaming(image_response.bytes_stream()))
|
image(url, req, &client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static
|
fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static
|
||||||
|
|
|
@ -75,7 +75,7 @@ services:
|
||||||
init: true
|
init: true
|
||||||
|
|
||||||
pictrs:
|
pictrs:
|
||||||
image: asonix/pictrs:0.5.0-rc.2
|
image: asonix/pictrs:0.5.13
|
||||||
# this needs to match the pictrs url in lemmy.hjson
|
# this needs to match the pictrs url in lemmy.hjson
|
||||||
hostname: pictrs
|
hostname: pictrs
|
||||||
# we can set options to pictrs like this, here we set max. image size and forced format for conversion
|
# we can set options to pictrs like this, here we set max. image size and forced format for conversion
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
ALTER TABLE remote_image
|
||||||
|
ADD UNIQUE (link),
|
||||||
|
DROP CONSTRAINT remote_image_pkey,
|
||||||
|
ADD COLUMN id serial PRIMARY KEY;
|
||||||
|
|
||||||
|
DROP TABLE image_details;
|
||||||
|
|
15
migrations/2024-05-05-162540_add_image_detail_table/up.sql
Normal file
15
migrations/2024-05-05-162540_add_image_detail_table/up.sql
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
-- Drop the id column from the remote_image table, just use link
|
||||||
|
ALTER TABLE remote_image
|
||||||
|
DROP COLUMN id,
|
||||||
|
ADD PRIMARY KEY (link),
|
||||||
|
DROP CONSTRAINT remote_image_link_key;
|
||||||
|
|
||||||
|
-- No good way to do references here unfortunately, unless we combine the images tables
|
||||||
|
-- The link should be the URL, not the pictrs_alias, to allow joining from post.thumbnail_url
|
||||||
|
CREATE TABLE image_details (
|
||||||
|
link text PRIMARY KEY,
|
||||||
|
width integer NOT NULL,
|
||||||
|
height integer NOT NULL,
|
||||||
|
content_type text NOT NULL,
|
||||||
|
published timestamptz DEFAULT now() NOT NULL
|
||||||
|
);
|
Loading…
Reference in a new issue