mirror of
https://github.com/girlbossceo/conduwuit.git
synced 2024-11-29 00:36:19 +00:00
add thumbnail dimension structure
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
7b0e830f4c
commit
4d42a29c51
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -2975,7 +2975,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
"js_int",
|
"js_int",
|
||||||
|
@ -2997,7 +2997,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-appservice-api"
|
name = "ruma-appservice-api"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -3009,7 +3009,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-client-api"
|
name = "ruma-client-api"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
|
@ -3032,7 +3032,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-common"
|
name = "ruma-common"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
@ -3062,7 +3062,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap 2.4.0",
|
"indexmap 2.4.0",
|
||||||
|
@ -3086,7 +3086,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-federation-api"
|
name = "ruma-federation-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
|
@ -3104,7 +3104,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identifiers-validation"
|
name = "ruma-identifiers-validation"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -3113,7 +3113,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identity-service-api"
|
name = "ruma-identity-service-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -3123,7 +3123,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-macros"
|
name = "ruma-macros"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
|
@ -3138,7 +3138,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-push-gateway-api"
|
name = "ruma-push-gateway-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -3150,7 +3150,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-server-util"
|
name = "ruma-server-util"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
@ -3163,7 +3163,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-signatures"
|
name = "ruma-signatures"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
@ -3179,7 +3179,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-state-res"
|
name = "ruma-state-res"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
"js_int",
|
"js_int",
|
||||||
|
|
|
@ -314,7 +314,7 @@ version = "0.1.2"
|
||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://github.com/girlbossceo/ruwuma"
|
git = "https://github.com/girlbossceo/ruwuma"
|
||||||
#branch = "conduwuit-changes"
|
#branch = "conduwuit-changes"
|
||||||
rev = "25fbd64b968c5d5088c07750aaa4873e072831b0"
|
rev = "a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
@ -14,7 +14,7 @@ use ruma::{
|
||||||
},
|
},
|
||||||
Mxc,
|
Mxc,
|
||||||
};
|
};
|
||||||
use service::media::{FileMeta, MXC_LENGTH};
|
use service::media::{Dim, FileMeta, MXC_LENGTH};
|
||||||
|
|
||||||
use crate::{Ruma, RumaResponse};
|
use crate::{Ruma, RumaResponse};
|
||||||
|
|
||||||
|
@ -326,22 +326,12 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||||
media_id: &body.media_id,
|
media_id: &body.media_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
content,
|
content,
|
||||||
content_type,
|
content_type,
|
||||||
content_disposition,
|
content_disposition,
|
||||||
}) = services
|
}) = services.media.get_thumbnail(&mxc, &dim).await?
|
||||||
.media
|
|
||||||
.get_thumbnail(
|
|
||||||
&mxc,
|
|
||||||
body.width
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?,
|
|
||||||
body.height
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
{
|
||||||
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
|
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use conduit::{
|
||||||
use database::{Database, Map};
|
use database::{Database, Map};
|
||||||
use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
|
use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
|
||||||
|
|
||||||
use super::preview::UrlPreviewData;
|
use super::{preview::UrlPreviewData, thumbnail::Dim};
|
||||||
|
|
||||||
pub(crate) struct Data {
|
pub(crate) struct Data {
|
||||||
mediaid_file: Arc<Map>,
|
mediaid_file: Arc<Map>,
|
||||||
|
@ -33,8 +33,8 @@ impl Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_file_metadata(
|
pub(super) fn create_file_metadata(
|
||||||
&self, mxc: &Mxc<'_>, user: Option<&UserId>, width: u32, height: u32,
|
&self, mxc: &Mxc<'_>, user: Option<&UserId>, dim: &Dim, content_disposition: Option<&ContentDisposition>,
|
||||||
content_disposition: Option<&ContentDisposition>, content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let mut key: Vec<u8> = Vec::new();
|
let mut key: Vec<u8> = Vec::new();
|
||||||
key.extend_from_slice(b"mxc://");
|
key.extend_from_slice(b"mxc://");
|
||||||
|
@ -42,8 +42,8 @@ impl Data {
|
||||||
key.extend_from_slice(b"/");
|
key.extend_from_slice(b"/");
|
||||||
key.extend_from_slice(mxc.media_id.as_bytes());
|
key.extend_from_slice(mxc.media_id.as_bytes());
|
||||||
key.push(0xFF);
|
key.push(0xFF);
|
||||||
key.extend_from_slice(&width.to_be_bytes());
|
key.extend_from_slice(&dim.width.to_be_bytes());
|
||||||
key.extend_from_slice(&height.to_be_bytes());
|
key.extend_from_slice(&dim.height.to_be_bytes());
|
||||||
key.push(0xFF);
|
key.push(0xFF);
|
||||||
key.extend_from_slice(
|
key.extend_from_slice(
|
||||||
content_disposition
|
content_disposition
|
||||||
|
@ -128,15 +128,15 @@ impl Data {
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result<Metadata> {
|
pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Metadata> {
|
||||||
let mut prefix: Vec<u8> = Vec::new();
|
let mut prefix: Vec<u8> = Vec::new();
|
||||||
prefix.extend_from_slice(b"mxc://");
|
prefix.extend_from_slice(b"mxc://");
|
||||||
prefix.extend_from_slice(mxc.server_name.as_bytes());
|
prefix.extend_from_slice(mxc.server_name.as_bytes());
|
||||||
prefix.extend_from_slice(b"/");
|
prefix.extend_from_slice(b"/");
|
||||||
prefix.extend_from_slice(mxc.media_id.as_bytes());
|
prefix.extend_from_slice(mxc.media_id.as_bytes());
|
||||||
prefix.push(0xFF);
|
prefix.push(0xFF);
|
||||||
prefix.extend_from_slice(&width.to_be_bytes());
|
prefix.extend_from_slice(&dim.width.to_be_bytes());
|
||||||
prefix.extend_from_slice(&height.to_be_bytes());
|
prefix.extend_from_slice(&dim.height.to_be_bytes());
|
||||||
prefix.push(0xFF);
|
prefix.push(0xFF);
|
||||||
|
|
||||||
let (key, _) = self
|
let (key, _) = self
|
||||||
|
|
|
@ -13,13 +13,14 @@ use conduit::{
|
||||||
utils::{self, MutexMap},
|
utils::{self, MutexMap},
|
||||||
warn, Err, Result, Server,
|
warn, Err, Result, Server,
|
||||||
};
|
};
|
||||||
use data::{Data, Metadata};
|
|
||||||
use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
|
use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::data::{Data, Metadata};
|
||||||
|
pub use self::thumbnail::Dim;
|
||||||
use crate::{client, globals, sending, Dep};
|
use crate::{client, globals, sending, Dep};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -78,7 +79,7 @@ impl Service {
|
||||||
// Width, Height = 0 if it's not a thumbnail
|
// Width, Height = 0 if it's not a thumbnail
|
||||||
let key = self
|
let key = self
|
||||||
.db
|
.db
|
||||||
.create_file_metadata(mxc, user, 0, 0, content_disposition, content_type)?;
|
.create_file_metadata(mxc, user, &Dim::default(), content_disposition, content_type)?;
|
||||||
|
|
||||||
//TODO: Dangling metadata in database if creation fails
|
//TODO: Dangling metadata in database if creation fails
|
||||||
let mut f = self.create_media_file(&key).await?;
|
let mut f = self.create_media_file(&key).await?;
|
||||||
|
@ -141,7 +142,7 @@ impl Service {
|
||||||
content_disposition,
|
content_disposition,
|
||||||
content_type,
|
content_type,
|
||||||
key,
|
key,
|
||||||
}) = self.db.search_file_metadata(mxc, 0, 0)
|
}) = self.db.search_file_metadata(mxc, &Dim::default())
|
||||||
{
|
{
|
||||||
let mut content = Vec::new();
|
let mut content = Vec::new();
|
||||||
let path = self.get_media_file(&key);
|
let path = self.get_media_file(&key);
|
||||||
|
@ -350,7 +351,7 @@ impl Service {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_metadata(&self, mxc: &Mxc<'_>) -> Option<FileMeta> {
|
pub fn get_metadata(&self, mxc: &Mxc<'_>) -> Option<FileMeta> {
|
||||||
self.db
|
self.db
|
||||||
.search_file_metadata(mxc, 0, 0)
|
.search_file_metadata(mxc, &Dim::default())
|
||||||
.map(|metadata| FileMeta {
|
.map(|metadata| FileMeta {
|
||||||
content_disposition: metadata.content_disposition,
|
content_disposition: metadata.content_disposition,
|
||||||
content_type: metadata.content_type,
|
content_type: metadata.content_type,
|
||||||
|
|
|
@ -3,6 +3,8 @@ use std::time::Duration;
|
||||||
use conduit::{debug_warn, err, implement, utils::content_disposition::make_content_disposition, Err, Error, Result};
|
use conduit::{debug_warn, err, implement, utils::content_disposition::make_content_disposition, Err, Error, Result};
|
||||||
use ruma::{api::client::media, Mxc};
|
use ruma::{api::client::media, Mxc};
|
||||||
|
|
||||||
|
use super::Dim;
|
||||||
|
|
||||||
#[implement(super::Service)]
|
#[implement(super::Service)]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub async fn fetch_remote_thumbnail_legacy(
|
pub async fn fetch_remote_thumbnail_legacy(
|
||||||
|
@ -33,19 +35,8 @@ pub async fn fetch_remote_thumbnail_legacy(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.upload_thumbnail(
|
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
|
||||||
&mxc,
|
self.upload_thumbnail(&mxc, None, None, reponse.content_type.as_deref(), &dim, &reponse.file)
|
||||||
None,
|
|
||||||
None,
|
|
||||||
reponse.content_type.as_deref(),
|
|
||||||
body.width
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?,
|
|
||||||
body.height
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?,
|
|
||||||
&reponse.file,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(reponse)
|
Ok(reponse)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::{cmp, io::Cursor, num::Saturating as Sat};
|
use std::{cmp, io::Cursor, num::Saturating as Sat};
|
||||||
|
|
||||||
use conduit::{checked, Result};
|
use conduit::{checked, err, Result};
|
||||||
use image::{imageops::FilterType, DynamicImage};
|
use image::{imageops::FilterType, DynamicImage};
|
||||||
use ruma::{http_headers::ContentDisposition, Mxc, UserId};
|
use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
@ -10,16 +10,24 @@ use tokio::{
|
||||||
|
|
||||||
use super::{data::Metadata, FileMeta};
|
use super::{data::Metadata, FileMeta};
|
||||||
|
|
||||||
|
/// Dimension specification for a thumbnail.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Dim {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub method: Method,
|
||||||
|
}
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Uploads or replaces a file thumbnail.
|
/// Uploads or replaces a file thumbnail.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn upload_thumbnail(
|
pub async fn upload_thumbnail(
|
||||||
&self, mxc: &Mxc<'_>, user: Option<&UserId>, content_disposition: Option<&ContentDisposition>,
|
&self, mxc: &Mxc<'_>, user: Option<&UserId>, content_disposition: Option<&ContentDisposition>,
|
||||||
content_type: Option<&str>, width: u32, height: u32, file: &[u8],
|
content_type: Option<&str>, dim: &Dim, file: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let key = self
|
let key = self
|
||||||
.db
|
.db
|
||||||
.create_file_metadata(mxc, user, width, height, content_disposition, content_type)?;
|
.create_file_metadata(mxc, user, dim, content_disposition, content_type)?;
|
||||||
|
|
||||||
//TODO: Dangling metadata in database if creation fails
|
//TODO: Dangling metadata in database if creation fails
|
||||||
let mut f = self.create_media_file(&key).await?;
|
let mut f = self.create_media_file(&key).await?;
|
||||||
|
@ -42,15 +50,14 @@ impl super::Service {
|
||||||
/// For width,height <= 96 the server uses another thumbnailing algorithm
|
/// For width,height <= 96 the server uses another thumbnailing algorithm
|
||||||
/// which crops the image afterwards.
|
/// which crops the image afterwards.
|
||||||
#[tracing::instrument(skip(self), name = "thumbnail", level = "debug")]
|
#[tracing::instrument(skip(self), name = "thumbnail", level = "debug")]
|
||||||
pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result<Option<FileMeta>> {
|
pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Option<FileMeta>> {
|
||||||
// 0, 0 because that's the original file
|
// 0, 0 because that's the original file
|
||||||
let (width, height, crop) = thumbnail_properties(width, height).unwrap_or((0, 0, false));
|
let dim = dim.normalized();
|
||||||
|
|
||||||
if let Ok(metadata) = self.db.search_file_metadata(mxc, width, height) {
|
if let Ok(metadata) = self.db.search_file_metadata(mxc, &dim) {
|
||||||
self.get_thumbnail_saved(metadata).await
|
self.get_thumbnail_saved(metadata).await
|
||||||
} else if let Ok(metadata) = self.db.search_file_metadata(mxc, 0, 0) {
|
} else if let Ok(metadata) = self.db.search_file_metadata(mxc, &Dim::default()) {
|
||||||
self.get_thumbnail_generate(mxc, width, height, crop, metadata)
|
self.get_thumbnail_generate(mxc, &dim, metadata).await
|
||||||
.await
|
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -71,9 +78,7 @@ impl super::Service {
|
||||||
|
|
||||||
/// Generate a thumbnail
|
/// Generate a thumbnail
|
||||||
#[tracing::instrument(skip(self), name = "generate", level = "debug")]
|
#[tracing::instrument(skip(self), name = "generate", level = "debug")]
|
||||||
async fn get_thumbnail_generate(
|
async fn get_thumbnail_generate(&self, mxc: &Mxc<'_>, dim: &Dim, data: Metadata) -> Result<Option<FileMeta>> {
|
||||||
&self, mxc: &Mxc<'_>, width: u32, height: u32, crop: bool, 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);
|
||||||
fs::File::open(path)
|
fs::File::open(path)
|
||||||
|
@ -86,20 +91,19 @@ impl super::Service {
|
||||||
return Ok(Some(into_filemeta(data, content)));
|
return Ok(Some(into_filemeta(data, content)));
|
||||||
};
|
};
|
||||||
|
|
||||||
if width > image.width() || height > image.height() {
|
if dim.width > image.width() || dim.height > image.height() {
|
||||||
return Ok(Some(into_filemeta(data, content)));
|
return Ok(Some(into_filemeta(data, content)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut thumbnail_bytes = Vec::new();
|
let mut thumbnail_bytes = Vec::new();
|
||||||
let thumbnail = thumbnail_generate(&image, width, height, crop)?;
|
let thumbnail = thumbnail_generate(&image, dim)?;
|
||||||
thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), image::ImageFormat::Png)?;
|
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
|
// 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(
|
||||||
mxc,
|
mxc,
|
||||||
None,
|
None,
|
||||||
width,
|
dim,
|
||||||
height,
|
|
||||||
data.content_disposition.as_ref(),
|
data.content_disposition.as_ref(),
|
||||||
data.content_type.as_deref(),
|
data.content_type.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -111,23 +115,63 @@ impl super::Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn thumbnail_generate(image: &DynamicImage, width: u32, height: u32, crop: bool) -> Result<DynamicImage> {
|
fn thumbnail_generate(image: &DynamicImage, requested: &Dim) -> Result<DynamicImage> {
|
||||||
let thumbnail = if crop {
|
let thumbnail = if !requested.crop() {
|
||||||
image.resize_to_fill(width, height, FilterType::CatmullRom)
|
let Dim {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
..
|
||||||
|
} = requested.scaled(&Dim {
|
||||||
|
width: image.width(),
|
||||||
|
height: image.height(),
|
||||||
|
..Dim::default()
|
||||||
|
})?;
|
||||||
|
image.thumbnail_exact(width, height)
|
||||||
} else {
|
} else {
|
||||||
let (exact_width, exact_height) = thumbnail_dimension(image, width, height)?;
|
image.resize_to_fill(requested.width, requested.height, FilterType::CatmullRom)
|
||||||
image.thumbnail_exact(exact_width, exact_height)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(thumbnail)
|
Ok(thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn thumbnail_dimension(image: &DynamicImage, width: u32, height: u32) -> Result<(u32, u32)> {
|
fn into_filemeta(data: Metadata, content: Vec<u8>) -> FileMeta {
|
||||||
let image_width = image.width();
|
FileMeta {
|
||||||
let image_height = image.height();
|
content: Some(content),
|
||||||
|
content_type: data.content_type,
|
||||||
|
content_disposition: data.content_disposition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let width = cmp::min(width, image_width);
|
impl Dim {
|
||||||
let height = cmp::min(height, image_height);
|
/// Instantiate a Dim from Ruma integers with optional method.
|
||||||
|
pub fn from_ruma(width: UInt, height: UInt, method: Option<Method>) -> Result<Self> {
|
||||||
|
let width = width
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?;
|
||||||
|
let height = height
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?;
|
||||||
|
|
||||||
|
Ok(Self::new(width, height, method))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instantiate a Dim with optional method
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(width: u32, height: u32, method: Option<Method>) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
method: method.unwrap_or(Method::Scale),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scaled(&self, image: &Self) -> Result<Self> {
|
||||||
|
let image_width = image.width;
|
||||||
|
let image_height = image.height;
|
||||||
|
|
||||||
|
let width = cmp::min(self.width, image_width);
|
||||||
|
let height = cmp::min(self.height, image_height);
|
||||||
|
|
||||||
let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width);
|
let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width);
|
||||||
|
|
||||||
|
@ -145,26 +189,41 @@ fn thumbnail_dimension(image: &DynamicImage, width: u32, height: u32) -> Result<
|
||||||
height
|
height
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((x, y))
|
Ok(Self {
|
||||||
|
width: x,
|
||||||
|
height: y,
|
||||||
|
method: Method::Scale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns width, height of the thumbnail and whether it should be cropped.
|
||||||
|
/// Returns None when the server should send the original file.
|
||||||
|
/// Ignores the input Method.
|
||||||
|
#[must_use]
|
||||||
|
pub fn normalized(&self) -> Self {
|
||||||
|
match (self.width, self.height) {
|
||||||
|
(0..=32, 0..=32) => Self::new(32, 32, Some(Method::Crop)),
|
||||||
|
(0..=96, 0..=96) => Self::new(96, 96, Some(Method::Crop)),
|
||||||
|
(0..=320, 0..=240) => Self::new(320, 240, Some(Method::Scale)),
|
||||||
|
(0..=640, 0..=480) => Self::new(640, 480, Some(Method::Scale)),
|
||||||
|
(0..=800, 0..=600) => Self::new(800, 600, Some(Method::Scale)),
|
||||||
|
_ => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the method is Crop.
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn crop(&self) -> bool { self.method == Method::Crop }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns width, height of the thumbnail and whether it should be cropped.
|
impl Default for Dim {
|
||||||
/// Returns None when the server should send the original file.
|
#[inline]
|
||||||
fn thumbnail_properties(width: u32, height: u32) -> Option<(u32, u32, bool)> {
|
fn default() -> Self {
|
||||||
match (width, height) {
|
Self {
|
||||||
(0..=32, 0..=32) => Some((32, 32, true)),
|
width: 0,
|
||||||
(0..=96, 0..=96) => Some((96, 96, true)),
|
height: 0,
|
||||||
(0..=320, 0..=240) => Some((320, 240, false)),
|
method: Method::Scale,
|
||||||
(0..=640, 0..=480) => Some((640, 480, false)),
|
}
|
||||||
(0..=800, 0..=600) => Some((800, 600, false)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_filemeta(data: Metadata, content: Vec<u8>) -> FileMeta {
|
|
||||||
FileMeta {
|
|
||||||
content: Some(content),
|
|
||||||
content_type: data.content_type,
|
|
||||||
content_disposition: data.content_disposition,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue