1use bytes::Bytes;
5use clap::Parser;
6use serde::{Deserialize, Serialize};
7use std::{os::unix::ffi::OsStringExt, path::PathBuf, sync::OnceLock};
8use tokio::fs::File;
9use tokio::io::AsyncWriteExt;
10use uuid::Uuid;
11
12static STORE_PATH: OnceLock<PathBuf> = OnceLock::new();
15
16fn store_path() -> &'static PathBuf {
17 STORE_PATH.get().expect("Store path not initialized")
18}
19
20#[derive(Debug, Parser)]
22pub struct StoreCli {
23 #[arg(long, default_value = "store.db")]
24 store_path: PathBuf,
25}
26
27pub fn init(cli: StoreCli) {
31 log::info!("Initialize store at {}", cli.store_path.display());
32 std::fs::create_dir_all(cli.store_path.join("tmp")).expect("Failed to create store directory");
33 STORE_PATH
34 .set(cli.store_path)
35 .expect("Store path already initialized");
36}
37
38#[derive(thiserror::Error, Serialize, Deserialize, Debug, Clone)]
40pub enum Error {
41 #[error("I/O error: {0}")]
42 Io(String),
43 #[error("Hex decoding error: {0}")]
44 Hex(String),
45}
46
47impl From<std::io::Error> for Error {
48 fn from(e: std::io::Error) -> Self {
49 Error::Io(e.to_string())
50 }
51}
52
53impl From<hex::FromHexError> for Error {
54 fn from(e: hex::FromHexError) -> Self {
55 Error::Hex(e.to_string())
56 }
57}
58
59pub async fn item_get(table: Uuid, key: &[u8]) -> Result<Option<Bytes>, Error> {
60 log::debug!("{table} get");
61 let path = store_path().join(table.to_string()).join(hex::encode(key));
62
63 match tokio::fs::read(&path).await {
64 Ok(content) => Ok(Some(Bytes::from(content))),
65 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
66 Err(e) => Err(e.into()),
67 }
68}
69
70#[cfg(target_vendor = "apple")]
71fn fsync(f: &File) -> std::io::Result<()> {
72 use std::os::fd::{AsFd, AsRawFd};
73
74 if unsafe { libc::fsync(f.as_fd().as_raw_fd()) } == -1 {
75 return Err(std::io::Error::last_os_error());
76 }
77
78 Ok(())
79}
80
81pub async fn item_set(table: Uuid, key: &[u8], value: &[u8]) -> Result<(), Error> {
82 log::debug!("{table} set");
83 let tmp_path = store_path().join("tmp").join(Uuid::now_v7().to_string());
84 let mut file = File::create_new(&tmp_path).await?;
85 file.write_all(value).await?;
86
87 #[cfg(not(target_vendor = "apple"))]
88 file.sync_all().await?;
89 #[cfg(target_vendor = "apple")]
90 fsync(&file)?;
91
92 let path = store_path().join(table.to_string());
93 std::fs::create_dir_all(&path)?;
94 let path = path.join(hex::encode(key));
95 tokio::fs::rename(&tmp_path, &path).await?;
96 Ok(())
97}
98
99pub type KeyValue = (Vec<u8>, Vec<u8>);
100
101pub async fn item_list(table: Uuid) -> Result<Vec<KeyValue>, Error> {
102 log::debug!("{table} list");
103 let path = store_path().join(table.to_string());
104 let mut items = Vec::new();
105 let mut iter = match tokio::fs::read_dir(path).await {
106 Ok(iter) => iter,
107 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
108 return Ok(items);
109 }
110 Err(e) => return Err(e.into()),
111 };
112 while let Some(entry) = iter.next_entry().await? {
113 let key = hex::decode(entry.file_name().into_vec())?;
114 let value = item_get(table, &key).await?.expect("Item should exist");
115 items.push((key, value.to_vec()));
116 }
117 Ok(items)
118}
119
120pub async fn table_delete(table: Uuid) -> Result<(), Error> {
121 log::debug!("{table} destroy");
122 let path = store_path().join(table.to_string());
123 match tokio::fs::remove_dir_all(path).await {
124 Ok(()) => Ok(()),
125 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
126 Err(e) => Err(e.into()),
127 }
128}