use std::fs::{self, File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::chain::ChainEvent;
use crate::TheaterId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunMeta {
pub actor_id: String,
pub actor_name: Option<String>,
pub manifest_path: Option<String>,
pub started_at: u64,
}
pub struct ChainWriter {
writer: BufWriter<File>,
path: PathBuf,
}
impl ChainWriter {
pub fn new(actor_id: &TheaterId) -> Result<Self> {
let chains_dir = Self::chains_dir();
fs::create_dir_all(&chains_dir)
.with_context(|| format!("Failed to create chains directory: {:?}", chains_dir))?;
let path = chains_dir.join(format!("{}.chain", actor_id));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("Failed to open chain file: {:?}", path))?;
Ok(Self {
writer: BufWriter::new(file),
path,
})
}
pub fn chains_dir() -> PathBuf {
PathBuf::from("/tmp/theater/chains")
}
pub fn write_meta(actor_id: &TheaterId, meta: &RunMeta) -> Result<()> {
let chains_dir = Self::chains_dir();
fs::create_dir_all(&chains_dir)?;
let path = chains_dir.join(format!("{}.meta.json", actor_id));
let json = serde_json::to_string_pretty(meta)?;
fs::write(&path, json)?;
Ok(())
}
pub fn append(&mut self, event: &ChainEvent) -> Result<()> {
writeln!(self.writer, "EVENT {}", hex::encode(&event.hash))?;
match &event.parent_hash {
Some(parent) => writeln!(self.writer, "{}", hex::encode(parent))?,
None => writeln!(self.writer, "0000000000000000000000000000000000000000")?,
}
writeln!(self.writer, "{}", event.event_type)?;
writeln!(self.writer, "{}", event.data.len())?;
writeln!(self.writer)?;
self.writer.write_all(&event.data)?;
writeln!(self.writer)?;
writeln!(self.writer)?;
self.writer.flush()?;
Ok(())
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
impl Drop for ChainWriter {
fn drop(&mut self) {
let _ = self.writer.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
#[test]
fn test_chain_writer_append() {
let actor_id = TheaterId::generate();
let mut writer = ChainWriter::new(&actor_id).unwrap();
let event = ChainEvent {
hash: vec![0x1a, 0x2b, 0x3c],
parent_hash: None,
event_type: "test".to_string(),
data: b"hello world".to_vec(),
};
writer.append(&event).unwrap();
drop(writer);
let path = ChainWriter::chains_dir().join(format!("{}.chain", actor_id));
let mut file = File::open(&path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert!(contents.contains("EVENT 1a2b3c"));
assert!(contents.contains("0000000000000000000000000000000000000000"));
assert!(contents.contains("test"));
assert!(contents.contains("11")); assert!(contents.contains("hello world"));
fs::remove_file(&path).ok();
}
}