atagen

macltok

public
Star0
Watch0
8fa8a7cde89c163e43843797a791b75a52eaeae6
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/media.rsRust737 lines24.5 KB
1
use std::{
2
    borrow::Cow,
3
    collections::{HashMap, VecDeque},
4
    io::Read as _,
5
    panic::AssertUnwindSafe,
6
    sync::Arc,
7
    time::Duration,
8
};
9
10
use futures_util::{
11
    FutureExt, StreamExt,
12
    future::{Shared, WeakShared},
13
};
14
use matrix_sdk::{
15
    Client, Room, SendOutsideWasm, SyncOutsideWasm,
16
    media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
17
    room::reply::{EnforceThread, Reply as SdkReply},
18
    ruma::{
19
        api::{
20
            Metadata, OutgoingRequest,
21
            auth_scheme::SendAccessToken,
22
            client::{authenticated_media, media},
23
        },
24
        events::room::{
25
            EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileInfo, MediaSource,
26
            message::{AddMentions, ReplyWithinThread},
27
        },
28
    },
29
};
30
use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
31
use ruma::MxcUri;
32
use tokio::sync::Mutex;
33
34
use super::executor::EngineBoxFuture;
35
use crate::{
36
    AttachmentEncryption, AttachmentUpload, Error, ErrorKind, Media, MediaFile, MediaId, MediaKind,
37
    MediaLocation, MediaSize, TimelineId,
38
    normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
39
    upload::prepare_upload,
40
};
41
42
/// a stalled download gives up after this long without a byte
43
const DOWNLOAD_IDLE: Duration = Duration::from_secs(60);
44
/// the sdk client gives up on any request after thirty seconds, far too soon for a video
45
const DOWNLOAD_TOTAL: Duration = Duration::from_secs(60 * 60);
46
47
type FlightFuture = EngineBoxFuture<'static, Result<Media, Error>>;
48
type Flight = Shared<FlightFuture>;
49
50
/// owns everything needed to turn a media id or an mxc uri into bytes, so the
51
/// handle can answer media queries without a trip through the engine
52
pub(crate) struct MediaService {
53
    client: Client,
54
    registry: Mutex<MediaRegistry>,
55
    flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
56
    max_bytes: usize,
57
}
58
59
impl MediaService {
60
    pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {
61
        Self {
62
            client,
63
            registry: Mutex::new(MediaRegistry::new(max_sources)),
64
            flights: Mutex::new(HashMap::new()),
65
            max_bytes,
66
        }
67
    }
68
69
    pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {
70
        if !media.is_empty() {
71
            self.registry.lock().await.insert_all(media);
72
        }
73
    }
74
75
    pub(crate) async fn release(&self, scope: &MediaScope) {
76
        self.registry.lock().await.release(scope);
77
    }
78
79
    pub(crate) async fn clear(&self) {
80
        self.registry.lock().await.clear();
81
    }
82
83
    pub(crate) async fn timeline_media(
84
        &self,
85
        media_id: &MediaId,
86
        size: MediaSize,
87
    ) -> Result<Media, Error> {
88
        let chosen = choose_source(self.registered(media_id).await?, size)?;
89
        if chosen
90
            .declared_size
91
            .is_some_and(|size| size > self.max_bytes as u64)
92
        {
93
            return Err(too_large("media exceeds the configured byte limit"));
94
        }
95
        self.fetch_or_file(chosen.request, chosen.mimetype).await
96
    }
97
98
    pub(crate) async fn media_location(
99
        &self,
100
        media_id: &MediaId,
101
        size: MediaSize,
102
    ) -> Result<MediaLocation, Error> {
103
        let media = self.registered(media_id).await?;
104
        let chosen = choose_source(media.clone(), size)?;
105
        if is_local(&chosen.request.source) {
106
            return Ok(MediaLocation::Local);
107
        }
108
        if !matches!(chosen.request.format, MediaFormat::Thumbnail(_)) {
109
            return Ok(MediaLocation::File(
110
                media_file(
111
                    &self.client,
112
                    chosen.request,
113
                    chosen.mimetype,
114
                    chosen.declared_size,
115
                )
116
                .await?,
117
            ));
118
        }
119
        let url = media_http_request(&self.client, &chosen.request)
120
            .await?
121
            .uri()
122
            .to_string();
123
        let fallback = MediaRequestParameters {
124
            source: media.full.source,
125
            format: MediaFormat::File,
126
        };
127
        Ok(MediaLocation::ServerThumbnail {
128
            url,
129
            fallback: media_file(
130
                &self.client,
131
                fallback,
132
                media.full.mimetype,
133
                media.full.declared_size,
134
            )
135
            .await?,
136
        })
137
    }
138
139
    async fn registered(&self, media_id: &MediaId) -> Result<MediaRegistration, Error> {
140
        if let Some(uri) = media_id.as_plain() {
141
            return plain_registration(media_id, uri);
142
        }
143
        self.registry
144
            .lock()
145
            .await
146
            .get(media_id)
147
            .cloned()
148
            .ok_or_else(|| {
149
                Error::new(
150
                    ErrorKind::MediaUnknown,
151
                    "media is not registered in a retained timeline",
152
                )
153
            })
154
    }
155
156
    /// plenty of servers won't thumbnail everything, and the file itself is the next best
157
    /// thing. the byte cap still holds either way
158
    async fn fetch_or_file(
159
        &self,
160
        request: MediaRequestParameters,
161
        mimetype: Option<String>,
162
    ) -> Result<Media, Error> {
163
        if !matches!(request.format, MediaFormat::Thumbnail(_)) {
164
            return self.fetch(request, mimetype).await;
165
        }
166
        match self.fetch(request.clone(), mimetype.clone()).await {
167
            Err(error) if error.kind == ErrorKind::MediaUnavailable => {
168
                let file = MediaRequestParameters {
169
                    source: request.source,
170
                    format: MediaFormat::File,
171
                };
172
                self.fetch(file, mimetype).await
173
            }
174
            result => result,
175
        }
176
    }
177
178
    async fn fetch(
179
        &self,
180
        request: MediaRequestParameters,
181
        mimetype: Option<String>,
182
    ) -> Result<Media, Error> {
183
        let key = flight_key(&request);
184
        let flight = {
185
            let mut flights = self.flights.lock().await;
186
            flights.retain(|_, flight| flight.upgrade().is_some());
187
            match flights.get(&key).and_then(WeakShared::upgrade) {
188
                Some(flight) => flight,
189
                None => {
190
                    let prepared = AssertUnwindSafe(fetch(
191
                        self.client.clone(),
192
                        request,
193
                        mimetype,
194
                        self.max_bytes,
195
                    ))
196
                    .catch_unwind()
197
                    .map(|result| {
198
                        result.unwrap_or_else(|_| {
199
                            Err(Error::new(ErrorKind::Other, "media fetch panicked"))
200
                        })
201
                    });
202
                    let boxed: FlightFuture = Box::pin(prepared);
203
                    let flight: Flight = boxed.shared();
204
                    if let Some(weak) = flight.downgrade() {
205
                        flights.insert(key, weak);
206
                    }
207
                    flight
208
                }
209
            }
210
        };
211
        flight.await
212
    }
213
}
214
215
struct ChosenSource {
216
    request: MediaRequestParameters,
217
    mimetype: Option<String>,
218
    declared_size: Option<u64>,
219
}
220
221
fn plain_registration(media_id: &MediaId, uri: &MxcUri) -> Result<MediaRegistration, Error> {
222
    if !uri.is_valid() {
223
        return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
224
    }
225
    Ok(MediaRegistration {
226
        id: media_id.clone(),
227
        kind: MediaKind::Image,
228
        full: MediaSourceInfo {
229
            source: MediaSource::Plain(uri.to_owned()),
230
            mimetype: None,
231
            declared_size: None,
232
        },
233
        thumbnail: None,
234
    })
235
}
236
237
fn choose_source(media: MediaRegistration, size: MediaSize) -> Result<ChosenSource, Error> {
238
    let (source, format, mimetype, declared_size) = match size {
239
        MediaSize::Full => (
240
            media.full.source,
241
            MediaFormat::File,
242
            media.full.mimetype,
243
            media.full.declared_size,
244
        ),
245
        MediaSize::Thumbnail { width, height } => match media.thumbnail {
246
            Some(thumbnail) => (
247
                thumbnail.source,
248
                MediaFormat::File,
249
                thumbnail.mimetype,
250
                thumbnail.declared_size,
251
            ),
252
            // the server can only resize what it can read
253
            None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
254
                && matches!(media.full.source, MediaSource::Plain(_)) =>
255
            {
256
                // the server picks the thumbnail's type, so the original's would be a lie
257
                let format = if is_local(&media.full.source) {
258
                    MediaFormat::File
259
                } else {
260
                    thumbnail_format(width, height)
261
                };
262
                (media.full.source, format, None, None)
263
            }
264
            None => {
265
                return Err(Error::new(
266
                    ErrorKind::MediaUnavailable,
267
                    "the requested media size is unavailable",
268
                ));
269
            }
270
        },
271
    };
272
    Ok(ChosenSource {
273
        request: MediaRequestParameters { source, format },
274
        mimetype,
275
        declared_size,
276
    })
277
}
278
279
async fn media_file(
280
    client: &Client,
281
    request: MediaRequestParameters,
282
    mimetype: Option<String>,
283
    declared_size: Option<u64>,
284
) -> Result<MediaFile, Error> {
285
    let url = media_http_request(client, &request)
286
        .await?
287
        .uri()
288
        .to_string();
289
    let encryption = match &request.source {
290
        MediaSource::Plain(_) => None,
291
        MediaSource::Encrypted(file) => {
292
            let (EncryptedFileInfo::V2(info), Some(EncryptedFileHash::Sha256(sha256))) = (
293
                &file.info,
294
                file.hashes.get(&EncryptedFileHashAlgorithm::Sha256),
295
            ) else {
296
                return Err(Error::new(
297
                    ErrorKind::MediaUnavailable,
298
                    "encrypted media is not v2 with a sha256 hash",
299
                ));
300
            };
301
            Some(AttachmentEncryption {
302
                key: info.k.encode(),
303
                iv: info.iv.encode(),
304
                sha256: sha256.encode(),
305
            })
306
        }
307
    };
308
    Ok(MediaFile {
309
        url,
310
        mimetype,
311
        declared_size,
312
        encryption,
313
    })
314
}
315
316
fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
317
    MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into()))
