Skip to main content

tablo_core/
limits.rs

1//! `Limits` — the bounds a panel holds each request to.
2
3use topcoat::{
4    context::Cx,
5    router::{Body, BodyLimit, IntoPath, LayerFn, LayerFuture, Next},
6};
7
8/// The bounds a panel holds each request to, which
9/// [`Panel::limits`](crate::Panel::limits) sets. Each one keeps a request to a known size: a
10/// request past one is refused whole, never served in part.
11///
12/// ```rust
13/// # use tablo_core::{Limits, Panel};
14/// let panel = Panel::new("admin").limits(
15///     Limits::default()
16///         .export_rows(50_000)
17///         .body_bytes(50 * 1024 * 1024),
18/// );
19/// # let _ = panel;
20/// ```
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct Limits {
23    pub(crate) export_rows: usize,
24    pub(crate) import_rows: usize,
25    pub(crate) bulk_records: usize,
26    pub(crate) body_bytes: usize,
27}
28
29impl Default for Limits {
30    /// 10,000 exported rows, 1,000 imported rows, 400 selected records and a 10 MiB body.
31    fn default() -> Self {
32        Self {
33            export_rows: 10_000,
34            import_rows: 1_000,
35            bulk_records: 400,
36            body_bytes: 10 * 1024 * 1024,
37        }
38    }
39}
40
41impl Limits {
42    /// The most rows a CSV export holds; a list with more answers 413 rather than a partial file.
43    /// Each row the export counts is read twice, once to count and once to stream.
44    #[must_use]
45    pub fn export_rows(mut self, rows: usize) -> Self {
46        self.export_rows = rows.max(1);
47        self
48    }
49
50    /// The most rows one CSV import creates. Every row writes in the request's one transaction,
51    /// so a longer file is refused whole.
52    #[must_use]
53    pub fn import_rows(mut self, rows: usize) -> Self {
54        self.import_rows = rows.max(1);
55        self
56    }
57
58    /// The most records one bulk action runs on. The selection loads as one `IN` list, which
59    /// some databases bound: SQLite takes 32,766 values. A selection of every matching record
60    /// past it writes nothing and asks the user to narrow the list.
61    #[must_use]
62    pub fn bulk_records(mut self, records: usize) -> Self {
63        self.bulk_records = records.max(1);
64        self
65    }
66
67    /// The most bytes a request body under the panel's prefix holds, uploaded files included; a
68    /// larger body answers 413. A file field's `max_bytes` must stay below it.
69    #[must_use]
70    pub fn body_bytes(mut self, bytes: usize) -> Self {
71        self.body_bytes = bytes.max(1);
72        self
73    }
74}
75
76impl Limits {
77    /// A layer that caps the bodies of the routes under `prefix` at the body limit. The cap is
78    /// the layer's context, so a route's own cap, such as an auth form's, shadows it.
79    pub(crate) fn body_cap(&self, prefix: impl IntoPath) -> LayerFn {
80        LayerFn::new(Some(prefix), pass_through).with(BodyLimit::max(self.body_bytes))
81    }
82}
83
84fn pass_through<'a>(cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
85    next.run(cx, body)
86}
87
88/// The limits of the request's panel, which entering it puts on the context; the defaults
89/// outside one.
90pub(crate) fn of(cx: &Cx) -> Limits {
91    topcoat::context::try_context::<Limits>(cx)
92        .copied()
93        .unwrap_or_default()
94}