tuwunel_api/client/admin/media/
mod.rs1mod delete_media;
4mod delete_media_by_date_size;
5mod delete_user_media;
6mod list_room_media;
7mod list_user_media;
8mod purge_media_cache;
9mod query_media;
10mod user_media_statistics;
11
12use ruma::{
13 UInt,
14 api::Direction::{self, Backward, Forward},
15};
16use synapse_admin_api::media::list_user_media::v1::MediaSortOrder;
17use tuwunel_service::media::UserMediaEntry;
18
19pub(crate) use self::{
20 delete_media::admin_delete_media_route,
21 delete_media_by_date_size::admin_delete_media_by_date_size_route,
22 delete_user_media::admin_delete_user_media_route,
23 list_room_media::admin_list_room_media_route, list_user_media::admin_list_user_media_route,
24 purge_media_cache::admin_purge_media_cache_route, query_media::admin_query_media_route,
25 user_media_statistics::admin_user_media_statistics_route,
26};
27
28pub(super) const MIN_BEFORE_TS: u64 = 30_000_000_000;
31
32#[derive(Clone, Copy)]
34pub(super) enum Column {
35 MediaId,
36 UploadName,
37 CreatedTs,
38 MediaLength,
39 MediaType,
40}
41
42#[derive(PartialEq, Eq, PartialOrd, Ord)]
44enum SortKey<'a> {
45 Str(&'a str),
46 OptStr(Option<&'a str>),
47 Num(Option<u64>),
48}
49
50pub(super) fn select_page(
53 entries: Vec<UserMediaEntry>,
54 order_by: Option<&MediaSortOrder>,
55 dir: Option<Direction>,
56 from: usize,
57 limit: usize,
58) -> Vec<UserMediaEntry> {
59 paginate(entries, column_of(order_by), direction_of(order_by, dir), from, limit)
60}
61
62fn column_of(order_by: Option<&MediaSortOrder>) -> Column {
63 match order_by {
64 | Some(MediaSortOrder::MediaId) => Column::MediaId,
65 | Some(MediaSortOrder::UploadName) => Column::UploadName,
66 | Some(MediaSortOrder::MediaLength) => Column::MediaLength,
67 | Some(MediaSortOrder::MediaType) => Column::MediaType,
68 | _ => Column::CreatedTs,
69 }
70}
71
72fn direction_of(order_by: Option<&MediaSortOrder>, dir: Option<Direction>) -> Direction {
75 match (order_by, dir) {
76 | (None, None) => Backward,
77 | (_, Some(dir)) => dir,
78 | (Some(_), None) => Forward,
79 }
80}
81
82fn paginate(
83 mut entries: Vec<UserMediaEntry>,
84 column: Column,
85 dir: Direction,
86 from: usize,
87 limit: usize,
88) -> Vec<UserMediaEntry> {
89 entries.sort_by(|a, b| {
90 sort_key(column, a)
91 .cmp(&sort_key(column, b))
92 .then_with(|| a.mxc.cmp(&b.mxc))
93 });
94
95 if matches!(dir, Backward) {
96 entries.reverse();
97 }
98
99 entries
100 .into_iter()
101 .skip(from)
102 .take(limit)
103 .collect()
104}
105
106fn sort_key(column: Column, entry: &UserMediaEntry) -> SortKey<'_> {
107 match column {
108 | Column::MediaId => SortKey::Str(entry.mxc.media_id().unwrap_or_default()),
109 | Column::UploadName => SortKey::OptStr(entry.upload_name.as_deref()),
110 | Column::MediaType => SortKey::OptStr(entry.media_type.as_deref()),
111 | Column::MediaLength => SortKey::Num(entry.media_length),
112 | Column::CreatedTs => SortKey::Num(Some(entry.created_ts)),
113 }
114}
115
116pub(super) fn usize_from(value: UInt) -> usize {
117 usize::try_from(u64::from(value)).unwrap_or(usize::MAX)
118}
119
120#[cfg(test)]
121mod tests {
122 use ruma::{
123 api::Direction::{Backward, Forward},
124 user_id,
125 };
126
127 use super::{Column, UserMediaEntry, paginate};
128
129 fn entry(media_id: &str, created_ts: u64, media_length: Option<u64>) -> UserMediaEntry {
130 UserMediaEntry {
131 mxc: format!("mxc://example.org/{media_id}").into(),
132 media_type: Some("image/png".to_owned()),
133 upload_name: Some(format!("{media_id}.png")),
134 media_length,
135 created_ts,
136 user_id: Some(user_id!("@alice:example.org").to_owned()),
137 }
138 }
139
140 fn ids(entries: &[UserMediaEntry]) -> Vec<&str> {
141 entries
142 .iter()
143 .map(|e| e.mxc.media_id().unwrap_or_default())
144 .collect()
145 }
146
147 #[test]
148 fn created_ts_backward_is_newest_first() {
149 let entries = vec![entry("a", 10, None), entry("b", 30, None), entry("c", 20, None)];
150
151 let page = paginate(entries, Column::CreatedTs, Backward, 0, 100);
152
153 assert_eq!(ids(&page), ["b", "c", "a"]);
154 }
155
156 #[test]
157 fn created_ts_forward_is_oldest_first() {
158 let entries = vec![entry("a", 10, None), entry("b", 30, None), entry("c", 20, None)];
159
160 let page = paginate(entries, Column::CreatedTs, Forward, 0, 100);
161
162 assert_eq!(ids(&page), ["a", "c", "b"]);
163 }
164
165 #[test]
166 fn from_and_limit_window_the_page() {
167 let entries = vec![entry("a", 10, None), entry("b", 30, None), entry("c", 20, None)];
168
169 let page = paginate(entries, Column::CreatedTs, Forward, 1, 1);
170
171 assert_eq!(ids(&page), ["c"]);
172 }
173
174 #[test]
175 fn media_length_backward_puts_largest_first() {
176 let entries =
177 vec![entry("a", 10, Some(5)), entry("b", 10, Some(30)), entry("c", 10, Some(20))];
178
179 let page = paginate(entries, Column::MediaLength, Backward, 0, 100);
180
181 assert_eq!(ids(&page), ["b", "c", "a"]);
182 }
183}