use crate::{clock, id};
use serde::{Deserialize, Serialize};
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
pub const DIR: &str = ".vivac";
pub const LOG: &str = "events";
pub const CONFIG: &str = "config";
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub version: u32,
pub project_id: String,
pub actor: String,
}
impl Config {
fn new_seeded() -> Config {
Config {
version: 1,
project_id: id::ulid(),
actor: format!("a_{}", &id::ulid()[..12]),
}
}
}
pub struct Store {
pub root: PathBuf,
pub config: Config,
}
pub fn find_root(from_dir: &Path) -> Option<PathBuf> {
let mut d = from_dir.to_path_buf();
loop {
if d.join(DIR).is_dir() {
return Some(d);
}
if !d.pop() {
return None;
}
}
}
impl Store {
pub fn abrir(root: PathBuf) -> std::io::Result<Store> {
let p = root.join(DIR).join(CONFIG);
let config = match fs::read_to_string(&p) {
Ok(s) => serde_json::from_str(&s).map_err(std::io::Error::other)?,
Err(_) => {
let c = Config::new_seeded();
write_config(&root, &c)?;
c
}
};
Ok(Store { root, config })
}
pub fn create(root: &Path) -> std::io::Result<Store> {
let d = root.join(DIR);
fs::create_dir_all(&d)?;
let config = Config::new_seeded();
write_config(root, &config)?;
if !d.join(LOG).exists() {
File::create(d.join(LOG))?;
}
Ok(Store {
root: root.to_path_buf(),
config,
})
}
pub fn log(&self) -> PathBuf {
self.root.join(DIR).join(LOG)
}
}
fn write_config(root: &Path, c: &Config) -> std::io::Result<()> {
let mut f = File::create(root.join(DIR).join(CONFIG))?;
f.write_all(serde_json::to_string_pretty(c)?.as_bytes())?;
f.write_all(b"\n")
}
impl Store {
pub fn read_all(&self) -> std::io::Result<(Vec<crate::event::Event>, usize)> {
let f = match File::open(self.log()) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok((vec![], 0)),
Err(e) => return Err(e),
};
let mut eventos = Vec::new();
let mut rotas = 0usize;
for line in BufReader::new(f).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
match serde_json::from_str(&line) {
Ok(e) => eventos.push(e),
Err(_) => rotas += 1,
}
}
Ok((eventos, rotas))
}
pub fn append(
&self,
cuerpo: Vec<crate::event::Body>,
desde_seq: u64,
) -> std::io::Result<()> {
let mut buf = String::with_capacity(256 * cuerpo.len());
for (i, c) in cuerpo.into_iter().enumerate() {
let e = crate::event::Event {
seq: desde_seq + i as u64 + 1,
id: id::ulid(),
ts: clock::now_rfc3339(),
actor: self.config.actor.clone(),
lane: "main".into(),
payload: c,
};
buf.push_str(&serde_json::to_string(&e).map_err(std::io::Error::other)?);
buf.push('\n');
}
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(self.log())?;
f.write_all(buf.as_bytes())
}
}
impl Store {
pub fn write_raw(&self, eventos: &[crate::event::Event]) -> std::io::Result<()> {
let mut buf = String::with_capacity(256 * eventos.len());
for e in eventos {
buf.push_str(&serde_json::to_string(e).map_err(std::io::Error::other)?);
buf.push('\n');
}
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(self.log())?;
f.write_all(buf.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_upward() {
let tmp = std::env::temp_dir().join(format!("vivac-t-{}", id::ulid()));
let depth_of = tmp.join("a").join("b").join("c");
fs::create_dir_all(&depth_of).unwrap();
assert!(find_root(&depth_of).is_none());
Store::create(&tmp).unwrap();
assert_eq!(find_root(&depth_of).unwrap(), tmp);
fs::remove_dir_all(&tmp).ok();
}
#[test]
fn the_actor_carries_no_personal_data() {
let c = Config::new_seeded();
assert!(c.actor.starts_with("a_"));
assert!(!c.actor.contains('@'));
assert_ne!(c.actor, whoami_ish());
}
fn whoami_ish() -> String {
std::env::var("USERNAME")
.or_else(|_| std::env::var("USER"))
.unwrap_or_default()
}
}