An upload that did not arrive says why
Test / test (pull_request) Successful in 1m21s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-30 21:08:28 +02:00
co-authored by Claude Opus 5.5
parent 95bffc221b
commit db89ea6400
4 changed files with 57 additions and 4 deletions
+4
View File
@@ -4,6 +4,10 @@ One line per change, grouped by the release that shipped it. Newest
first. A release is a Cargo.toml bump plus a `v<version>` tag; the first. A release is a Cargo.toml bump plus a `v<version>` tag; the
content repos pin the tag they run (see the end of this file). content repos pin the tag they run (see the end of this file).
## Unreleased
- An upload that did not arrive says why: larger than 20 MB (checked in the browser before anything is sent), a site that takes no files (no Garage configured), not allowed from here, or anything else. It used to say "try again, or with a smaller file" whatever the reason, which sent people looking for a smaller file when the site had no storage at all.
## 0.6.7 (2026-09-30) ## 0.6.7 (2026-09-30)
- `leaves_after: { minutes: 10 }` on a state: a record that has been there that long leaves its bucket's lists (days, hours and minutes add up; swept every minute; the event log keeps it). Trainer: `leaves_after` per stage on a need. A Klingenberg Bygg card taken off the front page leaves the office's list ten minutes later instead of standing there greyed out. - `leaves_after: { minutes: 10 }` on a state: a record that has been there that long leaves its bucket's lists (days, hours and minutes add up; swept every minute; the event log keeps it). Trainer: `leaves_after` per stage on a need. A Klingenberg Bygg card taken off the front page leaves the office's list ten minutes later instead of standing there greyed out.
+41 -1
View File
@@ -1310,6 +1310,12 @@ fn AlternativeCard(
}; };
let Some(files) = input.files() else { continue }; let Some(files) = input.files() else { continue };
let Some(file) = files.get(0) else { continue }; let Some(file) = files.get(0) else { continue };
// Too big is known before a byte is sent.
if file.size() > MAX_UPLOAD_BYTES as f64 {
send_error.set(Some(t(&lang().get_untracked(), "upload_too_large").to_string()));
sending.set(false);
return;
}
match upload_file(&file, &question_id_for_submit, &alt_name_for_submit).await { match upload_file(&file, &question_id_for_submit, &alt_name_for_submit).await {
Ok(key) => { Ok(key) => {
map.insert(name.clone(), serde_json::Value::String(key)); map.insert(name.clone(), serde_json::Value::String(key));
@@ -1319,7 +1325,8 @@ fn AlternativeCard(
// say so. // say so.
Err(e) => { Err(e) => {
leptos::logging::error!("upload of {name} failed: {e:?}"); leptos::logging::error!("upload of {name} failed: {e:?}");
send_error.set(Some(t(&lang().get_untracked(), "upload_failed").to_string())); let status = e.as_string().and_then(|m| m.rsplit(' ').next().and_then(|n| n.parse().ok()));
send_error.set(Some(t(&lang().get_untracked(), upload_refusal(status)).to_string()));
sending.set(false); sending.set(false);
return; return;
} }
@@ -2283,6 +2290,24 @@ pub fn uploaded_filename(value: &str) -> Option<&str> {
rest.strip_prefix('-').filter(|name| shaped && !name.is_empty()) rest.strip_prefix('-').filter(|name| shaped && !name.is_empty())
} }
/// The largest file one upload may carry. Here, not in `upload.rs`,
/// so the browser checks the same number before it sends anything.
pub const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024;
/// Why an upload did not arrive, in words a person can act on, from
/// the status `POST /upload` answered with (see `upload.rs`): too big,
/// a site that takes no files, a page they may not send from, or
/// anything else.
#[cfg_attr(not(feature = "hydrate"), allow(dead_code))]
fn upload_refusal(status: Option<u16>) -> &'static str {
match status {
Some(413) => "upload_too_large",
Some(503) => "upload_unavailable",
Some(403) => "upload_forbidden",
_ => "upload_failed",
}
}
/// What a refused server call says, without the wrapping a person /// What a refused server call says, without the wrapping a person
/// has no use for. /// has no use for.
fn server_message(e: &ServerFnError) -> String { fn server_message(e: &ServerFnError) -> String {
@@ -3241,6 +3266,21 @@ mod responsible_tests {
mod picture_tests { mod picture_tests {
use super::*; use super::*;
#[test]
fn an_upload_refusal_says_which_it_was() {
assert_eq!(upload_refusal(Some(413)), "upload_too_large");
assert_eq!(upload_refusal(Some(503)), "upload_unavailable");
assert_eq!(upload_refusal(Some(403)), "upload_forbidden");
assert_eq!(upload_refusal(Some(502)), "upload_failed");
assert_eq!(upload_refusal(None), "upload_failed");
for lang in ["en", "no"] {
for status in [Some(413), Some(503), Some(403), None] {
let key = upload_refusal(status);
assert_ne!(crate::i18n::t(lang, key), key, "{lang} has words for {key}");
}
}
}
#[test] #[test]
fn a_record_with_a_picture_is_shown_by_it() { fn a_record_with_a_picture_is_shown_by_it() {
assert!(is_picture("Portrett.JPG") && is_picture("a.webp") && !is_picture("rapport.pdf")); assert!(is_picture("Portrett.JPG") && is_picture("a.webp") && !is_picture("rapport.pdf"));
+11 -2
View File
@@ -24,7 +24,10 @@ fn lookup(lang: &str, key: &str) -> Option<&'static str> {
("en", "sign_in") => "Sign in", ("en", "sign_in") => "Sign in",
("en", "sign_out") => "Sign out", ("en", "sign_out") => "Sign out",
("en", "nothing_here_yet") => "Nothing here yet.", ("en", "nothing_here_yet") => "Nothing here yet.",
("en", "upload_failed") => "The file could not be sent, so nothing was. Try again, or with a smaller file.", ("en", "upload_failed") => "The file did not arrive, so nothing was sent. Try again.",
("en", "upload_too_large") => "The file is larger than 20 MB, so nothing was sent. Choose a smaller one, or a picture taken at a lower resolution.",
("en", "upload_unavailable") => "This site cannot take files at the moment, so nothing was sent. Let whoever runs the site know.",
("en", "upload_forbidden") => "Files cannot be sent from here by you, so nothing was sent. Are you signed in?",
("en", "current_file") => "Now:", ("en", "current_file") => "Now:",
("en", "keep_file") => "Choose a new one to replace it, or leave it as it is.", ("en", "keep_file") => "Choose a new one to replace it, or leave it as it is.",
("en", "nothing_here") => "Nothing here", ("en", "nothing_here") => "Nothing here",
@@ -57,7 +60,10 @@ fn lookup(lang: &str, key: &str) -> Option<&'static str> {
("no", "sign_in") => "Logg inn", ("no", "sign_in") => "Logg inn",
("no", "sign_out") => "Logg ut", ("no", "sign_out") => "Logg ut",
("no", "nothing_here_yet") => "Ingenting her ennå.", ("no", "nothing_here_yet") => "Ingenting her ennå.",
("no", "upload_failed") => "Filen kom ikke fram, så ingenting ble sendt. Prøv igjen, eller med en mindre fil.", ("no", "upload_failed") => "Filen kom ikke fram, så ingenting ble sendt. Prøv igjen.",
("no", "upload_too_large") => "Filen er større enn 20 MB, så ingenting ble sendt. Velg en mindre fil, eller et bilde med lavere oppløsning.",
("no", "upload_unavailable") => "Siden kan ikke ta imot filer akkurat nå, så ingenting ble sendt. Si fra til den som har siden.",
("no", "upload_forbidden") => "Du kan ikke sende filer herfra, så ingenting ble sendt. Er du logget inn?",
("no", "current_file") => "Nå:", ("no", "current_file") => "Nå:",
("no", "keep_file") => "Velg en ny for å bytte, eller la den være.", ("no", "keep_file") => "Velg en ny for å bytte, eller la den være.",
("no", "nothing_here") => "Ingenting her", ("no", "nothing_here") => "Ingenting her",
@@ -109,6 +115,9 @@ mod tests {
"sign_out", "sign_out",
"nothing_here_yet", "nothing_here_yet",
"upload_failed", "upload_failed",
"upload_too_large",
"upload_unavailable",
"upload_forbidden",
"current_file", "current_file",
"keep_file", "keep_file",
"nothing_here", "nothing_here",
+1 -1
View File
@@ -15,7 +15,7 @@ use serde::Serialize;
use crate::server::AppState; use crate::server::AppState;
pub const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024; pub const MAX_UPLOAD_BYTES: usize = crate::app::MAX_UPLOAD_BYTES;
#[derive(Clone)] #[derive(Clone)]
pub struct Garage { pub struct Garage {