use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::error::LinkError;
use crate::named_types::{NamedTypes, NamedTypesDecorator};
pub trait TransitionLogStore {
fn append_log_entry(&mut self, entry: &str) -> Result<(), LinkError>;
fn read_log_entries(&mut self) -> Result<Vec<String>, LinkError>;
fn flush_log(&mut self) -> Result<(), LinkError>;
}
fn storage_error(error: anyhow::Error) -> LinkError {
LinkError::StorageError(format!("{error:#}"))
}
impl TransitionLogStore for NamedTypesDecorator {
fn append_log_entry(&mut self, entry: &str) -> Result<(), LinkError> {
let link = self.create(0, 0);
self.set_name(link, entry).map_err(storage_error)?;
Ok(())
}
fn read_log_entries(&mut self) -> Result<Vec<String>, LinkError> {
let mut addresses: Vec<u32> = self.all().into_iter().map(|link| link.index).collect();
addresses.sort_unstable();
let mut entries = Vec::with_capacity(addresses.len());
for address in addresses {
if let Some(name) = NamedTypes::get_name(self, address).map_err(storage_error)? {
entries.push(name);
}
}
Ok(entries)
}
fn flush_log(&mut self) -> Result<(), LinkError> {
NamedTypesDecorator::save(self).map_err(storage_error)
}
}
#[derive(Debug)]
pub struct FileTransitionLog {
path: PathBuf,
file: File,
sync_on_append: bool,
}
impl FileTransitionLog {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, LinkError> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let repair = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)?;
truncate_torn_tail(&repair)?;
drop(repair);
let file = OpenOptions::new().read(true).append(true).open(&path)?;
Ok(Self {
path,
file,
sync_on_append: true,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn sync_on_append(&self) -> bool {
self.sync_on_append
}
pub fn set_sync_on_append(&mut self, value: bool) {
self.sync_on_append = value;
}
}
impl TransitionLogStore for FileTransitionLog {
fn append_log_entry(&mut self, entry: &str) -> Result<(), LinkError> {
if entry.contains('\n') || entry.contains('\r') {
return Err(LinkError::InvalidFormat(
"transition log entries must not contain line breaks".to_string(),
));
}
writeln!(self.file, "{entry}")?;
self.file.flush()?;
if self.sync_on_append {
self.file.sync_data()?;
}
Ok(())
}
fn read_log_entries(&mut self) -> Result<Vec<String>, LinkError> {
let mut contents = String::new();
File::open(&self.path)?.read_to_string(&mut contents)?;
Ok(contents
.lines()
.take(contents.matches('\n').count())
.filter(|line| !line.is_empty())
.map(|line| line.to_string())
.collect())
}
fn flush_log(&mut self) -> Result<(), LinkError> {
self.file.flush()?;
self.file.sync_data()?;
Ok(())
}
}
fn truncate_torn_tail(file: &File) -> Result<(), LinkError> {
let len = file.metadata()?.len();
if len == 0 {
return Ok(());
}
let mut file = file;
let mut end = len;
let mut buffer = [0u8; 8192];
while end > 0 {
let chunk = std::cmp::min(end, buffer.len() as u64);
let start = end - chunk;
file.seek(SeekFrom::Start(start))?;
let slice = &mut buffer[..chunk as usize];
file.read_exact(slice)?;
if let Some(offset) = slice.iter().rposition(|byte| *byte == b'\n') {
let complete = start + offset as u64 + 1;
if complete != len {
file.set_len(complete)?;
file.sync_all()?;
}
return Ok(());
}
end = start;
}
file.set_len(0)?;
file.sync_all()?;
Ok(())
}