Refactor admin media

This commit is contained in:
dasha_uwu
2026-01-26 11:38:37 +05:00
parent 7bf87cfb33
commit b0bdf2d8d9
2 changed files with 106 additions and 173 deletions

View File

@@ -1,143 +1,88 @@
use std::time::Duration; use std::time::Duration;
use ruma::{Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName}; use ruma::{CanonicalJsonValue, Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName};
use tuwunel_core::{ use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, error, info, trace, Err, Result, debug, err, error, info, trace,
utils::time::parse_timepoint_ago, warn, utils::{math::Expected, time::parse_timepoint_ago},
warn,
}; };
use tuwunel_service::media::Dim; use tuwunel_service::media::Dim;
use crate::{admin_command, utils::parse_local_user_id}; use crate::{admin_command, utils::parse_local_user_id};
#[admin_command] #[admin_command]
pub(super) async fn delete( pub(super) async fn delete(&self, mxc: OwnedMxcUri) -> Result {
&self,
mxc: Option<OwnedMxcUri>,
event_id: Option<OwnedEventId>,
) -> Result {
if event_id.is_some() && mxc.is_some() {
return Err!("Please specify either an MXC or an event ID, not both.",);
}
if let Some(mxc) = mxc {
trace!("Got MXC URL: {mxc}");
self.services self.services
.media .media
.delete(&mxc.as_str().try_into()?) .delete(&mxc.as_str().try_into()?)
.await?; .await?;
return Err!("Deleted the MXC from our database and on our filesystem.",); Err!("Deleted the MXC from our database and on our filesystem.")
} }
if let Some(event_id) = event_id { #[admin_command]
trace!("Got event ID to delete media from: {event_id}"); pub(super) async fn delete_by_event(&self, event_id: OwnedEventId) -> Result {
let mut mxc_urls = Vec::with_capacity(3);
let mut mxc_urls = Vec::with_capacity(4);
// parsing the PDU for any MXC URLs begins here // parsing the PDU for any MXC URLs begins here
match self let event_json = self
.services .services
.timeline .timeline
.get_pdu_json(&event_id) .get_pdu_json(&event_id)
.await .await
{ .map_err(|_| err!("Event ID does not exist or is not known to us."))?;
| Ok(event_json) => {
if let Some(content_key) = event_json.get("content") { let content = event_json
debug!("Event ID has \"content\"."); .get("content")
let content_obj = content_key.as_object(); .and_then(CanonicalJsonValue::as_object)
.ok_or_else(|| {
err!(
"Event ID does not have a \"content\" key, this is not a message or an event \
type that contains media.",
)
})?;
if let Some(content) = content_obj {
// 1. attempts to parse the "url" key // 1. attempts to parse the "url" key
debug!("Attempting to go into \"url\" key for main media file"); debug!("Attempting to go into \"url\" key for main media file");
if let Some(url) = content.get("url") { if let Some(url) = content
.get("url")
.and_then(CanonicalJsonValue::as_str)
{
debug!("Got a URL in the event ID {event_id}: {url}"); debug!("Got a URL in the event ID {event_id}: {url}");
if url.to_string().starts_with("\"mxc://") { mxc_urls.push(url.to_owned());
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else { } else {
info!( debug!("No main media found.");
"Found a URL in the event ID {event_id} but did not start \
with mxc://, ignoring"
);
}
} }
// 2. attempts to parse the "info" key // 2. attempts to parse the "info" key
debug!("Attempting to go into \"info\" key for thumbnails"); debug!("Attempting to go into \"info\" key for thumbnails");
if let Some(info_key) = content.get("info") { if let Some(thumbnail_url) = content
debug!("Event ID has \"info\"."); .get("info")
let info_obj = info_key.as_object(); .and_then(CanonicalJsonValue::as_object)
.and_then(|info| info.get("thumbnail_url"))
if let Some(info) = info_obj { .and_then(CanonicalJsonValue::as_str)
if let Some(thumbnail_url) = info.get("thumbnail_url") { {
debug!("Found a thumbnail_url in info key: {thumbnail_url}"); debug!("Found a thumbnail_url in info key: {thumbnail_url}");
if thumbnail_url.to_string().starts_with("\"mxc://") { mxc_urls.push(thumbnail_url.to_owned());
debug!(
"Pushing thumbnail URL {thumbnail_url} to list of \
MXCs to delete"
);
let final_thumbnail_url =
thumbnail_url.to_string().replace('"', "");
mxc_urls.push(final_thumbnail_url);
} else { } else {
info!( debug!("No thumbnails found.");
"Found a thumbnail URL in the event ID {event_id} \
but did not start with mxc://, ignoring"
);
}
} else {
info!(
"No \"thumbnail_url\" key in \"info\" key, assuming no \
thumbnails."
);
}
}
} }
// 3. attempts to parse the "file" key // 3. attempts to parse the "file" key
debug!("Attempting to go into \"file\" key"); debug!("Attempting to go into \"file\" key");
if let Some(file_key) = content.get("file") { if let Some(url) = content
debug!("Event ID has \"file\"."); .get("file")
let file_obj = file_key.as_object(); .and_then(CanonicalJsonValue::as_object)
.and_then(|file| file.get("url"))
if let Some(file) = file_obj { .and_then(CanonicalJsonValue::as_str)
if let Some(url) = file.get("url") { {
debug!("Found url in file key: {url}"); debug!("Found url in file key: {url}");
if url.to_string().starts_with("\"mxc://") { mxc_urls.push(url.to_owned());
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else { } else {
warn!( debug!("No \"url\" key in \"file\" key.");
"Found a URL in the event ID {event_id} but did not \
start with mxc://, ignoring"
);
}
} else {
error!("No \"url\" key in \"file\" key.");
}
}
}
} else {
return Err!(
"Event ID does not have a \"content\" key or failed parsing the \
event ID JSON.",
);
}
} else {
return Err!(
"Event ID does not have a \"content\" key, this is not a message or an \
event type that contains media.",
);
}
},
| _ => {
return Err!("Event ID does not exist or is not known to us.",);
},
} }
if mxc_urls.is_empty() { if mxc_urls.is_empty() {
@@ -147,35 +92,29 @@ pub(super) async fn delete(
let mut mxc_deletion_count: usize = 0; let mut mxc_deletion_count: usize = 0;
for mxc_url in mxc_urls { for mxc_url in mxc_urls {
match self if !mxc_url.starts_with("mxc://") {
.services warn!("Ignoring non-mxc url {mxc_url}");
.media continue;
.delete(&mxc_url.as_str().try_into()?) }
.await
{ let mxc: Mxc<'_> = mxc_url.as_str().try_into()?;
match self.services.media.delete(&mxc).await {
| Ok(()) => { | Ok(()) => {
debug_info!("Successfully deleted {mxc_url} from filesystem and database"); info!("Successfully deleted {mxc_url} from filesystem and database");
mxc_deletion_count = mxc_deletion_count.saturating_add(1); mxc_deletion_count = mxc_deletion_count.saturating_add(1);
}, },
| Err(e) => { | Err(e) => {
debug_warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}"); warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}");
continue;
}, },
} }
} }
return self self.write_str(&format!(
.write_str(&format!( "Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from \
"Deleted {mxc_deletion_count} total MXCs from our database and the filesystem \ event ID {event_id}."
from event ID {event_id}."
)) ))
.await; .await
}
Err!(
"Please specify either an MXC using --mxc or an event ID using --event-id of the \
message containing an image. See --help for details."
)
} }
#[admin_command] #[admin_command]
@@ -192,12 +131,12 @@ pub(super) async fn delete_list(&self) -> Result {
let mxc_list = self let mxc_list = self
.body .body
.to_vec() .to_vec()
.drain(1..self.body.len().checked_sub(1).unwrap()) .drain(1..self.body.len().expected_sub(1))
.filter_map(|mxc_s| { .filter_map(|mxc_s| {
mxc_s mxc_s
.try_into() .try_into()
.inspect_err(|e| { .inspect_err(|e| {
debug_warn!("Failed to parse user-provided MXC URI: {e}"); warn!("Failed to parse user-provided MXC URI: {e}");
failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1); failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1);
}) })
.ok() .ok()
@@ -210,11 +149,11 @@ pub(super) async fn delete_list(&self) -> Result {
trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk"); trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk");
match self.services.media.delete(mxc).await { match self.services.media.delete(mxc).await {
| Ok(()) => { | Ok(()) => {
debug_info!("Successfully deleted {mxc} from filesystem and database"); info!("Successfully deleted {mxc} from filesystem and database");
mxc_deletion_count = mxc_deletion_count.saturating_add(1); mxc_deletion_count = mxc_deletion_count.saturating_add(1);
}, },
| Err(e) => { | Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
continue; continue;
}, },
} }
@@ -238,7 +177,6 @@ pub(super) async fn delete_past_remote_media(
if before && after { if before && after {
return Err!("Please only pick one argument, --before or --after.",); return Err!("Please only pick one argument, --before or --after.",);
} }
assert!(!(before && after), "--before and --after should not be specified together");
let duration = parse_timepoint_ago(&duration)?; let duration = parse_timepoint_ago(&duration)?;
let deleted_count = self let deleted_count = self
@@ -294,7 +232,7 @@ pub(super) async fn delete_all_from_server(
for mxc in all_mxcs { for mxc in all_mxcs {
let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| { let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| {
debug_warn!( warn!(
"Failed to parse MXC {mxc} server name from database, ignoring error and \ "Failed to parse MXC {mxc} server name from database, ignoring error and \
skipping: {e}" skipping: {e}"
); );
@@ -302,13 +240,7 @@ pub(super) async fn delete_all_from_server(
continue; continue;
}; };
if mxc_server_name != server_name if mxc_server_name != server_name {
|| (self
.services
.globals
.server_is_ours(mxc_server_name)
&& !yes_i_want_to_delete_local_media)
{
trace!("skipping MXC URI {mxc}"); trace!("skipping MXC URI {mxc}");
continue; continue;
} }
@@ -320,8 +252,7 @@ pub(super) async fn delete_all_from_server(
deleted_count = deleted_count.saturating_add(1); deleted_count = deleted_count.saturating_add(1);
}, },
| Err(e) => { | Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
continue;
}, },
} }
} }

View File

@@ -11,16 +11,18 @@ use crate::admin_command_dispatch;
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub(super) enum MediaCommand { pub(super) enum MediaCommand {
/// - Deletes a single media file from our database and on the filesystem /// - Deletes a single media file from our database and on the filesystem
/// via a single MXC URL or event ID (not redacted) /// via a single MXC URL
Delete { Delete {
/// The MXC URL to delete /// The MXC URL to delete
#[arg(long)] #[arg(long)]
mxc: Option<OwnedMxcUri>, mxc: OwnedMxcUri,
},
/// - The message event ID which contains the media and thumbnail MXC /// - Deletes media files referenced by event
/// URLs DeleteByEvent {
/// - The event ID which contains the media and thumbnail MXC URLs
#[arg(long)] #[arg(long)]
event_id: Option<OwnedEventId>, event_id: OwnedEventId,
}, },
/// - Deletes a codeblock list of MXC URLs from our database and on the /// - Deletes a codeblock list of MXC URLs from our database and on the