shipper_core/state/events/
mod.rs1use std::fs::{self, File, OpenOptions};
43use std::io::{BufRead, BufReader, Write};
44use std::path::{Path, PathBuf};
45
46use anyhow::{Context, Result};
47use shipper_types::PublishEvent;
48
49#[cfg(test)]
50mod proptests;
51#[cfg(test)]
52mod tests;
53
54pub const EVENTS_FILE: &str = "events.jsonl";
56
57pub const PREFLIGHT_ONLY_EVENTS_FILE_PREFIX: &str = "preflight-only-";
65pub const PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX: &str = ".events.jsonl";
66
67pub fn events_path(state_dir: &Path) -> PathBuf {
71 state_dir.join(EVENTS_FILE)
72}
73
74pub fn preflight_only_events_path(state_dir: &Path, session_id: &str) -> PathBuf {
81 state_dir.join(format!(
82 "{PREFLIGHT_ONLY_EVENTS_FILE_PREFIX}{session_id}{PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX}"
83 ))
84}
85
86pub fn preflight_only_events_paths(state_dir: &Path) -> Result<Vec<PathBuf>> {
88 let mut paths = Vec::new();
89
90 if !state_dir.exists() {
91 return Ok(paths);
92 }
93
94 for entry in fs::read_dir(state_dir)
95 .with_context(|| format!("failed to read state dir {}", state_dir.display()))?
96 {
97 let entry =
98 entry.with_context(|| format!("failed to read entry in {}", state_dir.display()))?;
99 let file_type = entry
100 .file_type()
101 .with_context(|| format!("failed to stat {}", entry.path().display()))?;
102
103 if !file_type.is_file() {
104 continue;
105 }
106
107 let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
108 continue;
109 };
110
111 if file_name.starts_with(PREFLIGHT_ONLY_EVENTS_FILE_PREFIX)
112 && file_name.ends_with(PREFLIGHT_ONLY_EVENTS_FILE_SUFFIX)
113 {
114 paths.push(entry.path());
115 }
116 }
117
118 paths.sort();
119 Ok(paths)
120}
121
122#[derive(Debug, Default)]
126pub struct EventLog {
127 events: Vec<PublishEvent>,
128}
129
130impl EventLog {
131 pub fn new() -> Self {
133 Self { events: Vec::new() }
134 }
135
136 pub fn record(&mut self, event: PublishEvent) {
140 self.events.push(event);
141 }
142
143 pub fn write_to_file(&self, path: &Path) -> Result<()> {
147 if let Some(parent) = path.parent() {
148 fs::create_dir_all(parent)
149 .with_context(|| format!("failed to create events dir {}", parent.display()))?;
150 }
151
152 let file = OpenOptions::new()
154 .create(true)
155 .append(true)
156 .open(path)
157 .with_context(|| format!("failed to open events file {}", path.display()))?;
158
159 let mut writer = std::io::BufWriter::new(file);
160
161 self.write_events_to(&mut writer)?;
162
163 writer.flush().context("failed to flush events file")?;
164
165 Ok(())
166 }
167
168 fn write_events_to<W: Write>(&self, writer: &mut W) -> Result<()> {
169 for event in &self.events {
170 serde_json::to_writer(&mut *writer, event)
171 .context("failed to serialize event to JSON")?;
172 writer
173 .write_all(b"\n")
174 .context("failed to write event line")?;
175 }
176
177 Ok(())
178 }
179
180 pub fn read_from_file(path: &Path) -> Result<Self> {
184 if !path.exists() {
185 return Ok(Self::new());
186 }
187
188 let file = File::open(path)
189 .with_context(|| format!("failed to open events file {}", path.display()))?;
190
191 let reader = BufReader::new(file);
192 let mut events = Vec::new();
193
194 for line in reader.lines() {
195 let line = line.with_context(|| {
196 format!("failed to read line from events file {}", path.display())
197 })?;
198 let event: PublishEvent = serde_json::from_str(&line)
199 .with_context(|| format!("failed to parse event JSON from line: {}", line))?;
200 events.push(event);
201 }
202
203 Ok(Self { events })
204 }
205
206 pub fn events_for_package(&self, package: &str) -> Vec<&PublishEvent> {
210 self.events
211 .iter()
212 .filter(|e| e.package == package)
213 .collect()
214 }
215
216 pub fn all_events(&self) -> &[PublishEvent] {
218 &self.events
219 }
220
221 pub fn clear(&mut self) {
223 self.events.clear();
224 }
225
226 pub fn len(&self) -> usize {
228 self.events.len()
229 }
230
231 pub fn is_empty(&self) -> bool {
233 self.events.is_empty()
234 }
235}