tuwunel_admin/media/
commands.rs1use std::time::Duration;
2
3use ruma::{CanonicalJsonValue, Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName};
4use tuwunel_core::{
5 Err, Result, debug, err, error, info, trace,
6 utils::{math::Expected, time::parse_timepoint_ago},
7 warn,
8};
9use tuwunel_service::media::Dim;
10
11use crate::{admin_command, utils::parse_local_user_id};
12
13#[admin_command]
14pub(super) async fn delete(&self, mxc: OwnedMxcUri) -> Result {
15 self.services
16 .media
17 .delete(&mxc.as_str().try_into()?)
18 .await?;
19
20 Err!("Deleted the MXC from our database and on our filesystem.")
21}
22
23#[admin_command]
24pub(super) async fn delete_by_event(&self, event_id: OwnedEventId) -> Result {
25 let mut mxc_urls = Vec::with_capacity(3);
26
27 let event_json = self
29 .services
30 .timeline
31 .get_pdu_json(&event_id)
32 .await
33 .map_err(|_| err!("Event ID does not exist or is not known to us."))?;
34
35 let content = event_json
36 .get("content")
37 .and_then(CanonicalJsonValue::as_object)
38 .ok_or_else(|| {
39 err!(
40 "Event ID does not have a \"content\" key, this is not a message or an event \
41 type that contains media.",
42 )
43 })?;
44
45 debug!("Attempting to go into \"url\" key for main media file");
47 if let Some(url) = content
48 .get("url")
49 .and_then(CanonicalJsonValue::as_str)
50 {
51 debug!("Got a URL in the event ID {event_id}: {url}");
52
53 mxc_urls.push(url.to_owned());
54 } else {
55 debug!("No main media found.");
56 }
57
58 debug!("Attempting to go into \"info\" key for thumbnails");
60 if let Some(thumbnail_url) = content
61 .get("info")
62 .and_then(CanonicalJsonValue::as_object)
63 .and_then(|info| info.get("thumbnail_url"))
64 .and_then(CanonicalJsonValue::as_str)
65 {
66 debug!("Found a thumbnail_url in info key: {thumbnail_url}");
67
68 mxc_urls.push(thumbnail_url.to_owned());
69 } else {
70 debug!("No thumbnails found.");
71 }
72
73 debug!("Attempting to go into \"file\" key");
75 if let Some(url) = content
76 .get("file")
77 .and_then(CanonicalJsonValue::as_object)
78 .and_then(|file| file.get("url"))
79 .and_then(CanonicalJsonValue::as_str)
80 {
81 debug!("Found url in file key: {url}");
82
83 mxc_urls.push(url.to_owned());
84 } else {
85 debug!("No \"url\" key in \"file\" key.");
86 }
87
88 if mxc_urls.is_empty() {
89 return Err!("Parsed event ID but found no MXC URLs.",);
90 }
91
92 let mut mxc_deletion_count: usize = 0;
93
94 for mxc_url in mxc_urls {
95 if !mxc_url.starts_with("mxc://") {
96 warn!("Ignoring non-mxc url {mxc_url}");
97 continue;
98 }
99
100 let mxc: Mxc<'_> = mxc_url.as_str().try_into()?;
101
102 match self.services.media.delete(&mxc).await {
103 | Ok(()) => {
104 info!("Successfully deleted {mxc_url} from filesystem and database");
105 mxc_deletion_count = mxc_deletion_count.saturating_add(1);
106 },
107 | Err(e) => {
108 warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}");
109 },
110 }
111 }
112
113 self.write_str(&format!(
114 "Deleted {mxc_deletion_count} total MXCs from our database and the filesystem from \
115 event ID {event_id}."
116 ))
117 .await
118}
119
120#[admin_command]
121pub(super) async fn delete_list(&self) -> Result {
122 if self.body.len() < 2
123 || !self.body[0].trim().starts_with("```")
124 || self.body.last().unwrap_or(&"").trim() != "```"
125 {
126 return Err!("Expected code block in command body. Add --help for details.",);
127 }
128
129 let mut failed_parsed_mxcs: usize = 0;
130
131 let mxc_list = self
132 .body
133 .to_vec()
134 .drain(1..self.body.len().expected_sub(1))
135 .filter_map(|mxc_s| {
136 mxc_s
137 .try_into()
138 .inspect_err(|e| {
139 warn!("Failed to parse user-provided MXC URI: {e}");
140 failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1);
141 })
142 .ok()
143 })
144 .collect::<Vec<Mxc<'_>>>();
145
146 let mut mxc_deletion_count: usize = 0;
147
148 for mxc in &mxc_list {
149 trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk");
150 match self.services.media.delete(mxc).await {
151 | Ok(()) => {
152 info!("Successfully deleted {mxc} from filesystem and database");
153 mxc_deletion_count = mxc_deletion_count.saturating_add(1);
154 },
155 | Err(e) => {
156 warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
157 continue;
158 },
159 }
160 }
161
162 self.write_str(&format!(
163 "Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database \
164 and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from the database.",
165 ))
166 .await
167}
168
169#[admin_command]
170pub(super) async fn delete_range(
171 &self,
172 duration: String,
173 older_than: bool,
174 newer_than: bool,
175 yes_i_want_to_delete_local_media: bool,
176) -> Result {
177 if older_than == newer_than {
178 return Err!("Please pick only one of --older_than or --newer_than.",);
179 }
180
181 let duration = parse_timepoint_ago(&duration)?;
182 let deleted_count = self
183 .services
184 .media
185 .delete_range(duration, older_than, newer_than, yes_i_want_to_delete_local_media)
186 .await?;
187
188 self.write_str(&format!("Deleted {deleted_count} total files."))
189 .await
190}
191
192#[admin_command]
193pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
194 let user_id = parse_local_user_id(self.services, &username)?;
195
196 let deleted_count = self
197 .services
198 .media
199 .delete_from_user(&user_id)
200 .await?;
201
202 self.write_str(&format!("Deleted {deleted_count} total files."))
203 .await
204}
205
206#[admin_command]
207pub(super) async fn delete_all_from_server(
208 &self,
209 server_name: OwnedServerName,
210 yes_i_want_to_delete_local_media: bool,
211) -> Result {
212 if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
213 return Err!("This command only works for remote media by default.",);
214 }
215
216 let Ok(all_mxcs) = self
217 .services
218 .media
219 .get_all_mxcs()
220 .await
221 .inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}"))
222 else {
223 return Err!("Failed to get MXC URIs from our database",);
224 };
225
226 let mut deleted_count: usize = 0;
227
228 for mxc in all_mxcs {
229 let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| {
230 warn!(
231 "Failed to parse MXC {mxc} server name from database, ignoring error and \
232 skipping: {e}"
233 );
234 }) else {
235 continue;
236 };
237
238 if mxc_server_name != server_name {
239 trace!("skipping MXC URI {mxc}");
240 continue;
241 }
242
243 let mxc: Mxc<'_> = mxc.as_str().try_into()?;
244
245 match self.services.media.delete(&mxc).await {
246 | Ok(()) => {
247 deleted_count = deleted_count.saturating_add(1);
248 },
249 | Err(e) => {
250 warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
251 },
252 }
253 }
254
255 self.write_str(&format!("Deleted {deleted_count} total files."))
256 .await
257}
258
259#[admin_command]
260pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result {
261 let mxc: Mxc<'_> = mxc.as_str().try_into()?;
262 let metadata = self.services.media.get_metadata(&mxc).await;
263
264 self.write_str(&format!("```\n{metadata:#?}\n```"))
265 .await
266}
267
268#[admin_command]
269pub(super) async fn get_remote_file(
270 &self,
271 mxc: OwnedMxcUri,
272 server: Option<OwnedServerName>,
273 timeout: u32,
274) -> Result {
275 let mxc: Mxc<'_> = mxc.as_str().try_into()?;
276 let timeout = Duration::from_millis(timeout.into());
277 let mut result = self
278 .services
279 .media
280 .fetch_remote_content(&mxc, server.as_deref(), timeout)
281 .await?;
282
283 let len = result.content.len();
285 result.content.clear();
286
287 self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
288 .await
289}
290
291#[admin_command]
292pub(super) async fn get_remote_thumbnail(
293 &self,
294 mxc: OwnedMxcUri,
295 server: Option<OwnedServerName>,
296 timeout: u32,
297 width: u32,
298 height: u32,
299) -> Result {
300 let mxc: Mxc<'_> = mxc.as_str().try_into()?;
301 let timeout = Duration::from_millis(timeout.into());
302 let dim = Dim::new(width, height, None);
303 let mut result = self
304 .services
305 .media
306 .fetch_remote_thumbnail(&mxc, server.as_deref(), timeout, &dim)
307 .await?;
308
309 let len = result.content.len();
311 result.content.clear();
312
313 self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
314 .await
315}