mirror of
https://github.com/girlbossceo/conduwuit.git
synced 2025-03-14 18:55:37 +00:00
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",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"image",
|
||||
"ipaddress",
|
||||
"itertools 0.13.0",
|
||||
"libc",
|
||||
|
|
|
@ -71,7 +71,6 @@ figment.workspace = true
|
|||
futures.workspace = true
|
||||
http-body-util.workspace = true
|
||||
http.workspace = true
|
||||
image.workspace = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
libc.workspace = true
|
||||
|
|
|
@ -48,8 +48,6 @@ pub enum Error {
|
|||
Http(#[from] http::Error),
|
||||
#[error(transparent)]
|
||||
HttpHeader(#[from] http::header::InvalidHeaderValue),
|
||||
#[error("Image error: {0}")]
|
||||
Image(#[from] image::error::ImageError),
|
||||
#[error("Join error: {0}")]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
#[error(transparent)]
|
||||
|
|
|
@ -41,8 +41,10 @@ default = [
|
|||
"gzip_compression",
|
||||
"io_uring",
|
||||
"jemalloc",
|
||||
"media_thumbnail",
|
||||
"release_max_log_level",
|
||||
"systemd",
|
||||
"url_preview",
|
||||
"zstd_compression",
|
||||
]
|
||||
|
||||
|
@ -83,6 +85,9 @@ jemalloc_prof = [
|
|||
jemalloc_stats = [
|
||||
"conduwuit-core/jemalloc_stats",
|
||||
]
|
||||
media_thumbnail = [
|
||||
"conduwuit-service/media_thumbnail",
|
||||
]
|
||||
perf_measurements = [
|
||||
"dep:opentelemetry",
|
||||
"dep:tracing-flame",
|
||||
|
@ -121,6 +126,9 @@ tokio_console = [
|
|||
"dep:console-subscriber",
|
||||
"tokio/tracing",
|
||||
]
|
||||
url_preview = [
|
||||
"conduwuit-service/url_preview",
|
||||
]
|
||||
zstd_compression = [
|
||||
"conduwuit-api/zstd_compression",
|
||||
"conduwuit-core/zstd_compression",
|
||||
|
|
|
@ -28,8 +28,8 @@ element_hacks = []
|
|||
gzip_compression = [
|
||||
"reqwest/gzip",
|
||||
]
|
||||
zstd_compression = [
|
||||
"reqwest/zstd",
|
||||
media_thumbnail = [
|
||||
"dep:image",
|
||||
]
|
||||
release_max_log_level = [
|
||||
"tracing/max_level_trace",
|
||||
|
@ -37,6 +37,13 @@ release_max_log_level = [
|
|||
"log/max_level_trace",
|
||||
"log/release_max_level_info",
|
||||
]
|
||||
url_preview = [
|
||||
"dep:image",
|
||||
"dep:webpage",
|
||||
]
|
||||
zstd_compression = [
|
||||
"reqwest/zstd",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
arrayvec.workspace = true
|
||||
|
@ -51,6 +58,7 @@ futures.workspace = true
|
|||
hickory-resolver.workspace = true
|
||||
http.workspace = true
|
||||
image.workspace = true
|
||||
image.optional = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
|
@ -73,6 +81,7 @@ tokio.workspace = true
|
|||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
webpage.workspace = true
|
||||
webpage.optional = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
|
|||
use conduwuit::{
|
||||
debug, debug_info, err,
|
||||
utils::{str_from_bytes, stream::TryIgnore, string_from_bytes, ReadyExt},
|
||||
Err, Error, Result,
|
||||
Err, Result,
|
||||
};
|
||||
use database::{Database, Interfix, Map};
|
||||
use futures::StreamExt;
|
||||
|
@ -123,30 +123,21 @@ impl Data {
|
|||
|
||||
let content_type = parts
|
||||
.next()
|
||||
.map(|bytes| {
|
||||
string_from_bytes(bytes).map_err(|_| {
|
||||
Error::bad_database("Content type in mediaid_file is invalid unicode.")
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
.map(string_from_bytes)
|
||||
.transpose()
|
||||
.map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?;
|
||||
|
||||
let content_disposition_bytes = parts
|
||||
let content_disposition = parts
|
||||
.next()
|
||||
.ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?;
|
||||
|
||||
let content_disposition = if content_disposition_bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
string_from_bytes(content_disposition_bytes)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Content Disposition in mediaid_file is invalid unicode.",
|
||||
)
|
||||
})?
|
||||
.parse()?,
|
||||
)
|
||||
};
|
||||
.map(Some)
|
||||
.ok_or_else(|| err!(Database(error!(?mxc, "Media ID in db is invalid."))))?
|
||||
.filter(|bytes| !bytes.is_empty())
|
||||
.map(string_from_bytes)
|
||||
.transpose()
|
||||
.map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.transpose()?;
|
||||
|
||||
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 image::ImageReader as ImgReader;
|
||||
use ipaddress::IPAddress;
|
||||
use ruma::Mxc;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
use webpage::HTML;
|
||||
|
||||
use super::{Service, MXC_LENGTH};
|
||||
use super::Service;
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
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)
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub async fn get_url_preview(&self, url: &Url) -> Result<UrlPreviewData> {
|
||||
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)
|
||||
}
|
||||
|
||||
#[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)]
|
||||
async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
|
||||
use webpage::HTML;
|
||||
|
||||
let client = &self.services.client.url_preview;
|
||||
let mut response = client.get(url).send().await?;
|
||||
|
||||
|
@ -159,6 +178,12 @@ async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
|
|||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "url_preview"))]
|
||||
#[implement(Service)]
|
||||
async fn download_html(&self, _url: &str) -> Result<UrlPreviewData> {
|
||||
Err!(FeatureDisabled("url_preview"))
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub fn url_preview_allowed(&self, url: &Url) -> bool {
|
||||
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 image::{imageops::FilterType, DynamicImage};
|
||||
use std::{cmp, num::Saturating as Sat};
|
||||
|
||||
use conduwuit::{checked, err, implement, Result};
|
||||
use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId};
|
||||
use tokio::{
|
||||
fs,
|
||||
|
@ -67,65 +73,89 @@ impl super::Service {
|
|||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Using saved thumbnail
|
||||
#[tracing::instrument(skip(self), name = "saved", level = "debug")]
|
||||
async fn get_thumbnail_saved(&self, data: Metadata) -> Result<Option<FileMeta>> {
|
||||
let mut content = Vec::new();
|
||||
let path = self.get_media_file(&data.key);
|
||||
fs::File::open(path)
|
||||
.await?
|
||||
.read_to_end(&mut content)
|
||||
.await?;
|
||||
|
||||
Ok(Some(into_filemeta(data, content)))
|
||||
}
|
||||
|
||||
/// Generate a thumbnail
|
||||
#[tracing::instrument(skip(self), name = "generate", level = "debug")]
|
||||
async fn get_thumbnail_generate(
|
||||
&self,
|
||||
mxc: &Mxc<'_>,
|
||||
dim: &Dim,
|
||||
data: Metadata,
|
||||
) -> Result<Option<FileMeta>> {
|
||||
let mut content = Vec::new();
|
||||
let path = self.get_media_file(&data.key);
|
||||
fs::File::open(path)
|
||||
.await?
|
||||
.read_to_end(&mut content)
|
||||
.await?;
|
||||
|
||||
let Ok(image) = image::load_from_memory(&content) else {
|
||||
// Couldn't parse file to generate thumbnail, send original
|
||||
return Ok(Some(into_filemeta(data, content)));
|
||||
};
|
||||
|
||||
if dim.width > image.width() || dim.height > image.height() {
|
||||
return Ok(Some(into_filemeta(data, content)));
|
||||
}
|
||||
|
||||
let mut thumbnail_bytes = Vec::new();
|
||||
let thumbnail = thumbnail_generate(&image, dim)?;
|
||||
thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), image::ImageFormat::Png)?;
|
||||
|
||||
// Save thumbnail in database so we don't have to generate it again next time
|
||||
let thumbnail_key = self.db.create_file_metadata(
|
||||
mxc,
|
||||
None,
|
||||
dim,
|
||||
data.content_disposition.as_ref(),
|
||||
data.content_type.as_deref(),
|
||||
)?;
|
||||
|
||||
let mut f = self.create_media_file(&thumbnail_key).await?;
|
||||
f.write_all(&thumbnail_bytes).await?;
|
||||
|
||||
Ok(Some(into_filemeta(data, thumbnail_bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_generate(image: &DynamicImage, requested: &Dim) -> Result<DynamicImage> {
|
||||
/// Using saved thumbnail
|
||||
#[implement(super::Service)]
|
||||
#[tracing::instrument(name = "saved", level = "debug", skip(self, data))]
|
||||
async fn get_thumbnail_saved(&self, data: Metadata) -> Result<Option<FileMeta>> {
|
||||
let mut content = Vec::new();
|
||||
let path = self.get_media_file(&data.key);
|
||||
fs::File::open(path)
|
||||
.await?
|
||||
.read_to_end(&mut content)
|
||||
.await?;
|
||||
|
||||
Ok(Some(into_filemeta(data, content)))
|
||||
}
|
||||
|
||||
/// Generate a thumbnail
|
||||
#[cfg(feature = "media_thumbnail")]
|
||||
#[implement(super::Service)]
|
||||
#[tracing::instrument(name = "generate", level = "debug", skip(self, data))]
|
||||
async fn get_thumbnail_generate(
|
||||
&self,
|
||||
mxc: &Mxc<'_>,
|
||||
dim: &Dim,
|
||||
data: Metadata,
|
||||
) -> Result<Option<FileMeta>> {
|
||||
let mut content = Vec::new();
|
||||
let path = self.get_media_file(&data.key);
|
||||
fs::File::open(path)
|
||||
.await?
|
||||
.read_to_end(&mut content)
|
||||
.await?;
|
||||
|
||||
let Ok(image) = image::load_from_memory(&content) else {
|
||||
// Couldn't parse file to generate thumbnail, send original
|
||||
return Ok(Some(into_filemeta(data, content)));
|
||||
};
|
||||
|
||||
if dim.width > image.width() || dim.height > image.height() {
|
||||
return Ok(Some(into_filemeta(data, content)));
|
||||
}
|
||||
|
||||
let mut thumbnail_bytes = Vec::new();
|
||||
let thumbnail = thumbnail_generate(&image, dim)?;
|
||||
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
|
||||
let thumbnail_key = self.db.create_file_metadata(
|
||||
mxc,
|
||||
None,
|
||||
dim,
|
||||
data.content_disposition.as_ref(),
|
||||
data.content_type.as_deref(),
|
||||
)?;
|
||||
|
||||
let mut f = self.create_media_file(&thumbnail_key).await?;
|
||||
f.write_all(&thumbnail_bytes).await?;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
#[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 Dim { width, height, .. } = requested.scaled(&Dim {
|
||||
width: image.width(),
|
||||
|
|
Loading…
Add table
Reference in a new issue