use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use shipper_types::PublishEvent;
#[cfg(test)]
mod proptests;
#[cfg(test)]
mod tests;
pub const EVENTS_FILE: &str = "events.jsonl";
pub const PREFLIGHT_ONLY_EVENTS_FILE_PREFIX: &str = "preflight-only-";
pub const PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX: &str = ".events.jsonl";
pub fn events_path(state_dir: &Path) -> PathBuf {
state_dir.join(EVENTS_FILE)
}
pub fn preflight_only_events_path(state_dir: &Path, session_id: &str) -> PathBuf {
state_dir.join(format!(
"{PREFLIGHT_ONLY_EVENTS_FILE_PREFIX}{session_id}{PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX}"
))
}
pub fn preflight_only_events_paths(state_dir: &Path) -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
if !state_dir.exists() {
return Ok(paths);
}
for entry in fs::read_dir(state_dir)
.with_context(|| format!("failed to read state dir {}", state_dir.display()))?
{
let entry =
entry.with_context(|| format!("failed to read entry in {}", state_dir.display()))?;
let file_type = entry
.file_type()
.with_context(|| format!("failed to stat {}", entry.path().display()))?;
if !file_type.is_file() {
continue;
}
let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if file_name.starts_with(PREFLIGHT_ONLY_EVENTS_FILE_PREFIX)
&& file_name.ends_with(PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX)
{
paths.push(entry.path());
}
}
paths.sort();
Ok(paths)
}
#[derive(Debug, Default)]
pub struct EventLog {
events: Vec<PublishEvent>,
}
impl EventLog {
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn record(&mut self, event: PublishEvent) {
self.events.push(event);
}
pub fn write_to_file(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create events dir {}", parent.display()))?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("failed to open events file {}", path.display()))?;
let mut writer = std::io::BufWriter::new(file);
self.write_events_to(&mut writer)?;
writer.flush().context("failed to flush events file")?;
Ok(())
}
fn write_events_to<W: Write>(&self, writer: &mut W) -> Result<()> {
for event in &self.events {
serde_json::to_writer(&mut *writer, event)
.context("failed to serialize event to JSON")?;
writer
.write_all(b"\n")
.context("failed to write event line")?;
}
Ok(())
}
pub fn read_from_file(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::new());
}
let file = File::open(path)
.with_context(|| format!("failed to open events file {}", path.display()))?;
let reader = BufReader::new(file);
let mut events = Vec::new();
for line in reader.lines() {
let line = line.with_context(|| {
format!("failed to read line from events file {}", path.display())
})?;
let event: PublishEvent = serde_json::from_str(&line)
.with_context(|| format!("failed to parse event JSON from line: {}", line))?;
events.push(event);
}
Ok(Self { events })
}
pub fn events_for_package(&self, package: &str) -> Vec<&PublishEvent> {
self.events
.iter()
.filter(|e| e.package == package)
.collect()
}
pub fn all_events(&self) -> &[PublishEvent] {
&self.events
}
pub fn clear(&mut self) {
self.events.clear();
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}