/picture/missing, /picture/put, /picture/get: the master copy of the pictures. Devices ask which of theirs are missing here, send those, and fetch the ones they lack.

4use ::log::{debug, error, info, trace, warn};
5use base64::Engine as _;
6use serde_json::{Value, json};
7use whiskers_ports::{HasPictures, PictureShelf as _, Put, SafePictureId};
9use super::{Route, sealed};
10use crate::Response;

POST /picture/missing, /picture/put and /picture/get. Needs the picture shelf.

13pub struct Pictures;
15impl sealed::Sealed for Pictures {}
16
17impl<C: HasPictures> Route<C> for Pictures {
18    const PATHS: &'static [&'static str] = &["/picture/missing", "/picture/put", "/picture/get"];
19    async fn answer(&self, adapter: &C, route: &str, body: &str) -> Response {
20        pictures(adapter, route, body).await
21    }
22}
23
24fn reply(status: u16, json: Value) -> Response {
25    Response::json(status, json.to_string())
26}
27
28fn failed(route: &str, e: &dyn std::fmt::Display) -> Response {
29    error!("{route}: the shelf failed: {e}");
30    reply(500, json!({ "error": e.to_string() }))
31}
32
33async fn pictures<C: HasPictures>(adapter: &C, route: &str, body: &str) -> Response {
34    let Ok(v) = serde_json::from_str::<Value>(body) else {
35        warn!("{route}: the body ({} bytes) is not JSON", body.len());
36        return reply(400, json!({ "error": "bad json" }));
37    };
38    match route {
39        "/picture/missing" => pictures_missing(adapter, &v).await,
40        "/picture/put" => picture_put(adapter, &v).await,
41        _ => picture_get(adapter, &v).await,
42    }
43}

Which of the named pictures are not kept here. A name that is not a picture name is not reported as missing: it could never be sent.

47async fn pictures_missing<C: HasPictures>(adapter: &C, v: &Value) -> Response {
48    let ids = v["ids"].as_array().cloned().unwrap_or_default();
49    let mut missing: Vec<String> = Vec::new();
50    for name in ids.iter().filter_map(Value::as_str) {
51        let Ok(id) = SafePictureId::parse(name) else {
52            trace!("pictures: a name of {} chars is not a picture name; not asked about", name.len());
53            continue;
54        };
55        match adapter.pictures().has(&id).await {
56            Ok(true) => {}
57            Ok(false) => missing.push(name.to_owned()),
58            Err(e) => return failed("/picture/missing", &e),
59        }
60    }
61    debug!("pictures: {} asked about, {} missing", ids.len(), missing.len());
62    reply(200, json!({ "missing": missing }))
63}
65async fn picture_put<C: HasPictures>(adapter: &C, v: &Value) -> Response {
66    let (Some(name), Some(data)) = (v["id"].as_str(), v["data_base64"].as_str()) else {
67        warn!("picture put without an id and data");
68        return reply(400, json!({ "error": "id and data_base64" }));
69    };
70    let Ok(id) = SafePictureId::parse(name) else {
71        warn!("picture put refused: unsafe name of {} chars", name.len());
72        return reply(400, json!({ "error": "not a picture name" }));
73    };
74    let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(data) else {
75        warn!("picture put {name} refused: bad base64 ({} chars)", data.len());
76        return reply(400, json!({ "error": "bad base64" }));
77    };
78    match adapter.pictures().put(&id, &bytes).await {
79        Ok(Put::Stored) => {
80            info!("picture {name} stored ({} bytes)", bytes.len());
81            reply(200, json!({ "ok": true }))
82        }
83        Ok(Put::AlreadyKept) => {
84            trace!("picture {name} already kept");
85            reply(200, json!({ "ok": true }))
86        }
87        Err(e) => failed("/picture/put", &e),
88    }
89}
90
91async fn picture_get<C: HasPictures>(adapter: &C, v: &Value) -> Response {
92    let Some(id) = v["id"].as_str().and_then(|n| SafePictureId::parse(n).ok()) else {
93        warn!("picture get refused: no safe name");
94        return reply(400, json!({ "error": "not a picture name" }));
95    };
96    match adapter.pictures().get(&id).await {
97        Ok(Some(bytes)) => {
98            debug!("picture {} served ({} bytes)", id.as_str(), bytes.len());
99            reply(200, json!({ "data_base64": base64::engine::general_purpose::STANDARD.encode(bytes) }))
100        }
101        Ok(None) => {
102            debug!("picture {} not found", id.as_str());
103            reply(404, json!({ "error": "no such picture" }))
104        }
105        Err(e) => failed("/picture/get", &e),
106    }
107}