Pictures as blobs in picture, under the names every device knows them by. A picture is never replaced: the same name is the same picture.

4use std::time::{SystemTime, UNIX_EPOCH};
6use ::log::{debug, error, trace, warn};
7use rusqlite::{OptionalExtension, params};
8use whiskers_core::{Image, PictureError, PictureId, Pictures};
9use whiskers_ports::{PictureShelf, Put, SafePictureId, StoreError};
10
11use crate::{DbError, Store};
12
13fn extension(media_type: &str) -> &'static str {
14    match media_type {
15        "image/jpeg" => "jpg",
16        "image/png" => "png",
17        "image/webp" => "webp",
18        "image/gif" => "gif",
19        _ => "img",
20    }
21}

Keeps bytes under name unless something is kept under it already. Returns whether it was kept.

24pub(crate) fn put(store: &Store, name: &str, bytes: &[u8]) -> Result<bool, DbError> {
25    let conn = store.lock();
26    let n = conn.execute("INSERT OR IGNORE INTO picture (id, bytes) VALUES (?1, ?2)", params![name, bytes])?;
27    Ok(n == 1)
28}
30pub(crate) fn get(store: &Store, name: &str) -> Result<Option<Vec<u8>>, DbError> {
31    Ok(store.lock().query_row("SELECT bytes FROM picture WHERE id = ?1", [name], |r| r.get(0)).optional()?)
32}
33
34pub(crate) fn has(store: &Store, name: &str) -> Result<bool, DbError> {
35    Ok(store.lock().query_row("SELECT 1 FROM picture WHERE id = ?1", [name], |_| Ok(())).optional()?.is_some())
36}

A device's pictures: what she showed Whiskers, and the ones that came with a memory from another device.

39#[derive(Clone)]
40pub struct SqlitePictures {
41    store: Store,
42}
44impl SqlitePictures {
45    pub fn new(store: Store) -> Self {
46        Self { store }
47    }

Whether a picture of this name is kept here. An unsafe name is simply not there.

50    pub fn has(&self, id: &PictureId) -> bool {
51        id.is_safe() && has(&self.store, &id.0).unwrap_or_else(|e| {
52            error!("picture {} could not be looked for: {e}", id.0);
53            false
54        })
55    }
57    pub fn read(&self, id: &PictureId) -> Option<Vec<u8>> {
58        if !id.is_safe() {
59            warn!("refusing to read a picture with an unsafe name ({} bytes)", id.0.len());
60            return None;
61        }
62        match get(&self.store, &id.0) {
63            Ok(b) => {
64                trace!("read picture {} ({} bytes)", id.0, b.as_ref().map_or(0, Vec::len));
65                b
66            }
67            Err(e) => {
68                error!("picture {} not readable: {e}", id.0);
69                None
70            }
71        }
72    }

Keeps a picture that came from another device under the name it was given there. Names are made from the time and a counter, so the same name means the same picture.

76    pub fn store(&self, id: &PictureId, bytes: &[u8]) -> Result<(), PictureError> {
77        if !id.is_safe() {
78            warn!("refusing to store a picture with an unsafe name ({} chars)", id.0.len());
79            return Err(PictureError(format!("not a picture name: {:?}", id.0)));
80        }
81        match put(&self.store, &id.0, bytes) {
82            Ok(true) => debug!("storing picture {} ({} bytes)", id.0, bytes.len()),
83            Ok(false) => trace!("picture {} already kept", id.0),
84            Err(e) => {
85                error!("storing picture {} failed: {e}", id.0);
86                return Err(PictureError(e.to_string()));
87            }
88        }
89        Ok(())
90    }
91}
93impl Pictures for SqlitePictures {
94    fn save(&mut self, image: &Image) -> Result<PictureId, PictureError> {
95        let ms = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_millis());
96        for n in 0..10_000u32 {
97            let name = format!("{ms:016}-{n:04}.{}", extension(&image.media_type));
98            match put(&self.store, &name, &image.bytes) {
99                Ok(true) => {
100                    debug!("saved picture {name} ({} bytes, {})", image.bytes.len(), image.media_type);
101                    return Ok(PictureId(name));
102                }
103                Ok(false) => continue,
104                Err(e) => {
105                    error!("saving picture {name} failed: {e}");
106                    return Err(PictureError(e.to_string()));
107                }
108            }
109        }
110        error!("no free picture name after 10000 tries");
111        Err(PictureError("could not find a free picture name".into()))
112    }
113}

The service's master copy of the pictures.

116#[derive(Clone)]
117pub struct SqlitePictureShelf {
118    store: Store,
119}
121impl SqlitePictureShelf {
122    pub fn new(store: Store) -> Self {
123        Self { store }
124    }
125}
126
127impl PictureShelf for SqlitePictureShelf {
128    async fn has(&self, id: &SafePictureId) -> Result<bool, StoreError> {
129        has(&self.store, id.as_str()).map_err(|e| {
130            error!("picture {} could not be looked for: {e}", id.as_str());
131            StoreError::Unavailable
132        })
133    }
134
135    async fn put(&self, id: &SafePictureId, bytes: &[u8]) -> Result<Put, StoreError> {
136        match put(&self.store, id.as_str(), bytes) {
137            Ok(true) => {
138                debug!("picture {} stored ({} bytes)", id.as_str(), bytes.len());
139                Ok(Put::Stored)
140            }
141            Ok(false) => {
142                trace!("picture {} already kept", id.as_str());
143                Ok(Put::AlreadyKept)
144            }
145            Err(e) => {
146                error!("picture {} not written: {e}", id.as_str());
147                Err(StoreError::Unavailable)
148            }
149        }
150    }
151
152    async fn get(&self, id: &SafePictureId) -> Result<Option<Vec<u8>>, StoreError> {
153        get(&self.store, id.as_str()).map_err(|e| {
154            error!("picture {} unreadable: {e}", id.as_str());
155            StoreError::Unavailable
156        })
157    }
158}