318
}
319
320
fn is_local(source: &MediaSource) -> bool {
321
    let uri = match source {
322
        MediaSource::Plain(uri) => uri,
323
        MediaSource::Encrypted(file) => &file.url,
324
    };
325
    uri.server_name()
326
        .is_ok_and(|server| server.as_str() == "send-queue.localhost")
327
}
328
329
/// plain and encrypted sources share a cache key in the sdk, but must never share a flight
330
fn flight_key(request: &MediaRequestParameters) -> String {
331
    let uri = match &request.source {
332
        MediaSource::Plain(uri) => format!("plain|{uri}"),
333
        MediaSource::Encrypted(file) => format!("encrypted|{}", file.url),
334
    };
335
    match &request.format {
336
        MediaFormat::File => uri.to_owned(),
337
        MediaFormat::Thumbnail(settings) => format!(
338
            "{uri}|{}|{}x{}|{}",
339
            settings.method, settings.width, settings.height, settings.animated
340
        ),
341
    }
342
}
343
344
fn too_large(message: &'static str) -> Error {
345
    Error::new(ErrorKind::MediaTooLarge, message)
346
}
347
348
async fn fetch(
349
    client: Client,
350
    request: MediaRequestParameters,
351
    mimetype: Option<String>,
352
    max_bytes: usize,
353
) -> Result<Media, Error> {
354
    if let Some(cached) = cached_media(&client, &request).await? {
355
        if cached.len() > max_bytes {
356
            if let Ok(store) = client.media_store().lock().await {
357
                let _ = store.remove_media_content(&request).await;
358
            }
359
            return Err(too_large("cached media exceeds the configured byte limit"));
360
        }
361
        // plain and encrypted share a cache key, so a plain fetch of an encrypted file's uri
362
        // leaves ciphertext here. its hash gives it away
363
        if is_ciphertext(&cached, &request.source) {
364
            let bytes = maybe_decrypt(cached, &request, max_bytes)?;
365
            cache(&client, &request, &bytes).await;
366
            return Ok(media(bytes, mimetype));
367
        }
368
        return Ok(media(cached, mimetype));
369
    }
370
    let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
371
    // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
372
    let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
373
    cache(&client, &request, &bytes).await;
374
    Ok(media(bytes, mimetype.or(served_mimetype)))
375
}
376
377
async fn cache(client: &Client, request: &MediaRequestParameters, bytes: &[u8]) {
378
    if let Ok(store) = client.media_store().lock().await {
379
        let _ = store
380
            .add_media_content(
381
                request,
382
                bytes.to_vec(),
383
                matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
384
            )
385
            .await;
386
    }
387
}
388
389
fn is_ciphertext(bytes: &[u8], source: &MediaSource) -> bool {
390
    use sha2::{Digest, Sha256};
391
    let MediaSource::Encrypted(file) = source else {
392
        return false;
393
    };
394
    file.hashes
395
        .get(&ruma::events::room::EncryptedFileHashAlgorithm::Sha256)
396
        .is_some_and(|hash| hash.as_bytes() == Sha256::digest(bytes).as_slice())
397
}
398
399
fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
400
    let mimetype = mimetype
