1//! Pictures as blobs in `picture`, under the names every device knows them by. A picture is never replaced: 2//! the same name is the same picture. 3 4use std::time::{SystemTime, UNIX_EPOCH}; 5 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} 22 23/// 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} 29 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} 37 38/// 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} 43 44impl SqlitePictures { 45 pub fn new(store: Store) -> Self { 46 Self { store } 47 } 48 49 /// 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 } 56 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 } 73 74 /// Keeps a picture that came from another device under the name it was given there. Names are made from the 75 /// 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} 92 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} 114 115/// The service's master copy of the pictures. 116#[derive(Clone)] 117pub struct SqlitePictureShelf { 118 store: Store, 119} 120 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}