Add default-enabled feature-gates for url_preview and media_thumbnail
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
0238f27605
commit
cc1889d135
8 changed files with 182 additions and 123 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue