use std::collections::BinaryHeap;
use std::fs::{File, OpenOptions};
use std::path::PathBuf;
use std::{fs, io};
use radicle::Profile;
pub struct LogRotator {
base: PathBuf,
existing_logs: BinaryHeap<u16>,
}
impl LogRotator {
pub const NODE_LOG: &str = "node.log";
pub const NODE_LOG_OLD_PREFIX: &str = "node.log.";
pub fn new(base: PathBuf) -> Self {
Self {
base,
existing_logs: BinaryHeap::new(),
}
}
pub fn found_logs(&mut self, suffixes: impl Iterator<Item = u16>) {
self.existing_logs.extend(suffixes);
}
pub fn remove_current(&self) -> Option<Remove> {
let current = self.current();
current.exists().then_some(Remove { current })
}
pub fn rotate(&self) -> Rotate {
let next = self.next_log();
let remove = self.remove_current();
Rotate {
next,
link: self.current(),
remove,
}
}
pub fn current(&self) -> PathBuf {
self.base.join(Self::NODE_LOG)
}
fn next_log(&self) -> PathBuf {
let suffix = self
.existing_logs
.peek()
.copied()
.unwrap_or(0)
.saturating_add(1);
self.base
.join(Self::NODE_LOG_OLD_PREFIX.to_owned() + suffix.to_string().as_str())
}
}
pub struct LogRotatorFileSystem {
rotator: LogRotator,
}
impl LogRotatorFileSystem {
pub fn from_profile(profile: &Profile) -> Self {
Self {
rotator: LogRotator::new(profile.home.node()),
}
}
pub fn rotate(mut self) -> io::Result<Rotated> {
self.rotator.found_logs(self.existing_logs().into_iter());
self.rotator.rotate().execute()
}
pub fn remove(self) -> io::Result<bool> {
self.rotator
.remove_current()
.map(|remove| remove.execute())
.transpose()
.map(|res| res.is_some())
}
fn parse_suffix(filename: String) -> Option<u16> {
filename
.strip_prefix(LogRotator::NODE_LOG_OLD_PREFIX)
.and_then(|suffix| suffix.parse::<u16>().ok())
}
fn existing_logs(&self) -> BinaryHeap<u16> {
self.rotator
.base
.read_dir()
.ok()
.map(|dir| {
dir.filter_map(Result::ok)
.filter_map(|entry| entry.file_name().into_string().ok())
.filter_map(Self::parse_suffix)
.collect()
})
.unwrap_or_default()
}
}
pub struct Remove {
current: PathBuf,
}
impl Remove {
pub fn execute(self) -> io::Result<()> {
fs::remove_file(self.current)
}
}
pub struct Rotate {
next: PathBuf,
link: PathBuf,
remove: Option<Remove>,
}
impl Rotate {
pub fn execute(self) -> io::Result<Rotated> {
if let Some(to_remove) = self.remove {
if let Err(err) = to_remove.execute() {
log::warn!(target: "cli", "Failed to remove current log file: {err}");
}
}
let log = OpenOptions::new()
.write(true)
.create_new(true)
.open(&self.next)?;
if let Err(err) = fs::hard_link(&self.next, &self.link) {
log::warn!(
target: "cli",
"Failed to create hard link from {} to {}: {err}",
self.next.display(),
self.link.display()
);
}
Ok(Rotated {
path: self.next,
log,
})
}
}
pub struct Rotated {
pub path: PathBuf,
pub log: File,
}