Add default-enabled feature-gates for url_preview and media_thumbnail

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-12-18 21:29:30 +00:00
parent 0238f27605
commit cc1889d135
8 changed files with 182 additions and 123 deletions

1
Cargo.lock generated
View file

@ -723,7 +723,6 @@ dependencies = [
"hardened_malloc-rs", "hardened_malloc-rs",
"http", "http",
"http-body-util", "http-body-util",
"image",
"ipaddress", "ipaddress",
"itertools 0.13.0", "itertools 0.13.0",
"libc", "libc",

View file

@ -71,7 +71,6 @@ figment.workspace = true
futures.workspace = true futures.workspace = true
http-body-util.workspace = true http-body-util.workspace = true
http.workspace = true http.workspace = true
image.workspace = true
ipaddress.workspace = true ipaddress.workspace = true
itertools.workspace = true itertools.workspace = true
libc.workspace = true libc.workspace = true

View file

@ -48,8 +48,6 @@ pub enum Error {
Http(#[from] http::Error), Http(#[from] http::Error),
#[error(transparent)] #[error(transparent)]
HttpHeader(#[from] http::header::InvalidHeaderValue), HttpHeader(#[from] http::header::InvalidHeaderValue),
#[error("Image error: {0}")]
Image(#[from] image::error::ImageError),
#[error("Join error: {0}")] #[error("Join error: {0}")]
JoinError(#[from] tokio::task::JoinError), JoinError(#[from] tokio::task::JoinError),
#[error(transparent)] #[error(transparent)]

View file

@ -41,8 +41,10 @@ default = [
"gzip_compression", "gzip_compression",
"io_uring", "io_uring",
"jemalloc", "jemalloc",
"media_thumbnail",
"release_max_log_level", "release_max_log_level",
"systemd", "systemd",
"url_preview",
"zstd_compression", "zstd_compression",
] ]
@ -83,6 +85,9 @@ jemalloc_prof = [
jemalloc_stats = [ jemalloc_stats = [
"conduwuit-core/jemalloc_stats", "conduwuit-core/jemalloc_stats",
] ]
media_thumbnail = [
"conduwuit-service/media_thumbnail",
]
perf_measurements = [ perf_measurements = [
"dep:opentelemetry", "dep:opentelemetry",
"dep:tracing-flame", "dep:tracing-flame",
@ -121,6 +126,9 @@ tokio_console = [
"dep:console-subscriber", "dep:console-subscriber",
"tokio/tracing", "tokio/tracing",
] ]
url_preview = [
"conduwuit-service/url_preview",
]
zstd_compression = [ zstd_compression = [
"conduwuit-api/zstd_compression", "conduwuit-api/zstd_compression",
"conduwuit-core/zstd_compression", "conduwuit-core/zstd_compression",

View file

@ -28,8 +28,8 @@ element_hacks = []
gzip_compression = [ gzip_compression = [
"reqwest/gzip", "reqwest/gzip",
] ]
zstd_compression = [ media_thumbnail = [
"reqwest/zstd", "dep:image",
] ]
release_max_log_level = [ release_max_log_level = [
"tracing/max_level_trace", "tracing/max_level_trace",
@ -37,6 +37,13 @@ release_max_log_level = [
"log/max_level_trace", "log/max_level_trace",
"log/release_max_level_info", "log/release_max_level_info",
] ]
url_preview = [
"dep:image",
"dep:webpage",
]
zstd_compression = [
"reqwest/zstd",
]
[dependencies] [dependencies]
arrayvec.workspace = true arrayvec.workspace = true
@ -51,6 +58,7 @@ futures.workspace = true
hickory-resolver.workspace = true hickory-resolver.workspace = true
http.workspace = true http.workspace = true
image.workspace = true image.workspace = true
image.optional = true
ipaddress.workspace = true ipaddress.workspace = true
itertools.workspace = true itertools.workspace = true
jsonwebtoken.workspace = true jsonwebtoken.workspace = true
@ -73,6 +81,7 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
url.workspace = true url.workspace = true
webpage.workspace = true webpage.workspace = true
webpage.optional = true
[lints] [lints]
workspace = true workspace = true

View file

@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
use conduwuit::{ use conduwuit::{
debug, debug_info, err, debug, debug_info, err,
utils::{str_from_bytes, stream::TryIgnore, string_from_bytes, ReadyExt}, utils::{str_from_bytes, stream::TryIgnore, string_from_bytes, ReadyExt},
Err, Error, Result, Err, Result,
}; };
use database::{Database, Interfix, Map}; use database::{Database, Interfix, Map};
use futures::StreamExt; use futures::StreamExt;
@ -123,30 +123,21 @@ impl Data {
let content_type = parts let content_type = parts
.next() .next()
.map(|bytes| { .map(string_from_bytes)
string_from_bytes(bytes).map_err(|_| { .transpose()
Error::bad_database("Content type in mediaid_file is invalid unicode.") .map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?;
})
})
.transpose()?;
let content_disposition_bytes = parts let content_disposition = parts
.next() .next()
.ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?; .map(Some)
.ok_or_else(|| err!(Database(error!(?mxc, "Media ID in db is invalid."))))?
let content_disposition = if content_disposition_bytes.is_empty() { .filter(|bytes| !bytes.is_empty())
None .map(string_from_bytes)
} else { .transpose()
Some( .map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?
string_from_bytes(content_disposition_bytes) .as_deref()
.map_err(|_| { .map(str::parse)
Error::bad_database( .transpose()?;
"Content Disposition in mediaid_file is invalid unicode.",
)
})?
.parse()?,
)
};
Ok(Metadata { content_disposition, content_type, key }) Ok(Metadata { content_disposition, content_type, key })
} }

View file

@ -1,15 +1,19 @@
use std::{io::Cursor, time::SystemTime}; //! URL Previews
//!
//! This functionality is gated by 'url_preview', but not at the unit level for
//! historical and simplicity reasons. Instead the feature gates the inclusion
//! of dependencies and nulls out results through the existing interface when
//! not featured.
use conduwuit::{debug, utils, Err, Result}; use std::time::SystemTime;
use conduwuit::{debug, Err, Result};
use conduwuit_core::implement; use conduwuit_core::implement;
use image::ImageReader as ImgReader;
use ipaddress::IPAddress; use ipaddress::IPAddress;
use ruma::Mxc;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
use webpage::HTML;
use super::{Service, MXC_LENGTH}; use super::Service;
#[derive(Serialize, Default)] #[derive(Serialize, Default)]
pub struct UrlPreviewData { pub struct UrlPreviewData {
@ -41,34 +45,6 @@ pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<
self.db.set_url_preview(url, data, now) self.db.set_url_preview(url, data, now)
} }
#[implement(Service)]
pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
let client = &self.services.client.url_preview;
let image = client.get(url).send().await?.bytes().await?;
let mxc = Mxc {
server_name: self.services.globals.server_name(),
media_id: &utils::random_string(MXC_LENGTH),
};
self.create(&mxc, None, None, None, &image).await?;
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
| Err(_) => (None, None),
| Ok(reader) => match reader.into_dimensions() {
| Err(_) => (None, None),
| Ok((width, height)) => (Some(width), Some(height)),
},
};
Ok(UrlPreviewData {
image: Some(mxc.to_string()),
image_size: Some(image.len()),
image_width: width,
image_height: height,
..Default::default()
})
}
#[implement(Service)] #[implement(Service)]
pub async fn get_url_preview(&self, url: &Url) -> Result<UrlPreviewData> { pub async fn get_url_preview(&self, url: &Url) -> Result<UrlPreviewData> {
if let Ok(preview) = self.db.get_url_preview(url.as_str()).await { if let Ok(preview) = self.db.get_url_preview(url.as_str()).await {
@ -121,8 +97,51 @@ async fn request_url_preview(&self, url: &Url) -> Result<UrlPreviewData> {
Ok(data) Ok(data)
} }
#[cfg(feature = "url_preview")]
#[implement(Service)]
pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
use conduwuit::utils::random_string;
use image::ImageReader;
use ruma::Mxc;
let image = self.services.client.url_preview.get(url).send().await?;
let image = image.bytes().await?;
let mxc = Mxc {
server_name: self.services.globals.server_name(),
media_id: &random_string(super::MXC_LENGTH),
};
self.create(&mxc, None, None, None, &image).await?;
let cursor = std::io::Cursor::new(&image);
let (width, height) = match ImageReader::new(cursor).with_guessed_format() {
| Err(_) => (None, None),
| Ok(reader) => match reader.into_dimensions() {
| Err(_) => (None, None),
| Ok((width, height)) => (Some(width), Some(height)),
},
};
Ok(UrlPreviewData {
image: Some(mxc.to_string()),
image_size: Some(image.len()),
image_width: width,
image_height: height,
..Default::default()
})
}
#[cfg(not(feature = "url_preview"))]
#[implement(Service)]
pub async fn download_image(&self, _url: &str) -> Result<UrlPreviewData> {
Err!(FeatureDisabled("url_preview"))
}
#[cfg(feature = "url_preview")]
#[implement(Service)] #[implement(Service)]
async fn download_html(&self, url: &str) -> Result<UrlPreviewData> { async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
use webpage::HTML;
let client = &self.services.client.url_preview; let client = &self.services.client.url_preview;
let mut response = client.get(url).send().await?; let mut response = client.get(url).send().await?;
@ -159,6 +178,12 @@ async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
Ok(data) Ok(data)
} }
#[cfg(not(feature = "url_preview"))]
#[implement(Service)]
async fn download_html(&self, _url: &str) -> Result<UrlPreviewData> {
Err!(FeatureDisabled("url_preview"))
}
#[implement(Service)] #[implement(Service)]
pub fn url_preview_allowed(&self, url: &Url) -> bool { pub fn url_preview_allowed(&self, url: &Url) -> bool {
if ["http", "https"] if ["http", "https"]

View file

@ -1,7 +1,13 @@
use std::{cmp, io::Cursor, num::Saturating as Sat}; //! Media Thumbnails
//!
//! This functionality is gated by 'media_thumbnail', but not at the unit level
//! for historical and simplicity reasons. Instead the feature gates the
//! inclusion of dependencies and nulls out results using the existing interface
//! when not featured.
use conduwuit::{checked, err, Result}; use std::{cmp, num::Saturating as Sat};
use image::{imageops::FilterType, DynamicImage};
use conduwuit::{checked, err, implement, Result};
use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId}; use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId};
use tokio::{ use tokio::{
fs, fs,
@ -67,9 +73,11 @@ impl super::Service {
Ok(None) Ok(None)
} }
} }
}
/// Using saved thumbnail /// Using saved thumbnail
#[tracing::instrument(skip(self), name = "saved", level = "debug")] #[implement(super::Service)]
#[tracing::instrument(name = "saved", level = "debug", skip(self, data))]
async fn get_thumbnail_saved(&self, data: Metadata) -> Result<Option<FileMeta>> { async fn get_thumbnail_saved(&self, data: Metadata) -> Result<Option<FileMeta>> {
let mut content = Vec::new(); let mut content = Vec::new();
let path = self.get_media_file(&data.key); let path = self.get_media_file(&data.key);
@ -82,7 +90,9 @@ impl super::Service {
} }
/// Generate a thumbnail /// Generate a thumbnail
#[tracing::instrument(skip(self), name = "generate", level = "debug")] #[cfg(feature = "media_thumbnail")]
#[implement(super::Service)]
#[tracing::instrument(name = "generate", level = "debug", skip(self, data))]
async fn get_thumbnail_generate( async fn get_thumbnail_generate(
&self, &self,
mxc: &Mxc<'_>, mxc: &Mxc<'_>,
@ -107,7 +117,10 @@ impl super::Service {
let mut thumbnail_bytes = Vec::new(); let mut thumbnail_bytes = Vec::new();
let thumbnail = thumbnail_generate(&image, dim)?; let thumbnail = thumbnail_generate(&image, dim)?;
thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), image::ImageFormat::Png)?; let mut cursor = std::io::Cursor::new(&mut thumbnail_bytes);
thumbnail
.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|error| err!(error!(?error, "Error writing PNG thumbnail.")))?;
// Save thumbnail in database so we don't have to generate it again next time // Save thumbnail in database so we don't have to generate it again next time
let thumbnail_key = self.db.create_file_metadata( let thumbnail_key = self.db.create_file_metadata(
@ -123,9 +136,26 @@ impl super::Service {
Ok(Some(into_filemeta(data, thumbnail_bytes))) Ok(Some(into_filemeta(data, thumbnail_bytes)))
} }
#[cfg(not(feature = "media_thumbnail"))]
#[implement(super::Service)]
#[tracing::instrument(name = "fallback", level = "debug", skip_all)]
async fn get_thumbnail_generate(
&self,
_mxc: &Mxc<'_>,
_dim: &Dim,
data: Metadata,
) -> Result<Option<FileMeta>> {
self.get_thumbnail_saved(data).await
} }
fn thumbnail_generate(image: &DynamicImage, requested: &Dim) -> Result<DynamicImage> { #[cfg(feature = "media_thumbnail")]
fn thumbnail_generate(
image: &image::DynamicImage,
requested: &Dim,
) -> Result<image::DynamicImage> {
use image::imageops::FilterType;
let thumbnail = if !requested.crop() { let thumbnail = if !requested.crop() {
let Dim { width, height, .. } = requested.scaled(&Dim { let Dim { width, height, .. } = requested.scaled(&Dim {
width: image.width(), width: image.width(),