Skip to main content

tablo_core/schema/fields/builders/
files.rs

1//! The file builder's rules: the extensions a file field takes, and its most bytes.
2
3use super::{super::ControlKind, FileField};
4
5impl<F> FileField<F> {
6    /// Refuse a file whose name does not end in one of `extensions`, listed without the dot and
7    /// matched without case, and hint them to the browser's file picker through `accept`.
8    ///
9    /// ```rust
10    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
11    /// # struct Doc { #[key] #[auto] id: uuid::Uuid, cover: String }
12    /// # use tablo_core::{Field, lens};
13    /// Field::file(lens!(Doc.cover))
14    ///     .extensions(["png", "jpg", "jpeg", "webp"])
15    ///     .max_bytes(2 * 1024 * 1024);
16    /// ```
17    ///
18    /// - The extension is the check, not the bytes. A served upload's type comes from its
19    ///   extension, under `X-Content-Type-Options: nosniff`, so the extension is what decides how a
20    ///   browser treats the file: sniffing the content would pass PNG bytes named `page.html`.
21    ///   Nothing checks that the bytes are what the extension says; parse them in the
22    ///   [`Uploader`](crate::Uploader) when that matters, as before resizing an image.
23    /// - The name checked is the sanitized one the [`Uploader`](crate::Uploader) receives. A custom
24    ///   uploader that names the stored file otherwise should keep its extension.
25    /// - Only the server's check refuses: `accept` is a hint the browser's picker may ignore. A
26    ///   refused file is not stored, and the error renders on the field.
27    /// - A refused submission carries a stored upload back through `keep_<field>`, signed for the
28    ///   field, so a carry names only a file this field's rules passed; a carried path whose
29    ///   extension the field does not take is dropped too, as one the uploader does not hold is. A
30    ///   value the record already stores is not checked again.
31    /// - Mounting refuses an empty list, and an extension that is not letters and digits.
32    pub fn extensions<I>(mut self, extensions: I) -> Self
33    where
34        I: IntoIterator,
35        I::Item: AsRef<str>,
36    {
37        let extensions = extensions
38            .into_iter()
39            .map(|ext| {
40                let ext = ext.as_ref().trim();
41                ext.strip_prefix('.').unwrap_or(ext).to_ascii_lowercase()
42            })
43            .collect();
44        if let ControlKind::File(file) = &mut self.0.control {
45            file.extensions = Some(extensions);
46        }
47        self
48    }
49
50    /// Refuse a file larger than `max_bytes`; the error spells the size in KiB or MiB when it
51    /// divides evenly. Only the server checks it, before the [`Uploader`](crate::Uploader) stores
52    /// the file; a carried upload passed it when it was stored. Mounting refuses a size at or
53    /// above the 10 MiB form-body cap, which refuses a larger body first.
54    pub fn max_bytes(mut self, max_bytes: usize) -> Self {
55        if let ControlKind::File(file) = &mut self.0.control {
56            file.max_bytes = Some(max_bytes);
57        }
58        self
59    }
60}