401
        .or_else(|| sniff_image(&bytes).map(str::to_owned))
402
        .unwrap_or_else(|| "application/octet-stream".to_owned());
403
    Media {
404
        bytes: Arc::from(bytes),
405
        mimetype,
406
    }
407
}
408
409
/// the cache forgets content types, and avatars never declare one. images are the common case
410
fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
411
    match bytes {
412
        [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
413
        [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
414
        [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
415
        [
416
            b'R',
417
            b'I',
418
            b'F',
419
            b'F',
420
            _,
421
            _,
422
            _,
423
            _,
424
            b'W',
425
            b'E',
426
            b'B',
427
            b'P',
428
            ..,
429
        ] => Some("image/webp"),
430
        _ => None,
431
    }
432
}
433
434
async fn cached_media(
435
    client: &Client,
436
    request: &MediaRequestParameters,
437
) -> Result<Option<Vec<u8>>, Error> {
438
    fn cache_failed(
439
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
440
    ) -> Error {
441
        Error::with_source(ErrorKind::Store, "reading media cache failed", source)
442
    }
443
    client
444
        .media_store()
445
        .lock()
446
        .await
447
        .map_err(cache_failed)?
448
        .get_media_content(request)
449
        .await
450
        .map_err(cache_failed)
451
}
452
453
fn maybe_decrypt(
454
    bytes: Vec<u8>,
455
    request: &MediaRequestParameters,
456
    max_bytes: usize,
457
) -> Result<Vec<u8>, Error> {
458
    let MediaSource::Encrypted(file) = &request.source else {
459
        return Ok(bytes);
460
    };
461
    fn decrypt_failed(
462
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
463
    ) -> Error {
464
        Error::with_source(
465
            ErrorKind::MediaUnavailable,
466
            "media decryption failed",
467
            source,
468
        )
469
    }
470
    let info: MediaEncryptionInfo = (**file).clone().into();
471
    let mut cursor = std::io::Cursor::new(bytes);
472
    let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
473
    let mut decrypted = Vec::new();
474
    let mut chunk = [0_u8; 8192];
475
    loop {
476
        let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
477
        if read == 0 {
478
            return Ok(decrypted);
479
        }
480
        if read > max_bytes.saturating_sub(decrypted.len()) {
481
            return Err(too_large(
482
                "decrypted media exceeds the configured byte limit",
483
            ));
484
        }
485
        decrypted.extend_from_slice(&chunk[..read]);
486
    }
487
}
488
489
/// the sdk's own download has no size cap, so we do it by hand
490
async fn download_bounded(
491
    client: &Client,
492
    request: &MediaRequestParameters,
493
    max_bytes: usize,
494
) -> Result<(Vec<u8>, Option<String>), Error> {
495
    if is_local(&request.source) {
496
        return Err(Error::new(
497
            ErrorKind::MediaUnavailable,
498
            "local media is not in the media cache",
499
        ));
500
    }
501
    let network_failed =
502
        |source| Error::with_source(ErrorKind::Network, "media download failed", source);
503
    let mut refreshed = false;
504
    let response = loop {
505
        let http_request = media_http_request(client, request).await?;
506
        let (parts, _) = http_request.into_parts();
507
        let mut builder = client
508
            .http_client()
509
            .get(parts.uri.to_string())
510
            .timeout(DOWNLOAD_TOTAL);
511
        for (name, value) in &parts.headers {
512
            builder = builder.header(name.as_str(), value.as_bytes());
513
        }
514
        let response = builder.send().await.map_err(network_failed)?;
515
        if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
516
            break response;
517
        }
518
        if refreshed
519
            || client
520
                .session_tokens()
521
                .is_none_or(|tokens| tokens.refresh_token.is_none())
522
        {
523
            return Err(Error::new(
524
                ErrorKind::Unauthenticated,
525
                "media download was not authorized",
526
            ));
527
        }
528
        client.refresh_access_token().await.map_err(|source| {
529
            Error::from_refresh_token("refreshing media access token failed", source)
530
        })?;
531
        refreshed = true;
532
    };
533
    let status = response.status();
534
    if !status.is_success() {
535
        return Err(Error::new(
536
            ErrorKind::MediaUnavailable,
537
            format!("media download failed with status {status}"),
538
        ));
539
    }
540
    if response
541
        .content_length()
542
        .is_some_and(|length| length > max_bytes as u64)
543
    {
544
        return Err(too_large("media exceeds the configured byte limit"));
545
    }
546
    let mimetype = response
547
        .headers()
548
        .get(http::header::CONTENT_TYPE)
549
        .and_then(|value| value.to_str().ok())
550
        .map(str::to_owned);
551
    let mut stream = response.bytes_stream();
552
    let mut bytes = Vec::new();
553
    loop {
554
        let chunk = matrix_sdk::timeout::timeout(stream.next(), DOWNLOAD_IDLE)
555
            .await
556
            .map_err(|_| Error::new(ErrorKind::Network, "media download stalled"))?;
557
        let Some(chunk) = chunk else { break };
558
        let chunk = chunk.map_err(network_failed)?;
559
        if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
560
            return Err(too_large(
561
                "downloaded media exceeds the configured byte limit",
562
            ));
563
        }
564
        bytes.extend_from_slice(&chunk);
565
    }
566
    Ok((bytes, mimetype))
567
}
568
569
async fn media_http_request(
570
    client: &Client,
571
    request: &MediaRequestParameters,
572
) -> Result<http::Request<Vec<u8>>, Error> {
573
    let versions = client.supported_versions().await.map_err(|source| {
574
        Error::with_source(
575
            ErrorKind::Network,
576
            "reading supported server versions failed",
577
            source,
578
        )
579
    })?;
580
    let authenticated =
581
        authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
582
    let base = client.homeserver().to_string();
583
    let token = client.access_token();
584
    let invalid_uri =
585
        |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
586
    let (uri, thumbnail) = match (&request.source, &request.format) {
587
        (MediaSource::Encrypted(file), _) => (&file.url, None),
588
        (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
589
        (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
590
    };
591
    // each endpoint has its own auth input type, so no generic helper can take them all
592
    macro_rules! into_http {
593
        ($endpoint:expr, $token:expr) => {
594
            $endpoint
595
                .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
596
                .map_err(|source| {
597
                    Error::with_source(ErrorKind::Other, "media request is invalid", source)
598
                })?
599
        };
600
    }
601
    let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
602
    #[allow(deprecated)]
603
    let mut http_request = match (thumbnail, authenticated) {
604
        (None, true) => into_http!(
605
            authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
606
            authed
607
        ),
608
        (None, false) => into_http!(
609
            media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
610
            SendAccessToken::None
611
        ),
612
        (Some(settings), true) => {
613
            let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
614
                uri,
615
                settings.width,
616
                settings.height,
617
            )
618
            .map_err(invalid_uri)?;
619
            endpoint.method = Some(settings.method.clone());
620
            endpoint.animated = Some(settings.animated);
621
            into_http!(endpoint, authed)
622
        }
623
        (Some(settings), false) => {
624
            let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
625
                uri,
626
                settings.width,
627
                settings.height,
628
            )
629
            .map_err(invalid_uri)?;
630
            endpoint.method = Some(settings.method.clone());
631
            endpoint.animated = Some(settings.animated);
632
            into_http!(endpoint, SendAccessToken::None)
633
        }
634
    };
635
    // legacy endpoints don't ask for a token but plenty of servers want one anyway
636
    if let Some(token) = token.as_deref() {
637
        let value = format!("Bearer {token}").parse().map_err(|_| {
638
            Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
639
        })?;
640
        http_request
641
            .headers_mut()
642
            .insert(http::header::AUTHORIZATION, value);
643
    }
644
    Ok(http_request)
645
}
646
647
pub(crate) async fn queue_upload(
648
    room: Room,
649
    upload: AttachmentUpload,
650
    max_bytes: usize,
651
) -> Result<(), Error> {
652
    let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
653
    if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
654
        config.reply = Some(SdkReply {
655
            event_id: root_event_id.clone(),
656
            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
657
            add_mentions: AddMentions::No,
658
        });
659
    }
