Skip to main content

headless_lms_utils/file_store/
media_uploads.rs

1//! Where uploaded course media is stored, under which names, and how large it may be. Shared by the
2//! CMS upload and the agent upload so both put files in the same places under the same limits.
3
4use std::path::PathBuf;
5
6use uuid::Uuid;
7
8use super::file_utils::get_extension_from_filename;
9use crate::strings::generate_random_string;
10
11/// Who an upload belongs to; the first folder of its path.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MediaOwner {
14    Organization(Uuid),
15    Course(Uuid),
16    Exam(Uuid),
17}
18
19/// What an upload is, by its MIME top-level type.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MediaKind {
22    Image,
23    Audio,
24    File,
25}
26
27impl MediaKind {
28    /// The kind for a MIME type's top-level type, e.g. `image`.
29    pub fn from_mime_top_level(top_level: &str) -> Self {
30        match top_level {
31            "image" => MediaKind::Image,
32            "audio" => MediaKind::Audio,
33            _ => MediaKind::File,
34        }
35    }
36}
37
38/// A fresh random name to store an upload under, keeping the letters and digits of the original
39/// name's extension.
40pub fn stored_file_name(original_name: &str) -> String {
41    let mut name = generate_random_string(30);
42    let extension: String = get_extension_from_filename(original_name)
43        .unwrap_or_default()
44        .chars()
45        .filter(|c| c.is_alphanumeric())
46        .collect();
47    if !extension.is_empty() {
48        name.push('.');
49        name.push_str(&extension);
50    }
51    name
52}
53
54/// `<owner>/<id>/<images|audios|files>/<file_name>`.
55pub fn media_path(owner: MediaOwner, kind: MediaKind, file_name: &str) -> PathBuf {
56    let (base_dir, base_id) = match owner {
57        MediaOwner::Organization(id) => ("organization", id),
58        MediaOwner::Course(id) => ("course", id),
59        MediaOwner::Exam(id) => ("exam", id),
60    };
61    let kind_dir = match kind {
62        MediaKind::Image => "images",
63        MediaKind::Audio => "audios",
64        MediaKind::File => "files",
65    };
66    [base_dir, &base_id.to_string(), kind_dir, file_name]
67        .iter()
68        .collect()
69}
70
71// Mirrors shared-module/packages/common/src/utils/files.ts; the ingress caps request size too
72// (kubernetes/base/ingress.yml).
73const SIZE_LIMITS_BYTES: &[(&str, i32)] = &[
74    ("image", 10 * 1024 * 1024),
75    ("audio", 100 * 1024 * 1024),
76    ("video", 100 * 1024 * 1024),
77    ("application", 25 * 1024 * 1024),
78    ("text", 25 * 1024 * 1024),
79];
80const DEFAULT_SIZE_LIMIT_BYTES: i32 = 10 * 1024 * 1024;
81
82/// The largest upload allowed for a MIME top-level type such as `image`.
83pub fn size_limit_bytes(mime_top_level: Option<&str>) -> i32 {
84    mime_top_level
85        .and_then(|top_level| SIZE_LIMITS_BYTES.iter().find(|(t, _)| *t == top_level))
86        .map(|(_, size)| *size)
87        .unwrap_or(DEFAULT_SIZE_LIMIT_BYTES)
88}