1//! Keeping the service's copy of the parents' log as long as the household says: the same setting a device applies to its 2//! own (`HouseholdConfig::log_retention_days`; none, the default, keeps everything). The lines older than that are 3//! emptied (never removed: the counts devices sync by are made of the lines' numbers), the pictures only they showed are 4//! deleted, and a `Cleared` line is written to the log as the device `service`, which every device pulls like any other 5//! line, so nothing leaves the log unannounced. 6 7use log::{error, info, warn}; 8use whiskers_core::{Entry, Event, Log}; 9use whiskers_ports::Hub; 10use whiskers_store::{SqliteHouseholdHub, SqliteLog, Store}; 11 12/// The name the service writes its own lines under. 13pub const SERVICE_DEVICE: &str = "service"; 14 15/// Clears what the household's setting says is too old, as of `now_ms`. Returns how many lines were emptied. 16pub fn apply_retention(store: &Store, household: &SqliteHouseholdHub, now_ms: u64) -> usize { 17 let Ok(doc) = whiskers_ports::run_ready(household.current()) else { 18 error!("retention: the household's choices cannot be read"); 19 return 0; 20 }; 21 let Some(days) = doc.config().log_retention_days else { return 0 }; 22 let cutoff = now_ms.saturating_sub(u64::from(days) * 86_400_000); 23 let log = SqliteLog::new(store.clone(), SERVICE_DEVICE); 24 match log.expire_before(cutoff) { 25 Ok(0) => 0, 26 Ok(n) => { 27 info!("retention: {n} line(s) older than {days} day(s) were emptied"); 28 if let Err(e) = log.clone().append(&Entry::new(now_ms, Event::Cleared { lines: n, older_than_ms: cutoff })) { 29 warn!("retention: {n} line(s) were emptied but the record of it could not be written: {}", e.0); 30 } 31 n 32 } 33 Err(e) => { 34 error!("retention: the log could not be cleared: {}", e.0); 35 0 36 } 37 } 38} 39 40#[cfg(test)] 41mod tests { 42 use super::*; 43 use whiskers_core::{HouseholdConfig, TimeKeeper}; 44 use whiskers_store::SqliteHousehold; 45 46 const DAY: u64 = 86_400_000; 47 48 #[test] 49 fn the_service_clears_its_copy_only_when_the_household_says_so_and_says_it_did() { 50 let store = Store::open_in_memory().unwrap(); 51 let hub = SqliteHouseholdHub::open(store.clone()); 52 let mut phone = SqliteLog::new(store.clone(), "phone"); 53 phone.append(&Entry::new(10 * DAY, Event::Heard { text: "old".into(), pictures: vec![] })).unwrap(); 54 phone.append(&Entry::new(99 * DAY, Event::Heard { text: "new".into(), pictures: vec![] })).unwrap(); 55 let now = 100 * DAY; 56 57 assert_eq!(apply_retention(&store, &hub, now), 0, "no setting keeps everything"); 58 59 let mut keeper = TimeKeeper::new(Box::new(SqliteHousehold::new(Store::open_in_memory().unwrap())), "phone"); 60 keeper.set_config(HouseholdConfig { log_retention_days: Some(30), ..HouseholdConfig::default() }, 5); 61 whiskers_ports::run_ready(hub.merge(keeper.household())).unwrap(); 62 assert_eq!(apply_retention(&store, &hub, now), 1); 63 assert_eq!(apply_retention(&store, &hub, now), 0, "once"); 64 let phone_lines = phone.own_entries().unwrap(); 65 assert_eq!(phone_lines[0].event, Event::Expired); 66 assert!(matches!(&phone_lines[1].event, Event::Heard { text, .. } if text == "new")); 67 let said = SqliteLog::new(store, SERVICE_DEVICE).own_entries().unwrap(); 68 assert!(matches!(said[0].event, Event::Cleared { lines: 1, .. }), "{said:?}"); 69 } 70}