track media uploads by user

Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
strawberry 2024-03-17 01:42:30 -04:00 committed by June
parent 19135eaa58
commit b079b94715
5 changed files with 54 additions and 13 deletions

View file

@ -138,6 +138,8 @@ pub async fn get_media_preview_v1_route(
/// - Some metadata will be saved in the database /// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory /// - Media will be saved in the media/ directory
pub async fn create_content_route(body: Ruma<create_content::v3::Request>) -> Result<create_content::v3::Response> { pub async fn create_content_route(body: Ruma<create_content::v3::Request>) -> Result<create_content::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mxc = format!( let mxc = format!(
"mxc://{}/{}", "mxc://{}/{}",
services().globals.server_name(), services().globals.server_name(),
@ -147,6 +149,7 @@ pub async fn create_content_route(body: Ruma<create_content::v3::Request>) -> Re
services() services()
.media .media
.create( .create(
Some(sender_user.clone()),
mxc.clone(), mxc.clone(),
body.filename.as_ref().map(|filename| "inline; filename=".to_owned() + filename).as_deref(), body.filename.as_ref().map(|filename| "inline; filename=".to_owned() + filename).as_deref(),
body.content_type.as_deref(), body.content_type.as_deref(),
@ -175,6 +178,8 @@ pub async fn create_content_route(body: Ruma<create_content::v3::Request>) -> Re
pub async fn create_content_v1_route( pub async fn create_content_v1_route(
body: Ruma<create_content::v3::Request>, body: Ruma<create_content::v3::Request>,
) -> Result<RumaResponse<create_content::v3::Response>> { ) -> Result<RumaResponse<create_content::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mxc = format!( let mxc = format!(
"mxc://{}/{}", "mxc://{}/{}",
services().globals.server_name(), services().globals.server_name(),
@ -184,6 +189,7 @@ pub async fn create_content_v1_route(
services() services()
.media .media
.create( .create(
Some(sender_user.clone()),
mxc.clone(), mxc.clone(),
body.filename.as_ref().map(|filename| "inline; filename=".to_owned() + filename).as_deref(), body.filename.as_ref().map(|filename| "inline; filename=".to_owned() + filename).as_deref(),
body.content_type.as_deref(), body.content_type.as_deref(),
@ -231,6 +237,7 @@ pub async fn get_remote_content(
services() services()
.media .media
.create( .create(
None,
mxc.to_owned(), mxc.to_owned(),
content_response.content_disposition.as_deref(), content_response.content_disposition.as_deref(),
content_response.content_type.as_deref(), content_response.content_type.as_deref(),
@ -484,6 +491,7 @@ pub async fn get_content_thumbnail_route(
services() services()
.media .media
.upload_thumbnail( .upload_thumbnail(
None,
mxc, mxc,
None, None,
get_thumbnail_response.content_type.as_deref(), get_thumbnail_response.content_type.as_deref(),
@ -566,6 +574,7 @@ pub async fn get_content_thumbnail_v1_route(
services() services()
.media .media
.upload_thumbnail( .upload_thumbnail(
None,
mxc, mxc,
None, None,
get_thumbnail_response.content_type.as_deref(), get_thumbnail_response.content_type.as_deref(),
@ -589,7 +598,7 @@ async fn download_image(client: &reqwest::Client, url: &str) -> Result<UrlPrevie
utils::random_string(MXC_LENGTH) utils::random_string(MXC_LENGTH)
); );
services().media.create(mxc.clone(), None, None, &image).await?; services().media.create(None, mxc.clone(), 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),

View file

@ -4,12 +4,14 @@ use tracing::debug;
use crate::{ use crate::{
database::KeyValueDatabase, database::KeyValueDatabase,
service::{self, media::UrlPreviewData}, service::{self, media::UrlPreviewData},
utils, Error, Result, utils::string_from_bytes,
Error, Result,
}; };
impl service::media::Data for KeyValueDatabase { impl service::media::Data for KeyValueDatabase {
fn create_file_metadata( fn create_file_metadata(
&self, mxc: String, width: u32, height: u32, content_disposition: Option<&str>, content_type: Option<&str>, &self, sender_user: Option<&str>, mxc: String, width: u32, height: u32, content_disposition: Option<&str>,
content_type: Option<&str>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let mut key = mxc.as_bytes().to_vec(); let mut key = mxc.as_bytes().to_vec();
key.push(0xFF); key.push(0xFF);
@ -22,6 +24,12 @@ impl service::media::Data for KeyValueDatabase {
self.mediaid_file.insert(&key, &[])?; self.mediaid_file.insert(&key, &[])?;
if let Some(user) = sender_user {
let key = mxc.as_bytes().to_vec();
let user = user.as_bytes().to_vec();
self.mediaid_user.insert(&key, &user)?;
}
Ok(key) Ok(key)
} }
@ -31,13 +39,22 @@ impl service::media::Data for KeyValueDatabase {
let mut prefix = mxc.as_bytes().to_vec(); let mut prefix = mxc.as_bytes().to_vec();
prefix.push(0xFF); prefix.push(0xFF);
debug!("MXC db prefix: {:?}", prefix); debug!("MXC db prefix: {prefix:?}");
for (key, _) in self.mediaid_file.scan_prefix(prefix) { 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()) {
if key == mxc.as_bytes().to_vec() {
let user = string_from_bytes(&value).unwrap_or_default();
debug!("Deleting key \"{key:?}\" which was uploaded by user {user}");
self.mediaid_user.remove(&key)?;
}
}
Ok(()) Ok(())
} }
@ -85,7 +102,7 @@ impl service::media::Data for KeyValueDatabase {
let content_type = parts let content_type = parts
.next() .next()
.map(|bytes| { .map(|bytes| {
utils::string_from_bytes(bytes) string_from_bytes(bytes)
.map_err(|_| Error::bad_database("Content type in mediaid_file is invalid unicode.")) .map_err(|_| Error::bad_database("Content type in mediaid_file is invalid unicode."))
}) })
.transpose()?; .transpose()?;
@ -97,7 +114,7 @@ impl service::media::Data for KeyValueDatabase {
None None
} else { } else {
Some( Some(
utils::string_from_bytes(content_disposition_bytes) string_from_bytes(content_disposition_bytes)
.map_err(|_| Error::bad_database("Content Disposition in mediaid_file is invalid unicode."))?, .map_err(|_| Error::bad_database("Content Disposition in mediaid_file is invalid unicode."))?,
) )
}; };

View file

@ -157,6 +157,7 @@ pub struct KeyValueDatabase {
//pub media: media::Media, //pub media: media::Media,
pub(super) mediaid_file: Arc<dyn KvTree>, // MediaId = MXC + WidthHeight + ContentDisposition + ContentType pub(super) mediaid_file: Arc<dyn KvTree>, // MediaId = MXC + WidthHeight + ContentDisposition + ContentType
pub(super) url_previews: Arc<dyn KvTree>, pub(super) url_previews: Arc<dyn KvTree>,
pub(super) mediaid_user: Arc<dyn KvTree>,
//pub key_backups: key_backups::KeyBackups, //pub key_backups: key_backups::KeyBackups,
pub(super) backupid_algorithm: Arc<dyn KvTree>, // BackupId = UserId + Version(Count) pub(super) backupid_algorithm: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
pub(super) backupid_etag: Arc<dyn KvTree>, // BackupId = UserId + Version(Count) pub(super) backupid_etag: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
@ -365,6 +366,7 @@ impl KeyValueDatabase {
roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?, roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?,
mediaid_file: builder.open_tree("mediaid_file")?, mediaid_file: builder.open_tree("mediaid_file")?,
url_previews: builder.open_tree("url_previews")?, url_previews: builder.open_tree("url_previews")?,
mediaid_user: builder.open_tree("mediaid_user")?,
backupid_algorithm: builder.open_tree("backupid_algorithm")?, backupid_algorithm: builder.open_tree("backupid_algorithm")?,
backupid_etag: builder.open_tree("backupid_etag")?, backupid_etag: builder.open_tree("backupid_etag")?,
backupkeyid_backup: builder.open_tree("backupkeyid_backup")?, backupkeyid_backup: builder.open_tree("backupkeyid_backup")?,

View file

@ -2,7 +2,8 @@ use crate::Result;
pub trait Data: Send + Sync { pub trait Data: Send + Sync {
fn create_file_metadata( fn create_file_metadata(
&self, mxc: String, width: u32, height: u32, content_disposition: Option<&str>, content_type: Option<&str>, &self, sender_user: Option<&str>, mxc: String, width: u32, height: u32, content_disposition: Option<&str>,
content_type: Option<&str>,
) -> Result<Vec<u8>>; ) -> Result<Vec<u8>>;
fn delete_file_mxc(&self, mxc: String) -> Result<()>; fn delete_file_mxc(&self, mxc: String) -> Result<()>;

View file

@ -3,7 +3,7 @@ use std::{collections::HashMap, io::Cursor, sync::Arc, time::SystemTime};
pub(crate) use data::Data; pub(crate) use data::Data;
use image::imageops::FilterType; use image::imageops::FilterType;
use ruma::OwnedMxcUri; use ruma::{OwnedMxcUri, OwnedUserId};
use serde::Serialize; use serde::Serialize;
use tokio::{ use tokio::{
fs::{self, File}, fs::{self, File},
@ -45,10 +45,15 @@ pub struct Service {
impl Service { impl Service {
/// Uploads a file. /// Uploads a file.
pub async fn create( pub async fn create(
&self, mxc: String, content_disposition: Option<&str>, content_type: Option<&str>, file: &[u8], &self, sender_user: Option<OwnedUserId>, mxc: String, content_disposition: Option<&str>,
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 = self.db.create_file_metadata(mxc, 0, 0, content_disposition, content_type)?; let key = if let Some(user) = sender_user {
self.db.create_file_metadata(Some(user.as_str()), mxc, 0, 0, content_disposition, content_type)?
} else {
self.db.create_file_metadata(None, mxc, 0, 0, content_disposition, content_type)?
};
let path; let path;
@ -106,11 +111,17 @@ impl Service {
} }
/// Uploads or replaces a file thumbnail. /// Uploads or replaces a file thumbnail.
#[allow(clippy::too_many_arguments)]
pub async fn upload_thumbnail( pub async fn upload_thumbnail(
&self, mxc: String, content_disposition: Option<&str>, content_type: Option<&str>, width: u32, height: u32, &self, sender_user: Option<OwnedUserId>, mxc: String, content_disposition: Option<&str>,
file: &[u8], content_type: Option<&str>, width: u32, height: u32, file: &[u8],
) -> Result<()> { ) -> Result<()> {
let key = self.db.create_file_metadata(mxc, width, height, content_disposition, content_type)?; let key = if let Some(user) = sender_user {
self.db.create_file_metadata(Some(user.as_str()), mxc, width, height, content_disposition, content_type)?
} else {
self.db.create_file_metadata(None, mxc, width, height, content_disposition, content_type)?
};
let path; let path;
#[allow(clippy::unnecessary_operation)] // error[E0658]: attributes on expressions are experimental #[allow(clippy::unnecessary_operation)] // error[E0658]: attributes on expressions are experimental
@ -403,6 +414,7 @@ impl 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,
width, width,
height, height,