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.
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.
Whether a picture of this name is kept here. An unsafe name is simply not there.
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.
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}