AkurAI Build
Menu

Public repository / master

AkurAI-Drive / client/src/state.rs

Latest change feb7ac86b5e60d55b429eacb7146cf0dc32c6628 - Add native Linux sync client (akurai-drive-sync) by olibuijr

//! Local SQLite sync-state: the record of what each key looked like at the last
//! successful sync, used to detect changes on both sides.

use anyhow::{Context, Result};
use rusqlite::Connection;
use std::collections::BTreeMap;
use std::path::Path;

/// One persisted state row (plus mtime, used only for the local hashing
/// fast-path — not part of the change-decision identity).
#[derive(Debug, Clone)]
pub struct Record {
    pub sha256: String,
    pub etag: String,
    pub size: u64,
    pub mtime: i64,
}

pub struct State {
    conn: Connection,
}

impl State {
    pub fn open(path: &Path) -> Result<Self> {
        if let Some(dir) = path.parent() {
            std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
        }
        let conn = Connection::open(path)
            .with_context(|| format!("opening state db {}", path.display()))?;
        conn.execute_batch(
            "PRAGMA journal_mode = WAL;
             CREATE TABLE IF NOT EXISTS files (
                 key    TEXT PRIMARY KEY,
                 sha256 TEXT NOT NULL,
                 etag   TEXT NOT NULL,
                 size   INTEGER NOT NULL,
                 mtime  INTEGER NOT NULL
             );
             CREATE TABLE IF NOT EXISTS meta (
                 k TEXT PRIMARY KEY,
                 v TEXT NOT NULL
             );",
        )
        .context("initializing state schema")?;
        Ok(Self { conn })
    }

    /// Load every tracked key into memory.
    pub fn load(&self) -> Result<BTreeMap<String, Record>> {
        let mut stmt = self
            .conn
            .prepare("SELECT key, sha256, etag, size, mtime FROM files")?;
        let rows = stmt.query_map([], |row| {
            Ok((
                row.get::<_, String>(0)?,
                Record {
                    sha256: row.get(1)?,
                    etag: row.get(2)?,
                    size: row.get::<_, i64>(3)? as u64,
                    mtime: row.get(4)?,
                },
            ))
        })?;
        let mut map = BTreeMap::new();
        for r in rows {
            let (k, v) = r?;
            map.insert(k, v);
        }
        Ok(map)
    }

    pub fn upsert(&self, key: &str, rec: &Record) -> Result<()> {
        self.conn.execute(
            "INSERT INTO files (key, sha256, etag, size, mtime)
             VALUES (?1, ?2, ?3, ?4, ?5)
             ON CONFLICT(key) DO UPDATE SET
                 sha256 = excluded.sha256,
                 etag   = excluded.etag,
                 size   = excluded.size,
                 mtime  = excluded.mtime",
            rusqlite::params![key, rec.sha256, rec.etag, rec.size as i64, rec.mtime],
        )?;
        Ok(())
    }

    pub fn remove(&self, key: &str) -> Result<()> {
        self.conn
            .execute("DELETE FROM files WHERE key = ?1", [key])?;
        Ok(())
    }

    pub fn meta_set(&self, k: &str, v: &str) -> Result<()> {
        self.conn.execute(
            "INSERT INTO meta (k, v) VALUES (?1, ?2)
             ON CONFLICT(k) DO UPDATE SET v = excluded.v",
            rusqlite::params![k, v],
        )?;
        Ok(())
    }

    pub fn meta_get(&self, k: &str) -> Result<Option<String>> {
        let v = self
            .conn
            .query_row("SELECT v FROM meta WHERE k = ?1", [k], |row| row.get(0))
            .ok();
        Ok(v)
    }
}