diff --git a/src/database/key_value/media.rs b/src/database/key_value/media.rs index 10a55554..5b097f51 100644 --- a/src/database/key_value/media.rs +++ b/src/database/key_value/media.rs @@ -161,4 +161,27 @@ impl service::media::Data for KeyValueDatabase { }) .collect() } + + fn all_file_metadata( + &self, + ) -> Box< + dyn Iterator> + '_, + > { + Box::new( + self.mediaid_file + .iter() + .map(|(key, _)| { + let key = MediaFileKey::new(key); + + let parts = MediaFileKeyParts::try_from(&key)?; + if parts.width != 0 && parts.height != 0 { + // Skip thumbnails + return Ok(None); + }; + + Ok(Some((parts.mxc, parts.meta, key))) + }) + .filter_map(Result::transpose), + ) + } } diff --git a/src/service/admin.rs b/src/service/admin.rs index 5bb56c4f..a1b81ebb 100644 --- a/src/service/admin.rs +++ b/src/service/admin.rs @@ -23,8 +23,8 @@ use ruma::{ TimelineEventType, }, signatures::verify_json, - EventId, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId, RoomId, - RoomVersionId, ServerName, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId, + OwnedServerName, RoomId, RoomVersionId, ServerName, UserId, }; use serde_json::value::to_raw_value; use tokio::sync::{mpsc, Mutex, RwLock}; @@ -185,6 +185,17 @@ enum AdminCommand { mxc: OwnedMxcUri, }, + /// Delete cached remote media from the database. + /// + /// This media may still be fetched and cached again in the future. + DeleteRemoteMedia { + /// If specified, only delete remote media from this origin. + /// + /// If not specified, all remote media will be deleted. + #[clap(long)] + origin: Option, + }, + /// Verify json signatures /// [commandbody]() /// # ``` @@ -803,6 +814,50 @@ impl Service { services().media.delete(mxc).await?; RoomMessageEventContent::text_plain("Media deleted.") } + AdminCommand::DeleteRemoteMedia { + origin, + } => { + if origin.as_deref() == Some(services().globals.server_name()) { + return Ok(RoomMessageEventContent::text_plain( + "Specified origin is this server. Will not delete \ + anything.", + )); + } + + let mut count = 0; + + // The `media.iter_all()` iterator is not `Send`, so spawn it in + // a separate thread and send the results over a channel. + let (tx, mut rx) = mpsc::channel(1); + tokio::task::spawn_blocking(move || { + for mxc in services().media.iter_all() { + if tx.blocking_send(mxc).is_err() { + break; + } + } + }); + + while let Some(mxc) = rx.recv().await { + let mxc = mxc?; + let server_name = mxc.server_name(); + + if server_name == Ok(services().globals.server_name()) { + continue; + } + if let Some(origin) = &origin { + if server_name != Ok(origin) { + continue; + } + } + + count += 1; + services().media.delete(mxc).await?; + } + + RoomMessageEventContent::text_plain(format!( + "{count} media objects deleted." + )) + } AdminCommand::DeactivateUser { leave_rooms, user_id, diff --git a/src/service/media.rs b/src/service/media.rs index 830a43e4..0b4903aa 100644 --- a/src/service/media.rs +++ b/src/service/media.rs @@ -141,6 +141,15 @@ impl Service { Ok(()) } + /// List all media stored in the database. + /// + /// Each MXC is list once. Thumbnails are not included separately from the + /// original media. + #[tracing::instrument(skip(self))] + pub(crate) fn iter_all(&self) -> impl Iterator> { + self.db.all_file_metadata().map(|media| media.map(|(mxc, ..)| mxc)) + } + /// Returns width, height of the thumbnail and whether it should be cropped. /// Returns None when the server should send the original file. fn thumbnail_properties( diff --git a/src/service/media/data.rs b/src/service/media/data.rs index d7734845..139032dc 100644 --- a/src/service/media/data.rs +++ b/src/service/media/data.rs @@ -29,4 +29,13 @@ pub(crate) trait Data: Send + Sync { &self, mxc: OwnedMxcUri, ) -> Result>; + + /// Returns an iterator over metadata for all media. + /// + /// Thumbnails are not included. + fn all_file_metadata( + &self, + ) -> Box< + dyn Iterator> + '_, + >; }