Skip to main content

tablo_core/schema/fields/builders/
files.rs

1//! The file builder's rules: the extensions a file field takes, its most bytes, a list's most
2//! files, and whether it shows them as images.
3
4use super::{super::ControlKind, FileField};
5
6impl<F> FileField<F> {
7    /// Refuse a file whose name does not end in one of `extensions`, listed without the dot and
8    /// matched without case, and hint them to the browser's file picker through `accept`.
9    ///
10    /// ```rust
11    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
12    /// # struct Doc { #[key] #[auto] id: uuid::Uuid, cover: String }
13    /// # use tablo_core::{Field, lens};
14    /// Field::file(lens!(Doc.cover))
15    ///     .extensions(["png", "jpg", "jpeg", "webp"])
16    ///     .max_bytes(2 * 1024 * 1024);
17    /// ```
18    ///
19    /// - The extension is the check, not the bytes. A served upload's type comes from its
20    ///   extension, under `X-Content-Type-Options: nosniff`, so the extension is what decides how a
21    ///   browser treats the file: sniffing the content would pass PNG bytes named `page.html`.
22    ///   Nothing checks that the bytes are what the extension says; parse them in the
23    ///   [`Uploader`](crate::Uploader) when that matters, as before resizing an image.
24    /// - The name checked is the sanitized one the [`Uploader`](crate::Uploader) receives. A custom
25    ///   uploader that names the stored file otherwise should keep its extension.
26    /// - Only the server's check refuses: `accept` is a hint the browser's picker may ignore. A
27    ///   refused file is not stored, and the error renders on the field.
28    /// - A refused submission carries a stored upload back through `keep_<field>`, signed for the
29    ///   field, so a carry names only a file this field's rules passed; a carried path whose
30    ///   extension the field does not take is dropped too, as one the uploader does not hold is. A
31    ///   value the record already stores is not checked again.
32    /// - Mounting refuses an empty list, and an extension that is not letters and digits.
33    pub fn extensions<I>(mut self, extensions: I) -> Self
34    where
35        I: IntoIterator,
36        I::Item: AsRef<str>,
37    {
38        let extensions = extensions
39            .into_iter()
40            .map(|ext| {
41                let ext = ext.as_ref().trim();
42                ext.strip_prefix('.').unwrap_or(ext).to_ascii_lowercase()
43            })
44            .collect();
45        if let ControlKind::File(file) = &mut self.0.control {
46            file.extensions = Some(extensions);
47        }
48        self
49    }
50
51    /// Refuse a file larger than `max_bytes`; the error spells the size in KiB or MiB when it
52    /// divides evenly. Only the server checks it, before the [`Uploader`](crate::Uploader) stores
53    /// the file; a carried upload passed it when it was stored. Mounting refuses a size at or
54    /// above the panel's [`Limits::body_bytes`](crate::Limits::body_bytes), which refuses a larger
55    /// body first.
56    pub fn max_bytes(mut self, max_bytes: usize) -> Self {
57        if let ControlKind::File(file) = &mut self.0.control {
58            file.max_bytes = Some(max_bytes);
59        }
60        self
61    }
62
63    /// Show each stored file as a thumbnail beside its name, and each file the user picks or drops
64    /// as one before the form saves it.
65    ///
66    /// ```rust
67    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
68    /// # struct Product { #[key] #[auto] id: uuid::Uuid, gallery: Vec<String> }
69    /// # use tablo_core::{Field, lens};
70    /// Field::files(lens!(Product.gallery))
71    ///     .image()
72    ///     .extensions(["png", "jpg", "jpeg", "webp"])
73    ///     .max_files(5);
74    /// ```
75    ///
76    /// - It changes what the field shows, not what it takes: pair it with
77    ///   [`extensions`](Self::extensions), or a file that is no image shows as a broken one. The
78    ///   thumbnail loads from the same signed link as the file's name.
79    /// - Only a stored value that may become a link has a thumbnail: a rooted path or an `http(s)`
80    ///   URL.
81    pub fn image(mut self) -> Self {
82        if let ControlKind::File(file) = &mut self.0.control {
83            file.image = true;
84        }
85        self
86    }
87
88    /// Refuse a submission leaving a [`Field::files`](crate::Field::files) list with more than
89    /// `count` files: the stored ones it keeps and the ones it adds. The error renders on the
90    /// field, and the files the submission stored are carried to the next one, so the user
91    /// removes some and saves again; one choosing more than `count` files at once stores none.
92    /// Mounting refuses it on a field over one path.
93    pub fn max_files(mut self, count: usize) -> Self {
94        if let ControlKind::File(file) = &mut self.0.control {
95            file.max_files = Some(count);
96        }
97        self
98    }
99}