use std::path::PathBuf;
use serde::{Deserialize, Serialize};
pub trait StoreBackend: Send + Sync + 'static {
fn load(&self, store_id: &str, device_id: &str) -> Option<(Vec<u8>, u64)>;
fn save(&self, store_id: &str, device_id: &str, data: &[u8], version: u64);
fn remove(&self, store_id: &str, device_id: &str);
}
#[derive(Debug, Default)]
pub struct MemoryBackend;
impl StoreBackend for MemoryBackend {
fn load(&self, _store_id: &str, _device_id: &str) -> Option<(Vec<u8>, u64)> {
None
}
fn save(&self, _store_id: &str, _device_id: &str, _data: &[u8], _version: u64) {}
fn remove(&self, _store_id: &str, _device_id: &str) {}
}
#[derive(Serialize, Deserialize)]
struct StoredSlice {
data: Vec<u8>,
version: u64,
}
pub struct FileBackend {
base_dir: PathBuf,
}
impl FileBackend {
pub fn new(base_dir: impl Into<PathBuf>) -> Self {
Self {
base_dir: base_dir.into(),
}
}
fn path(&self, store_id: &str, device_id: &str) -> PathBuf {
let safe_store = sanitize(store_id);
let safe_device = sanitize(device_id);
self.base_dir
.join(safe_store)
.join(format!("{safe_device}.json"))
}
}
fn sanitize(s: &str) -> String {
s.replace('/', "_").replace('\\', "_").replace("..", "_")
}
impl StoreBackend for FileBackend {
fn load(&self, store_id: &str, device_id: &str) -> Option<(Vec<u8>, u64)> {
let path = self.path(store_id, device_id);
let contents = std::fs::read_to_string(&path).ok()?;
let stored: StoredSlice = serde_json::from_str(&contents).ok()?;
Some((stored.data, stored.version))
}
fn save(&self, store_id: &str, device_id: &str, data: &[u8], version: u64) {
let path = self.path(store_id, device_id);
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
tracing::error!(
path = %parent.display(),
"file_backend: failed to create directory: {e}"
);
return;
}
}
let stored = StoredSlice {
data: data.to_vec(),
version,
};
let json = match serde_json::to_string(&stored) {
Ok(j) => j,
Err(e) => {
tracing::error!("file_backend: failed to serialize: {e}");
return;
}
};
let tmp_path = path.with_extension("json.tmp");
if let Err(e) = std::fs::write(&tmp_path, json.as_bytes()) {
tracing::error!(
path = %tmp_path.display(),
"file_backend: failed to write tmp file: {e}"
);
return;
}
if let Err(e) = std::fs::rename(&tmp_path, &path) {
tracing::error!(
from = %tmp_path.display(),
to = %path.display(),
"file_backend: failed to rename tmp file: {e}"
);
}
}
fn remove(&self, store_id: &str, device_id: &str) {
let path = self.path(store_id, device_id);
let _ = std::fs::remove_file(&path);
}
}