Refactor admin media
This commit is contained in:
@@ -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;
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user