refactor for structured Mxc type
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
54e6a41404
commit
152ae705a0
10 changed files with 133 additions and 120 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -2958,7 +2958,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "git+https://github.com/girlbossceo/ruwuma?rev=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
"js_int",
|
"js_int",
|
||||||
|
@ -2980,7 +2980,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -2992,7 +2992,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
|
@ -3015,7 +3015,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
@ -3045,7 +3045,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap 2.4.0",
|
"indexmap 2.4.0",
|
||||||
|
@ -3069,7 +3069,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
|
@ -3087,7 +3087,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -3096,7 +3096,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -3106,7 +3106,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
|
@ -3121,7 +3121,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
@ -3133,7 +3133,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
@ -3146,7 +3146,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
@ -3162,7 +3162,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=d23a8412bd8f875cf81bbd7e20cefa03263fcd0e#d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
"js_int",
|
"js_int",
|
||||||
|
|
|
@ -311,7 +311,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 = "d23a8412bd8f875cf81bbd7e20cefa03263fcd0e"
|
rev = "25fbd64b968c5d5088c07750aaa4873e072831b0"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
@ -15,7 +15,10 @@ pub(super) async fn delete(
|
||||||
|
|
||||||
if let Some(mxc) = mxc {
|
if let Some(mxc) = mxc {
|
||||||
debug!("Got MXC URL: {mxc}");
|
debug!("Got MXC URL: {mxc}");
|
||||||
self.services.media.delete(mxc.as_ref()).await?;
|
self.services
|
||||||
|
.media
|
||||||
|
.delete(&mxc.as_str().try_into()?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return Ok(RoomMessageEventContent::text_plain(
|
return Ok(RoomMessageEventContent::text_plain(
|
||||||
"Deleted the MXC from our database and on our filesystem.",
|
"Deleted the MXC from our database and on our filesystem.",
|
||||||
|
@ -123,7 +126,10 @@ pub(super) async fn delete(
|
||||||
}
|
}
|
||||||
|
|
||||||
for mxc_url in mxc_urls {
|
for mxc_url in mxc_urls {
|
||||||
self.services.media.delete(&mxc_url).await?;
|
self.services
|
||||||
|
.media
|
||||||
|
.delete(&mxc_url.as_str().try_into()?)
|
||||||
|
.await?;
|
||||||
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
|
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +163,7 @@ pub(super) async fn delete_list(&self) -> Result<RoomMessageEventContent> {
|
||||||
|
|
||||||
for mxc in mxc_list {
|
for mxc in mxc_list {
|
||||||
debug!("Deleting MXC {mxc} in bulk");
|
debug!("Deleting MXC {mxc} in bulk");
|
||||||
self.services.media.delete(mxc).await?;
|
self.services.media.delete(&mxc.try_into()?).await?;
|
||||||
mxc_deletion_count = mxc_deletion_count
|
mxc_deletion_count = mxc_deletion_count
|
||||||
.checked_add(1)
|
.checked_add(1)
|
||||||
.expect("mxc_deletion_count should not get this high");
|
.expect("mxc_deletion_count should not get this high");
|
||||||
|
|
|
@ -7,8 +7,12 @@ use conduit::{
|
||||||
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
|
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
|
||||||
Err, Result,
|
Err, Result,
|
||||||
};
|
};
|
||||||
use ruma::api::client::media::{
|
use ruma::{
|
||||||
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config, get_media_preview,
|
api::client::media::{
|
||||||
|
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||||
|
get_media_preview,
|
||||||
|
},
|
||||||
|
Mxc,
|
||||||
};
|
};
|
||||||
use service::media::{FileMeta, MXC_LENGTH};
|
use service::media::{FileMeta, MXC_LENGTH};
|
||||||
|
|
||||||
|
@ -106,16 +110,17 @@ pub(crate) async fn create_content_route(
|
||||||
body: Ruma<create_content::v3::Request>,
|
body: Ruma<create_content::v3::Request>,
|
||||||
) -> Result<create_content::v3::Response> {
|
) -> Result<create_content::v3::Response> {
|
||||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||||
|
|
||||||
let mxc = format!("mxc://{}/{}", services.globals.server_name(), utils::random_string(MXC_LENGTH));
|
|
||||||
|
|
||||||
let content_disposition = make_content_disposition(None, body.content_type.as_deref(), body.filename.as_deref());
|
let content_disposition = make_content_disposition(None, body.content_type.as_deref(), body.filename.as_deref());
|
||||||
|
let mxc = Mxc {
|
||||||
|
server_name: services.globals.server_name(),
|
||||||
|
media_id: &utils::random_string(MXC_LENGTH),
|
||||||
|
};
|
||||||
|
|
||||||
services
|
services
|
||||||
.media
|
.media
|
||||||
.create(
|
.create(
|
||||||
Some(sender_user.clone()),
|
|
||||||
&mxc,
|
&mxc,
|
||||||
|
Some(sender_user),
|
||||||
Some(&content_disposition),
|
Some(&content_disposition),
|
||||||
body.content_type.as_deref(),
|
body.content_type.as_deref(),
|
||||||
&body.file,
|
&body.file,
|
||||||
|
@ -123,7 +128,7 @@ pub(crate) async fn create_content_route(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(create_content::v3::Response {
|
Ok(create_content::v3::Response {
|
||||||
content_uri: mxc.into(),
|
content_uri: mxc.to_string().into(),
|
||||||
blurhash: None,
|
blurhash: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -161,7 +166,10 @@ pub(crate) async fn get_content_route(
|
||||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_content::v3::Request>,
|
body: Ruma<get_content::v3::Request>,
|
||||||
) -> Result<get_content::v3::Response> {
|
) -> Result<get_content::v3::Response> {
|
||||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
let mxc = Mxc {
|
||||||
|
server_name: &body.server_name,
|
||||||
|
media_id: &body.media_id,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
content,
|
content,
|
||||||
|
@ -181,13 +189,7 @@ pub(crate) async fn get_content_route(
|
||||||
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
||||||
let response = services
|
let response = services
|
||||||
.media
|
.media
|
||||||
.fetch_remote_content(
|
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
|
||||||
&mxc,
|
|
||||||
&body.server_name,
|
|
||||||
body.media_id.clone(),
|
|
||||||
body.allow_redirect,
|
|
||||||
body.timeout_ms,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
||||||
|
|
||||||
|
@ -241,7 +243,10 @@ pub(crate) async fn get_content_as_filename_route(
|
||||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_content_as_filename::v3::Request>,
|
body: Ruma<get_content_as_filename::v3::Request>,
|
||||||
) -> Result<get_content_as_filename::v3::Response> {
|
) -> Result<get_content_as_filename::v3::Response> {
|
||||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
let mxc = Mxc {
|
||||||
|
server_name: &body.server_name,
|
||||||
|
media_id: &body.media_id,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
content,
|
content,
|
||||||
|
@ -262,13 +267,7 @@ pub(crate) async fn get_content_as_filename_route(
|
||||||
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
||||||
let response = services
|
let response = services
|
||||||
.media
|
.media
|
||||||
.fetch_remote_content(
|
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
|
||||||
&mxc,
|
|
||||||
&body.server_name,
|
|
||||||
body.media_id.clone(),
|
|
||||||
body.allow_redirect,
|
|
||||||
body.timeout_ms,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
||||||
|
|
||||||
|
@ -322,7 +321,10 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||||
) -> Result<get_content_thumbnail::v3::Response> {
|
) -> Result<get_content_thumbnail::v3::Response> {
|
||||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
let mxc = Mxc {
|
||||||
|
server_name: &body.server_name,
|
||||||
|
media_id: &body.media_id,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(FileMeta {
|
if let Some(FileMeta {
|
||||||
content,
|
content,
|
||||||
|
@ -353,7 +355,7 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||||
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
|
||||||
let response = services
|
let response = services
|
||||||
.media
|
.media
|
||||||
.fetch_remote_thumbnail(&mxc, &body)
|
.fetch_remote_thumbnail_legacy(&body)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
.map_err(|e| err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}")))))?;
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,8 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IntoHttp(#[from] ruma::api::error::IntoHttpError),
|
IntoHttp(#[from] ruma::api::error::IntoHttpError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
Mxc(#[from] ruma::MxcUriError),
|
||||||
|
#[error(transparent)]
|
||||||
Mxid(#[from] ruma::IdParseError),
|
Mxid(#[from] ruma::IdParseError),
|
||||||
#[error("from {0}: {1}")]
|
#[error("from {0}: {1}")]
|
||||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use conduit::{debug, debug_info, utils::string_from_bytes, Error, Result};
|
use conduit::{
|
||||||
|
debug, debug_info, trace,
|
||||||
|
utils::{str_from_bytes, string_from_bytes},
|
||||||
|
Err, Error, Result,
|
||||||
|
};
|
||||||
use database::{Database, Map};
|
use database::{Database, Map};
|
||||||
use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition};
|
use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition, Mxc, UserId};
|
||||||
|
|
||||||
use super::preview::UrlPreviewData;
|
use super::preview::UrlPreviewData;
|
||||||
|
|
||||||
|
@ -29,10 +33,12 @@ impl Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_file_metadata(
|
pub(super) fn create_file_metadata(
|
||||||
&self, sender_user: Option<&str>, mxc: &str, width: u32, height: u32,
|
&self, mxc: &Mxc<'_>, user: Option<&UserId>, width: u32, height: u32,
|
||||||
content_disposition: Option<&ContentDisposition>, content_type: Option<&str>,
|
content_disposition: Option<&ContentDisposition>, content_type: Option<&str>,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let mut key = mxc.as_bytes().to_vec();
|
let mut key: Vec<u8> = Vec::new();
|
||||||
|
key.extend_from_slice(mxc.server_name.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(&width.to_be_bytes());
|
||||||
key.extend_from_slice(&height.to_be_bytes());
|
key.extend_from_slice(&height.to_be_bytes());
|
||||||
|
@ -53,8 +59,10 @@ impl Data {
|
||||||
|
|
||||||
self.mediaid_file.insert(&key, &[])?;
|
self.mediaid_file.insert(&key, &[])?;
|
||||||
|
|
||||||
if let Some(user) = sender_user {
|
if let Some(user) = user {
|
||||||
let key = mxc.as_bytes().to_vec();
|
let mut key: Vec<u8> = Vec::new();
|
||||||
|
key.extend_from_slice(mxc.server_name.as_bytes());
|
||||||
|
key.extend_from_slice(mxc.media_id.as_bytes());
|
||||||
let user = user.as_bytes().to_vec();
|
let user = user.as_bytes().to_vec();
|
||||||
self.mediaid_user.insert(&key, &user)?;
|
self.mediaid_user.insert(&key, &user)?;
|
||||||
}
|
}
|
||||||
|
@ -62,22 +70,23 @@ impl Data {
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn delete_file_mxc(&self, mxc: &str) -> Result<()> {
|
pub(super) fn delete_file_mxc(&self, mxc: &Mxc<'_>) -> Result<()> {
|
||||||
debug!("MXC URI: {:?}", mxc);
|
debug!("MXC URI: {mxc}");
|
||||||
|
|
||||||
let mut prefix = mxc.as_bytes().to_vec();
|
let mut prefix: Vec<u8> = Vec::new();
|
||||||
|
prefix.extend_from_slice(mxc.server_name.as_bytes());
|
||||||
|
prefix.extend_from_slice(mxc.media_id.as_bytes());
|
||||||
prefix.push(0xFF);
|
prefix.push(0xFF);
|
||||||
|
|
||||||
debug!("MXC db prefix: {prefix:?}");
|
trace!("MXC db prefix: {prefix:?}");
|
||||||
|
for (key, _) in self.mediaid_file.scan_prefix(prefix.clone()) {
|
||||||
for (key, _) in self.mediaid_file.scan_prefix(prefix) {
|
|
||||||
debug!("Deleting key: {:?}", key);
|
debug!("Deleting key: {:?}", key);
|
||||||
self.mediaid_file.remove(&key)?;
|
self.mediaid_file.remove(&key)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, value) in self.mediaid_user.scan_prefix(mxc.as_bytes().to_vec()) {
|
for (key, value) in self.mediaid_user.scan_prefix(prefix.clone()) {
|
||||||
if key == mxc.as_bytes().to_vec() {
|
if key.starts_with(&prefix) {
|
||||||
let user = string_from_bytes(&value).unwrap_or_default();
|
let user = str_from_bytes(&value).unwrap_or_default();
|
||||||
|
|
||||||
debug_info!("Deleting key \"{key:?}\" which was uploaded by user {user}");
|
debug_info!("Deleting key \"{key:?}\" which was uploaded by user {user}");
|
||||||
self.mediaid_user.remove(&key)?;
|
self.mediaid_user.remove(&key)?;
|
||||||
|
@ -88,10 +97,12 @@ impl Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Searches for all files with the given MXC
|
/// Searches for all files with the given MXC
|
||||||
pub(super) fn search_mxc_metadata_prefix(&self, mxc: &str) -> Result<Vec<Vec<u8>>> {
|
pub(super) fn search_mxc_metadata_prefix(&self, mxc: &Mxc<'_>) -> Result<Vec<Vec<u8>>> {
|
||||||
debug!("MXC URI: {:?}", mxc);
|
debug!("MXC URI: {mxc}");
|
||||||
|
|
||||||
let mut prefix = mxc.as_bytes().to_vec();
|
let mut prefix: Vec<u8> = Vec::new();
|
||||||
|
prefix.extend_from_slice(mxc.server_name.as_bytes());
|
||||||
|
prefix.extend_from_slice(mxc.media_id.as_bytes());
|
||||||
prefix.push(0xFF);
|
prefix.push(0xFF);
|
||||||
|
|
||||||
let keys: Vec<Vec<u8>> = self
|
let keys: Vec<Vec<u8>> = self
|
||||||
|
@ -101,18 +112,18 @@ impl Data {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
return Err(Error::bad_database(
|
return Err!(Database("Failed to find any keys in database for `{mxc}`",));
|
||||||
"Failed to find any keys in database with the provided MXC.",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Got the following keys: {:?}", keys);
|
debug!("Got the following keys: {keys:?}");
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn search_file_metadata(&self, mxc: &str, width: u32, height: u32) -> Result<Metadata> {
|
pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result<Metadata> {
|
||||||
let mut prefix = mxc.as_bytes().to_vec();
|
let mut prefix: Vec<u8> = Vec::new();
|
||||||
|
prefix.extend_from_slice(mxc.server_name.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(&width.to_be_bytes());
|
||||||
prefix.extend_from_slice(&height.to_be_bytes());
|
prefix.extend_from_slice(&height.to_be_bytes());
|
||||||
|
|
|
@ -10,7 +10,7 @@ use async_trait::async_trait;
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use conduit::{debug, debug_error, err, error, trace, utils, utils::MutexMap, Err, Result, Server};
|
use conduit::{debug, debug_error, err, error, trace, utils, utils::MutexMap, Err, Result, Server};
|
||||||
use data::{Data, Metadata};
|
use data::{Data, Metadata};
|
||||||
use ruma::{http_headers::ContentDisposition, OwnedMxcUri, OwnedUserId};
|
use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
||||||
|
@ -68,17 +68,13 @@ impl crate::Service for Service {
|
||||||
impl Service {
|
impl Service {
|
||||||
/// Uploads a file.
|
/// Uploads a file.
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
&self, sender_user: Option<OwnedUserId>, mxc: &str, content_disposition: Option<&ContentDisposition>,
|
&self, mxc: &Mxc<'_>, user: Option<&UserId>, content_disposition: Option<&ContentDisposition>,
|
||||||
content_type: Option<&str>, file: &[u8],
|
content_type: Option<&str>, file: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Width, Height = 0 if it's not a thumbnail
|
// Width, Height = 0 if it's not a thumbnail
|
||||||
let key = if let Some(user) = sender_user {
|
let key = self
|
||||||
self.db
|
.db
|
||||||
.create_file_metadata(Some(user.as_str()), mxc, 0, 0, content_disposition, content_type)?
|
.create_file_metadata(mxc, user, 0, 0, content_disposition, content_type)?;
|
||||||
} else {
|
|
||||||
self.db
|
|
||||||
.create_file_metadata(None, mxc, 0, 0, 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?;
|
||||||
|
@ -88,7 +84,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a file in the database and from the media directory via an MXC
|
/// Deletes a file in the database and from the media directory via an MXC
|
||||||
pub async fn delete(&self, mxc: &str) -> Result<()> {
|
pub async fn delete(&self, mxc: &Mxc<'_>) -> Result<()> {
|
||||||
if let Ok(keys) = self.db.search_mxc_metadata_prefix(mxc) {
|
if let Ok(keys) = self.db.search_mxc_metadata_prefix(mxc) {
|
||||||
for key in keys {
|
for key in keys {
|
||||||
trace!(?mxc, ?key, "Deleting from filesystem");
|
trace!(?mxc, ?key, "Deleting from filesystem");
|
||||||
|
@ -111,7 +107,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a file.
|
/// Downloads a file.
|
||||||
pub async fn get(&self, mxc: &str) -> Result<Option<FileMeta>> {
|
pub async fn get(&self, mxc: &Mxc<'_>) -> Result<Option<FileMeta>> {
|
||||||
if let Ok(Metadata {
|
if let Ok(Metadata {
|
||||||
content_disposition,
|
content_disposition,
|
||||||
content_type,
|
content_type,
|
||||||
|
@ -213,6 +209,7 @@ impl Service {
|
||||||
debug!("Deleting media now in the past {user_duration:?}.");
|
debug!("Deleting media now in the past {user_duration:?}.");
|
||||||
let mut deletion_count: usize = 0;
|
let mut deletion_count: usize = 0;
|
||||||
for mxc in remote_mxcs {
|
for mxc in remote_mxcs {
|
||||||
|
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
|
||||||
debug!("Deleting MXC {mxc} from database and filesystem");
|
debug!("Deleting MXC {mxc} from database and filesystem");
|
||||||
self.delete(&mxc).await?;
|
self.delete(&mxc).await?;
|
||||||
deletion_count = deletion_count.saturating_add(1);
|
deletion_count = deletion_count.saturating_add(1);
|
||||||
|
|
|
@ -4,6 +4,7 @@ use conduit::{debug, utils, warn, Err, Result};
|
||||||
use conduit_core::implement;
|
use conduit_core::implement;
|
||||||
use image::ImageReader as ImgReader;
|
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 webpage::HTML;
|
||||||
|
@ -44,13 +45,12 @@ pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<
|
||||||
pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
|
pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
|
||||||
let client = &self.services.client.url_preview;
|
let client = &self.services.client.url_preview;
|
||||||
let image = client.get(url).send().await?.bytes().await?;
|
let image = client.get(url).send().await?.bytes().await?;
|
||||||
let mxc = format!(
|
let mxc = Mxc {
|
||||||
"mxc://{}/{}",
|
server_name: self.services.globals.server_name(),
|
||||||
self.services.globals.server_name(),
|
media_id: &utils::random_string(MXC_LENGTH),
|
||||||
utils::random_string(MXC_LENGTH)
|
};
|
||||||
);
|
|
||||||
|
|
||||||
self.create(None, &mxc, None, None, &image).await?;
|
self.create(&mxc, None, None, None, &image).await?;
|
||||||
|
|
||||||
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
|
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
|
||||||
Err(_) => (None, None),
|
Err(_) => (None, None),
|
||||||
|
@ -61,7 +61,7 @@ pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(UrlPreviewData {
|
Ok(UrlPreviewData {
|
||||||
image: Some(mxc),
|
image: Some(mxc.to_string()),
|
||||||
image_size: Some(image.len()),
|
image_size: Some(image.len()),
|
||||||
image_width: width,
|
image_width: width,
|
||||||
image_height: height,
|
image_height: height,
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
use std::time::Duration;
|
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::{
|
use ruma::{api::client::media, Mxc};
|
||||||
api::client::media::{get_content, get_content_thumbnail},
|
|
||||||
ServerName,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[implement(super::Service)]
|
#[implement(super::Service)]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub async fn fetch_remote_thumbnail(
|
pub async fn fetch_remote_thumbnail_legacy(
|
||||||
&self, mxc: &str, body: &get_content_thumbnail::v3::Request,
|
&self, body: &media::get_content_thumbnail::v3::Request,
|
||||||
) -> Result<get_content_thumbnail::v3::Response> {
|
) -> Result<media::get_content_thumbnail::v3::Response> {
|
||||||
let server_name = &body.server_name;
|
let mxc = Mxc {
|
||||||
self.check_fetch_authorized(mxc, server_name)?;
|
server_name: &body.server_name,
|
||||||
|
media_id: &body.media_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.check_fetch_authorized(&mxc)?;
|
||||||
let reponse = self
|
let reponse = self
|
||||||
.services
|
.services
|
||||||
.sending
|
.sending
|
||||||
.send_federation_request(
|
.send_federation_request(
|
||||||
server_name,
|
mxc.server_name,
|
||||||
get_content_thumbnail::v3::Request {
|
media::get_content_thumbnail::v3::Request {
|
||||||
allow_remote: body.allow_remote,
|
allow_remote: body.allow_remote,
|
||||||
height: body.height,
|
height: body.height,
|
||||||
width: body.width,
|
width: body.width,
|
||||||
|
@ -34,8 +34,8 @@ pub async fn fetch_remote_thumbnail(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.upload_thumbnail(
|
self.upload_thumbnail(
|
||||||
|
&mxc,
|
||||||
None,
|
None,
|
||||||
mxc,
|
|
||||||
None,
|
None,
|
||||||
reponse.content_type.as_deref(),
|
reponse.content_type.as_deref(),
|
||||||
body.width
|
body.width
|
||||||
|
@ -53,20 +53,19 @@ pub async fn fetch_remote_thumbnail(
|
||||||
|
|
||||||
#[implement(super::Service)]
|
#[implement(super::Service)]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub async fn fetch_remote_content(
|
pub async fn fetch_remote_content_legacy(
|
||||||
&self, mxc: &str, server_name: &ServerName, media_id: String, allow_redirect: bool, timeout_ms: Duration,
|
&self, mxc: &Mxc<'_>, allow_redirect: bool, timeout_ms: Duration,
|
||||||
) -> Result<get_content::v3::Response, Error> {
|
) -> Result<media::get_content::v3::Response, Error> {
|
||||||
self.check_fetch_authorized(mxc, server_name)?;
|
self.check_fetch_authorized(mxc)?;
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.services
|
.services
|
||||||
.sending
|
.sending
|
||||||
.send_federation_request(
|
.send_federation_request(
|
||||||
server_name,
|
mxc.server_name,
|
||||||
get_content::v3::Request {
|
media::get_content::v3::Request {
|
||||||
allow_remote: true,
|
allow_remote: true,
|
||||||
server_name: server_name.to_owned(),
|
server_name: mxc.server_name.into(),
|
||||||
media_id,
|
media_id: mxc.media_id.into(),
|
||||||
timeout_ms,
|
timeout_ms,
|
||||||
allow_redirect,
|
allow_redirect,
|
||||||
},
|
},
|
||||||
|
@ -77,8 +76,8 @@ pub async fn fetch_remote_content(
|
||||||
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
|
make_content_disposition(response.content_disposition.as_ref(), response.content_type.as_deref(), None);
|
||||||
|
|
||||||
self.create(
|
self.create(
|
||||||
None,
|
|
||||||
mxc,
|
mxc,
|
||||||
|
None,
|
||||||
Some(&content_disposition),
|
Some(&content_disposition),
|
||||||
response.content_type.as_deref(),
|
response.content_type.as_deref(),
|
||||||
&response.file,
|
&response.file,
|
||||||
|
@ -89,14 +88,14 @@ pub async fn fetch_remote_content(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[implement(super::Service)]
|
#[implement(super::Service)]
|
||||||
fn check_fetch_authorized(&self, mxc: &str, server_name: &ServerName) -> Result<()> {
|
fn check_fetch_authorized(&self, mxc: &Mxc<'_>) -> Result<()> {
|
||||||
if self
|
if self
|
||||||
.services
|
.services
|
||||||
.server
|
.server
|
||||||
.config
|
.config
|
||||||
.prevent_media_downloads_from
|
.prevent_media_downloads_from
|
||||||
.iter()
|
.iter()
|
||||||
.any(|entry| entry == server_name)
|
.any(|entry| entry == mxc.server_name)
|
||||||
{
|
{
|
||||||
// we'll lie to the client and say the blocked server's media was not found and
|
// we'll lie to the client and say the blocked server's media was not found and
|
||||||
// log. the client has no way of telling anyways so this is a security bonus.
|
// log. the client has no way of telling anyways so this is a security bonus.
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{cmp, io::Cursor, num::Saturating as Sat};
|
||||||
|
|
||||||
use conduit::{checked, Result};
|
use conduit::{checked, Result};
|
||||||
use image::{imageops::FilterType, DynamicImage};
|
use image::{imageops::FilterType, DynamicImage};
|
||||||
use ruma::{http_headers::ContentDisposition, OwnedUserId};
|
use ruma::{http_headers::ContentDisposition, Mxc, UserId};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
@ -14,16 +14,12 @@ 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, sender_user: Option<OwnedUserId>, mxc: &str, 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>, width: u32, height: u32, file: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let key = if let Some(user) = sender_user {
|
let key = self
|
||||||
self.db
|
.db
|
||||||
.create_file_metadata(Some(user.as_str()), mxc, width, height, content_disposition, content_type)?
|
.create_file_metadata(mxc, user, width, height, content_disposition, content_type)?;
|
||||||
} else {
|
|
||||||
self.db
|
|
||||||
.create_file_metadata(None, mxc, width, height, 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?;
|
||||||
|
@ -46,7 +42,7 @@ 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: &str, width: u32, height: u32) -> Result<Option<FileMeta>> {
|
pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> 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 (width, height, crop) = thumbnail_properties(width, height).unwrap_or((0, 0, false));
|
||||||
|
|
||||||
|
@ -76,7 +72,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: &str, width: u32, height: u32, crop: bool, data: Metadata,
|
&self, mxc: &Mxc<'_>, width: u32, height: u32, crop: bool, data: Metadata,
|
||||||
) -> Result<Option<FileMeta>> {
|
) -> 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);
|
||||||
|
@ -100,8 +96,8 @@ impl super::Service {
|
||||||
|
|
||||||
// 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(
|
||||||
None,
|
|
||||||
mxc,
|
mxc,
|
||||||
|
None,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data.content_disposition.as_ref(),
|
data.content_disposition.as_ref(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue