feat: gsmol-compatible backup export and import behind the store feature

This commit is contained in:
randogoth 2026-09-29 00:23:04 +03:00
parent 96d2412370
commit 4a780ed648
6 changed files with 528 additions and 17 deletions

View file

@ -283,6 +283,21 @@ impl Store {
.and_then(|k| k.try_into().ok()))
}
/// Every pin, host and key, ordered by host — the export's server map.
pub fn pins(&self) -> Result<Vec<(String, [u8; KEY_LEN])>, Error> {
let db = self.db();
let mut stmt = db.prepare("SELECT host, static FROM servers ORDER BY host")?;
let rows = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows
.into_iter()
.filter_map(|(host, key)| Some((host, key.try_into().ok()?)))
.collect())
}
pub fn pin_server(&self, host: &str, key: &[u8; KEY_LEN]) -> Result<(), Error> {
self.db()
.execute(
@ -541,6 +556,21 @@ impl Store {
Ok(())
}
/// Whether a message id is already stored in `folder` ("inbox" or "sent"),
/// so an import can skip rather than re-store.
pub fn stored(&self, folder: &str, id: &[u8; ID_LEN]) -> Result<bool, Error> {
let table = match folder {
"sent" => "sent",
_ => "inbox",
};
Ok(self
.one::<i64>(
&format!("SELECT 1 FROM {table} WHERE id = ?1"),
&[id],
)?
.is_some())
}
/// One folder, ordered by arrival or sending time. `folder` is "inbox",
/// "requests", "sent" or "all".
pub fn mail(&self, folder: &str) -> Result<Vec<Stored>, Error> {
@ -597,13 +627,16 @@ mod tests {
core::array::from_fn(|i| i as u8)
}
/// A fresh scratch path per call: a monotonic counter, so two tests or
/// two runs cannot collide the way pid-and-second can.
fn scratch_path(tag: &str) -> std::path::PathBuf {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
std::env::temp_dir().join(format!("fumi-store-{tag}-{n}-{}.db", std::process::id()))
}
fn temp_store(tag: &str) -> Store {
let path = std::env::temp_dir().join(format!(
"fumi-store-{tag}-{}-{}.db",
std::process::id(),
now()
));
Store::open(&path).expect("open store")
Store::open(&scratch_path(tag)).expect("open store")
}
#[test]
@ -761,11 +794,7 @@ mod tests {
#[test]
fn schema_is_versioned_and_refuses_a_newer_store() {
let path = std::env::temp_dir().join(format!(
"fumi-store-schema-{}-{}.db",
std::process::id(),
now()
));
let path = scratch_path("schema");
drop(Store::open(&path).expect("open store"));
let db = Connection::open(&path).unwrap();
let version: i32 = db.query_row("PRAGMA user_version", [], |row| row.get(0)).unwrap();
@ -783,11 +812,7 @@ mod tests {
#[test]
fn v1_store_migrates_at_open() {
let path = std::env::temp_dir().join(format!(
"fumi-store-v1-{}-{}.db",
std::process::id(),
now()
));
let path = scratch_path("v1");
// A schema-version-1 store: the inbox has no `kept` column and the
// history table does not exist yet.
let db = Connection::open(&path).unwrap();