diff --git a/src/admin/media/commands.rs b/src/admin/media/commands.rs index 4d875635..476f048b 100644 --- a/src/admin/media/commands.rs +++ b/src/admin/media/commands.rs @@ -1,5 +1,5 @@ -use conduit::{debug, info, Result}; -use ruma::{events::room::message::RoomMessageEventContent, EventId, MxcUri}; +use conduit::{debug, info, trace, warn, Result}; +use ruma::{events::room::message::RoomMessageEventContent, EventId, Mxc, MxcUri, ServerName}; use crate::{admin_command, utils::parse_local_user_id}; @@ -201,3 +201,59 @@ pub(super) async fn delete_all_from_user(&self, username: String, force: bool) - "Deleted {deleted_count} total files.", ))) } + +#[admin_command] +pub(super) async fn delete_all_from_server( + &self, server_name: Box, force: bool, +) -> Result { + if server_name == self.services.globals.server_name() { + return Ok(RoomMessageEventContent::text_plain("This command only works for remote media.")); + } + + let Ok(all_mxcs) = self.services.media.get_all_mxcs().await else { + return Ok(RoomMessageEventContent::text_plain("Failed to get MXC URIs from our database")); + }; + + let mut deleted_count: usize = 0; + + for mxc in all_mxcs { + let mxc_server_name = match mxc.server_name() { + Ok(server_name) => server_name, + Err(e) => { + if force { + warn!("Failed to parse MXC {mxc} server name from database, ignoring error and skipping: {e}"); + continue; + } + + return Ok(RoomMessageEventContent::text_plain(format!( + "Failed to parse MXC {mxc} server name from database: {e}", + ))); + }, + }; + + if mxc_server_name != server_name || self.services.globals.server_is_ours(mxc_server_name) { + trace!("skipping MXC URI {mxc}"); + continue; + } + + let mxc: Mxc<'_> = mxc.as_str().try_into()?; + + match self.services.media.delete(&mxc).await { + Ok(()) => { + deleted_count = deleted_count.saturating_add(1); + }, + Err(e) => { + if force { + warn!("Failed to delete {mxc}, ignoring error and skipping: {e}"); + continue; + } + + return Ok(RoomMessageEventContent::text_plain(format!("Failed to delete MXC {mxc}: {e}"))); + }, + } + } + + Ok(RoomMessageEventContent::text_plain(format!( + "Deleted {deleted_count} total files.", + ))) +} diff --git a/src/admin/media/mod.rs b/src/admin/media/mod.rs index 3a1d4b11..1120a63d 100644 --- a/src/admin/media/mod.rs +++ b/src/admin/media/mod.rs @@ -2,7 +2,7 @@ mod commands; use clap::Subcommand; use conduit::Result; -use ruma::{EventId, MxcUri}; +use ruma::{EventId, MxcUri, ServerName}; use crate::admin_command_dispatch; @@ -46,4 +46,13 @@ pub(super) enum MediaCommand { #[arg(short, long)] force: bool, }, + + /// - Deletes all remote media from the specified remote server + DeleteAllFromServer { + server_name: Box, + + /// Continues deleting media if an undeletable object is found + #[arg(short, long)] + force: bool, + }, } diff --git a/src/service/media/mod.rs b/src/service/media/mod.rs index ea225ae6..f4a62666 100644 --- a/src/service/media/mod.rs +++ b/src/service/media/mod.rs @@ -159,11 +159,43 @@ impl Service { } } + /// Gets all the MXC URIs in our media database + pub async fn get_all_mxcs(&self) -> Result> { + let all_keys = self.db.get_all_media_keys(); + + let mut mxcs = Vec::with_capacity(all_keys.len()); + + for key in all_keys { + debug!("Full MXC key from database: {key:?}"); + + // we need to get the MXC URL from the first part of the key (the first 0xff / + // 255 push). this is all necessary because of conduit using magic keys for + // media + let mut parts = key.split(|&b| b == 0xFF); + let mxc = parts + .next() + .map(|bytes| { + utils::string_from_bytes(bytes) + .map_err(|e| err!(Database(error!("Failed to parse MXC unicode bytes from our database: {e}")))) + }) + .transpose()?; + + let Some(mxc_s) = mxc else { + return Err!(Database("Parsed MXC URL unicode bytes from database but still is None")); + }; + + debug_info!("Parsed MXC key to URL: {mxc_s}"); + let mxc = OwnedMxcUri::from(mxc_s); + + mxcs.push(mxc); + } + + Ok(mxcs) + } + /// Deletes all remote only media files in the given at or after /// time/duration. Returns a u32 with the amount of media files deleted. pub async fn delete_all_remote_media_at_after_time(&self, time: String, force: bool) -> Result { - let all_keys = self.db.get_all_media_keys(); - let user_duration: SystemTime = match cyborgtime::parse_duration(&time) { Err(e) => return Err!(Database(error!("Failed to parse specified time duration: {e}"))), Ok(duration) => SystemTime::now() @@ -171,7 +203,9 @@ impl Service { .ok_or(err!(Arithmetic("Duration {duration:?} is too large")))?, }; - let mut remote_mxcs: Vec = vec![]; + let all_keys = self.db.get_all_media_keys(); + + let mut remote_mxcs = Vec::with_capacity(all_keys.len()); for key in all_keys { debug!("Full MXC key from database: {key:?}");