Use MxcData in media service

This commit is contained in:
Lambda 2024-09-01 15:35:35 +00:00
parent 391fc2e51a
commit c1ea25e1e0
6 changed files with 89 additions and 91 deletions

View file

@ -20,10 +20,8 @@ use ruma::{
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::{ use crate::{
service::media::FileMeta, service::media::{FileMeta, MxcData},
services, services, utils, Ar, Error, Ra, Result,
utils::{self, MxcData},
Ar, Error, Ra, Result,
}; };
const MXC_LENGTH: usize = 32; const MXC_LENGTH: usize = 32;
@ -159,7 +157,7 @@ pub(crate) async fn create_content_route(
services() services()
.media .media
.create( .create(
mxc.to_string(), mxc,
body.filename body.filename
.clone() .clone()
.map(|filename| ContentDisposition { .map(|filename| ContentDisposition {
@ -350,7 +348,7 @@ pub(crate) async fn get_remote_content(
services() services()
.media .media
.create( .create(
mxc.to_string(), mxc,
response.content.content_disposition.as_ref(), response.content.content_disposition.as_ref(),
response.content.content_type.clone(), response.content.content_type.clone(),
&response.content.file, &response.content.file,
@ -455,7 +453,7 @@ async fn get_content_route_ruma(
.. ..
}, },
file, file,
)) = services().media.get(mxc.to_string()).await? )) = services().media.get(mxc).await?
{ {
Ok(authenticated_media_client::get_content::v1::Response { Ok(authenticated_media_client::get_content::v1::Response {
file, file,
@ -581,7 +579,7 @@ async fn get_content_as_filename_route_ruma(
.. ..
}, },
file, file,
)) = services().media.get(mxc.to_string()).await? )) = services().media.get(mxc).await?
{ {
Ok(authenticated_media_client::get_content_as_filename::v1::Response { Ok(authenticated_media_client::get_content_as_filename::v1::Response {
file, file,
@ -842,8 +840,7 @@ async fn get_content_thumbnail_route_ruma(
.. ..
}, },
file, file,
)) = )) = services().media.get_thumbnail(mxc, width, height).await?
services().media.get_thumbnail(mxc.to_string(), width, height).await?
{ {
return Ok(make_response(file, content_type)); return Ok(make_response(file, content_type));
} }
@ -872,7 +869,7 @@ async fn get_content_thumbnail_route_ruma(
services() services()
.media .media
.upload_thumbnail( .upload_thumbnail(
mxc.to_string(), mxc,
None, None,
resp.content.content_type.clone(), resp.content.content_type.clone(),
width, width,
@ -901,10 +898,7 @@ async fn get_content_thumbnail_route_ruma(
.. ..
}, },
file, file,
)) = services() )) = services().media.get_thumbnail(mxc, width, height).await?
.media
.get_thumbnail(mxc.to_string(), width, height)
.await?
{ {
return Ok(make_response(file, content_type)); return Ok(make_response(file, content_type));
} }

View file

@ -70,9 +70,12 @@ use super::appservice_server;
use crate::{ use crate::{
api::client_server::{self, claim_keys_helper, get_keys_helper}, api::client_server::{self, claim_keys_helper, get_keys_helper},
observability::{FoundIn, Lookup, METRICS}, observability::{FoundIn, Lookup, METRICS},
service::pdu::{gen_event_id_canonical_json, PduBuilder}, service::{
media::MxcData,
pdu::{gen_event_id_canonical_json, PduBuilder},
},
services, services,
utils::{self, dbg_truncate_str, MxcData}, utils::{self, dbg_truncate_str},
Ar, Error, PduEvent, Ra, Result, Ar, Error, PduEvent, Ra, Result,
}; };
@ -2050,7 +2053,7 @@ pub(crate) async fn media_download_route(
content_type, content_type,
}, },
file, file,
)) = services().media.get(mxc.to_string()).await? )) = services().media.get(mxc).await?
else { else {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::NotYetUploaded, ErrorKind::NotYetUploaded,
@ -2097,7 +2100,7 @@ pub(crate) async fn media_thumbnail_route(
.. ..
}, },
file, file,
)) = services().media.get_thumbnail(mxc.to_string(), width, height).await? )) = services().media.get_thumbnail(mxc, width, height).await?
else { else {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::NotYetUploaded, ErrorKind::NotYetUploaded,

View file

@ -4,7 +4,7 @@ use crate::{
database::KeyValueDatabase, database::KeyValueDatabase,
service::{ service::{
self, self,
media::{FileMeta, MediaFileKey}, media::{FileMeta, MediaFileKey, MxcData},
}, },
utils, Error, Result, utils, Error, Result,
}; };
@ -12,12 +12,12 @@ use crate::{
impl service::media::Data for KeyValueDatabase { impl service::media::Data for KeyValueDatabase {
fn create_file_metadata( fn create_file_metadata(
&self, &self,
mxc: String, mxc: MxcData<'_>,
width: u32, width: u32,
height: u32, height: u32,
meta: &FileMeta, meta: &FileMeta,
) -> Result<MediaFileKey> { ) -> Result<MediaFileKey> {
let mut key = mxc.as_bytes().to_vec(); let mut key = mxc.to_string().as_bytes().to_vec();
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());
@ -45,11 +45,11 @@ impl service::media::Data for KeyValueDatabase {
fn search_file_metadata( fn search_file_metadata(
&self, &self,
mxc: String, mxc: MxcData<'_>,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<(FileMeta, MediaFileKey)> { ) -> Result<(FileMeta, MediaFileKey)> {
let mut prefix = mxc.as_bytes().to_vec(); let mut prefix = mxc.to_string().as_bytes().to_vec();
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());

View file

@ -1,14 +1,17 @@
use std::io::Cursor; use std::{fmt, io::Cursor};
use image::imageops::FilterType; use image::imageops::FilterType;
use ruma::http_headers::ContentDisposition; use ruma::{
api::client::error::ErrorKind, http_headers::ContentDisposition, MxcUri,
MxcUriError, OwnedMxcUri,
};
use tokio::{ use tokio::{
fs::File, fs::File,
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
}; };
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::{services, Result}; use crate::{services, Error, Result};
mod data; mod data;
@ -37,6 +40,57 @@ impl MediaFileKey {
} }
} }
/// Data that makes up an `mxc://` URL.
#[derive(Debug, Clone, Copy)]
pub(crate) struct MxcData<'a> {
pub(crate) server_name: &'a ruma::ServerName,
pub(crate) media_id: &'a str,
}
impl<'a> MxcData<'a> {
pub(crate) fn new(
server_name: &'a ruma::ServerName,
media_id: &'a str,
) -> Result<Self> {
if !media_id.bytes().all(|b| {
matches!(b,
b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_'
)
}) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid MXC media id",
));
}
Ok(Self {
server_name,
media_id,
})
}
}
impl fmt::Display for MxcData<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "mxc://{}/{}", self.server_name, self.media_id)
}
}
impl From<MxcData<'_>> for OwnedMxcUri {
fn from(value: MxcData<'_>) -> Self {
value.to_string().into()
}
}
impl<'a> TryFrom<&'a MxcUri> for MxcData<'a> {
type Error = MxcUriError;
fn try_from(value: &'a MxcUri) -> Result<Self, Self::Error> {
Ok(Self::new(value.server_name()?, value.media_id()?)
.expect("validated MxcUri should always be valid MxcData"))
}
}
pub(crate) struct Service { pub(crate) struct Service {
pub(crate) db: &'static dyn Data, pub(crate) db: &'static dyn Data,
} }
@ -46,7 +100,7 @@ impl Service {
#[tracing::instrument(skip(self, file))] #[tracing::instrument(skip(self, file))]
pub(crate) async fn create( pub(crate) async fn create(
&self, &self,
mxc: String, mxc: MxcData<'_>,
content_disposition: Option<&ContentDisposition>, content_disposition: Option<&ContentDisposition>,
content_type: Option<String>, content_type: Option<String>,
file: &[u8], file: &[u8],
@ -69,7 +123,7 @@ impl Service {
#[tracing::instrument(skip(self, file))] #[tracing::instrument(skip(self, file))]
pub(crate) async fn upload_thumbnail( pub(crate) async fn upload_thumbnail(
&self, &self,
mxc: String, mxc: MxcData<'_>,
content_disposition: Option<String>, content_disposition: Option<String>,
content_type: Option<String>, content_type: Option<String>,
width: u32, width: u32,
@ -93,7 +147,7 @@ impl Service {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub(crate) async fn get( pub(crate) async fn get(
&self, &self,
mxc: String, mxc: MxcData<'_>,
) -> Result<Option<(FileMeta, Vec<u8>)>> { ) -> Result<Option<(FileMeta, Vec<u8>)>> {
if let Ok((meta, key)) = self.db.search_file_metadata(mxc, 0, 0) { if let Ok((meta, key)) = self.db.search_file_metadata(mxc, 0, 0) {
let path = services().globals.get_media_file(&key); let path = services().globals.get_media_file(&key);
@ -224,7 +278,7 @@ impl Service {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub(crate) async fn get_thumbnail( pub(crate) async fn get_thumbnail(
&self, &self,
mxc: String, mxc: MxcData<'_>,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<Option<(FileMeta, Vec<u8>)>> { ) -> Result<Option<(FileMeta, Vec<u8>)>> {
@ -233,7 +287,7 @@ impl Service {
Self::thumbnail_properties(width, height).unwrap_or((0, 0, false)); Self::thumbnail_properties(width, height).unwrap_or((0, 0, false));
if let Ok((meta, key)) = if let Ok((meta, key)) =
self.db.search_file_metadata(mxc.clone(), width, height) self.db.search_file_metadata(mxc, width, height)
{ {
debug!("Using saved thumbnail"); debug!("Using saved thumbnail");
let path = services().globals.get_media_file(&key); let path = services().globals.get_media_file(&key);
@ -243,8 +297,7 @@ impl Service {
return Ok(Some((meta, file.clone()))); return Ok(Some((meta, file.clone())));
} }
let Ok((meta, key)) = self.db.search_file_metadata(mxc.clone(), 0, 0) let Ok((meta, key)) = self.db.search_file_metadata(mxc, 0, 0) else {
else {
debug!("Original image not found, can't generate thumbnail"); debug!("Original image not found, can't generate thumbnail");
return Ok(None); return Ok(None);
}; };

View file

@ -1,10 +1,10 @@
use super::{FileMeta, MediaFileKey}; use super::{FileMeta, MediaFileKey, MxcData};
use crate::Result; use crate::Result;
pub(crate) trait Data: Send + Sync { pub(crate) trait Data: Send + Sync {
fn create_file_metadata( fn create_file_metadata(
&self, &self,
mxc: String, mxc: MxcData<'_>,
width: u32, width: u32,
height: u32, height: u32,
meta: &FileMeta, meta: &FileMeta,
@ -12,7 +12,7 @@ pub(crate) trait Data: Send + Sync {
fn search_file_metadata( fn search_file_metadata(
&self, &self,
mxc: String, mxc: MxcData<'_>,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<(FileMeta, MediaFileKey)>; ) -> Result<(FileMeta, MediaFileKey)>;

View file

@ -13,11 +13,10 @@ use cmp::Ordering;
use rand::{prelude::*, rngs::OsRng}; use rand::{prelude::*, rngs::OsRng};
use ring::digest; use ring::digest;
use ruma::{ use ruma::{
api::client::error::ErrorKind, canonical_json::try_from_json_map, canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject,
CanonicalJsonError, CanonicalJsonObject, MxcUri, MxcUriError, OwnedMxcUri,
}; };
use crate::{Error, Result}; use crate::Result;
// Hopefully we have a better chat protocol in 530 years // Hopefully we have a better chat protocol in 530 years
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)] #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
@ -255,57 +254,6 @@ pub(crate) fn dbg_truncate_str(s: &str, mut max_len: usize) -> Cow<'_, str> {
} }
} }
/// Data that makes up an `mxc://` URL.
#[derive(Debug, Clone, Copy)]
pub(crate) struct MxcData<'a> {
pub(crate) server_name: &'a ruma::ServerName,
pub(crate) media_id: &'a str,
}
impl<'a> MxcData<'a> {
pub(crate) fn new(
server_name: &'a ruma::ServerName,
media_id: &'a str,
) -> Result<Self> {
if !media_id.bytes().all(|b| {
matches!(b,
b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_'
)
}) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid MXC media id",
));
}
Ok(Self {
server_name,
media_id,
})
}
}
impl fmt::Display for MxcData<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "mxc://{}/{}", self.server_name, self.media_id)
}
}
impl From<MxcData<'_>> for OwnedMxcUri {
fn from(value: MxcData<'_>) -> Self {
value.to_string().into()
}
}
impl<'a> TryFrom<&'a MxcUri> for MxcData<'a> {
type Error = MxcUriError;
fn try_from(value: &'a MxcUri) -> Result<Self, Self::Error> {
Ok(Self::new(value.server_name()?, value.media_id()?)
.expect("validated MxcUri should always be valid MxcData"))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::dbg_truncate_str; use crate::utils::dbg_truncate_str;