umbral_admin/files.rs
1//! File descriptor helpers — MIME / extension → preview kind, plus the
2//! `serde_json::Value` shape the file-preview macros consume.
3//!
4//! ⚠ A first-class `File` / `Image` ORM field type is deferred. For now
5//! you store file paths in `Text` columns and emit a descriptor JSON
6//! string for the admin to render. This module owns the descriptor
7//! shape so other plugins (and user code) can construct one without
8//! re-implementing the MIME table.
9
10/// Resolve the `preview_kind` from a MIME type and file extension.
11///
12/// Returns a `&'static str` matching one of: `image`, `pdf`, `video`,
13/// `audio`, `text`, `code`, `download`. Extension wins over MIME so a
14/// `text/plain; charset=utf-8` `.py` file resolves to `code`, not `text`.
15pub fn resolve_preview_kind(mime: &str, filename: &str) -> &'static str {
16 let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase();
17 // Check extension-based code/text first so that e.g. "text/plain; charset=utf-8"
18 // on a .py file resolves to "code" rather than "text".
19 match ext.as_str() {
20 "rs" | "py" | "js" | "ts" | "jsx" | "tsx" | "json" | "toml" | "yaml" | "yml" | "html"
21 | "css" | "sql" | "sh" | "bash" | "zsh" | "fish" | "md" | "mdx" => return "code",
22 "txt" | "log" => return "text",
23 _ => {}
24 }
25 // Then MIME-based rules.
26 if mime.starts_with("image/") {
27 return "image";
28 }
29 if mime == "application/pdf" {
30 return "pdf";
31 }
32 if mime.starts_with("video/") {
33 return "video";
34 }
35 if mime.starts_with("audio/") {
36 return "audio";
37 }
38 if mime.starts_with("text/plain") {
39 return "text";
40 }
41 "download"
42}
43
44/// Build a file descriptor JSON value.
45///
46/// `url` is the pre-signed / auth-checked URL the admin should embed;
47/// `thumbnail_url` is optional and only set for the `image` kind where
48/// a thumbnail has been generated.
49pub fn file_descriptor(
50 filename: &str,
51 size: u64,
52 mime: &str,
53 url: &str,
54 thumbnail_url: Option<&str>,
55) -> serde_json::Value {
56 let preview_kind = resolve_preview_kind(mime, filename);
57 let language: Option<&str> = if preview_kind == "code" {
58 Some(filename.rsplit('.').next().unwrap_or("text"))
59 } else {
60 None
61 };
62 serde_json::json!({
63 "filename": filename,
64 "size": size,
65 "mime": mime,
66 "preview_kind": preview_kind,
67 "url": url,
68 "thumbnail_url": thumbnail_url,
69 "language": language,
70 })
71}