660
    room.send_queue()
661
        .send_attachment(upload.filename, mime, upload.bytes, config)
662
        .await
663
        .map(drop)
664
        .map_err(|source| {
665
            use matrix_sdk::send_queue::RoomSendQueueError;
666
            let kind = match &source {
667
                RoomSendQueueError::RoomNotJoined | RoomSendQueueError::RoomDisappeared => {
668
                    ErrorKind::RoomUnknown
669
                }
670
                RoomSendQueueError::StorageError(_) => ErrorKind::Store,
671
                _ => ErrorKind::MediaUnavailable,
672
            };
673
            Error::with_source(kind, "media upload could not be queued", source)
674
        })
675
}
676
677
/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
678
struct MediaRegistry {
679
    entries: HashMap<MediaId, (MediaRegistration, u64)>,
680
    order: VecDeque<(MediaId, u64)>,
681
    next_stamp: u64,
682
    limit: usize,
683
}
684
685
impl MediaRegistry {
686
    fn new(limit: usize) -> Self {
687
        Self {
688
            entries: HashMap::new(),
689
            order: VecDeque::new(),
690
            next_stamp: 0,
691
            limit,
692
        }
693
    }
694
695
    fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
696
        self.entries.get(id).map(|(media, _)| media)
697
    }
698
699
    fn insert_all(&mut self, media: Vec<MediaRegistration>) {
700
        for media in media {
701
            let stamp = self.next_stamp;
702
            self.next_stamp += 1;
703
            self.order.push_back((media.id.clone(), stamp));
704
            self.entries.insert(media.id.clone(), (media, stamp));
705
        }
706
        while self.entries.len() > self.limit {
707
            let Some((id, stamp)) = self.order.pop_front() else {
708
                break;
709
            };
710
            if self.is_current(&id, stamp) {
711
                self.entries.remove(&id);
712
            }
713
        }
714
        if self.order.len() > self.limit.saturating_mul(4) {
715
            let entries = &self.entries;
716
            self.order
717
                .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
718
        }
719
    }
720
721
    fn is_current(&self, id: &MediaId, stamp: u64) -> bool {
722
        self.entries
723
            .get(id)
724
            .is_some_and(|(_, current)| *current == stamp)
725
    }
726
727
    fn release(&mut self, scope: &MediaScope) {
728
        self.entries.retain(|id, _| !scope.owns(id));
729
        let entries = &self.entries;
730
        self.order.retain(|(id, _)| entries.contains_key(id));
731
    }
732
733
    fn clear(&mut self) {
734
        self.entries.clear();
735
        self.order.clear();
736
    }
737
}