use std::{
fs,
io::{
self,
LineWriter,
Write,
},
};
use camino::{
Utf8Path,
Utf8PathBuf,
};
use crate::{
env::try_expand_env_vars,
error::{
Error,
Result,
},
};
#[derive(Clone, Default)]
pub struct LogFileMeta {
est_len: u64,
}
impl LogFileMeta {
pub fn from_meta(meta: &fs::Metadata) -> Self {
Self {
est_len: meta.len(),
}
}
pub fn try_from_file(file: &fs::File) -> io::Result<Self> {
Ok(Self::from_meta(&file.metadata()?))
}
fn wrote(&mut self, bs_count: usize) {
self.est_len = self.est_len.saturating_add(bs_count as u64);
}
fn len_estimate(&self) -> u64 {
self.est_len
}
}
#[derive(Clone)]
pub enum Trigger {
Size { limit: u64 },
}
impl Trigger {
fn should_roll(&self, meta: &LogFileMeta) -> bool {
matches!(self, Self::Size{limit} if *limit < meta.len_estimate())
}
}
#[derive(Clone)]
pub struct FixedWindow {
last: Option<usize>,
count: usize,
pattern: String,
}
impl FixedWindow {
const COUNT_BASE: usize = 0;
pub(crate) const INDEX_TOKEN: &'static str = "{}";
fn inc_last(&mut self) -> usize {
match &mut self.last {
None => {
self.last.replace(Self::COUNT_BASE);
Self::COUNT_BASE
},
Some(x) if (x.saturating_add(1)) == self.count => *x,
Some(x) => {
*x = x.saturating_add(1);
*x
},
}
}
fn roll(&mut self, path: &Utf8Path) -> io::Result<()> {
{
if let Some(mut c) = self.last {
if c.saturating_add(1) == self.count {
if c == 0 {
self.inc_last();
let new_path = self
.pattern
.replace(Self::INDEX_TOKEN, &Self::COUNT_BASE.to_string());
return fs::rename(path, new_path);
}
c = c.saturating_sub(1);
}
while c > 0 && c > Self::COUNT_BASE {
Self::pattern_roll(&self.pattern, c, c.saturating_add(1))?;
c = c.saturating_sub(1);
}
Self::pattern_roll(&self.pattern, c, c.saturating_add(1))?;
}
}
self.inc_last();
let new_path = self
.pattern
.replace(Self::INDEX_TOKEN, &Self::COUNT_BASE.to_string());
fs::rename(path, new_path)
}
fn pattern_roll(pattern: &str, from: usize, to: usize) -> io::Result<()> {
fs::rename(
pattern.replace(Self::INDEX_TOKEN, &from.to_string()),
pattern.replace(Self::INDEX_TOKEN, &to.to_string()),
)
}
}
#[derive(Clone)]
pub enum Roller {
Delete,
FixedWindow(FixedWindow),
}
impl Roller {
pub fn new_fixed(pattern: String, count: usize) -> Self {
Self::FixedWindow(FixedWindow {
last: None,
pattern,
count,
})
}
pub fn roll(
&mut self,
path: &Utf8Path,
writer: &mut Option<LineWriter<fs::File>>,
) -> io::Result<()> {
if let Some(w) = writer {
w.flush()?;
}
writer.take();
match self {
Self::FixedWindow(x) => {
x.roll(path)?;
},
Self::Delete => fs::remove_file(path)?,
}
writer.replace(RollingFile::new_writer(path)?);
Ok(())
}
}
pub struct RollingFile {
path: Utf8PathBuf,
writer: Option<LineWriter<fs::File>>,
meta: LogFileMeta,
trigger: Trigger,
roller: Roller,
}
impl RollingFile {
const DEFAULT_FILE_NAME: &'static str = "log";
const DEFAULT_ROLL_PATTERN: &'static str = "{filename}.{}";
const FILE_NAME_TOKEN: &'static str = "{filename}";
pub fn new(p: impl AsRef<Utf8Path>, trigger: Trigger, roller: Roller) -> Result<Self> {
let expanded_path = try_expand_env_vars(p.as_ref());
let (writer, meta) = {
let writer = Self::new_writer(&expanded_path).map_err(|e| Error::CreateFailed {
path: expanded_path.clone().into_owned(),
source: e,
})?;
let meta = writer
.get_ref()
.metadata()
.map_err(|e| Error::MetadataFailed {
path: expanded_path.clone().into_owned(),
source: e,
})?;
(writer, LogFileMeta::from_meta(&meta))
};
Ok(Self {
path: expanded_path.into_owned(),
writer: Some(writer),
meta,
trigger,
roller,
})
}
pub fn correct_path(&mut self) -> io::Result<()> {
let correct = fs::metadata(&self.path);
let existing = self.writer.as_ref().map(|w| w.get_ref().metadata());
if needs_remount(existing, correct) {
self.remount()?;
}
Ok(())
}
pub fn path_str(&self) -> &str {
self.path.as_str()
}
pub fn get_path(&self) -> &Utf8Path {
&self.path
}
pub fn get_path_buf(&self) -> Utf8PathBuf {
self.path.to_path_buf()
}
fn remount(&mut self) -> io::Result<()> {
self.writer
.as_mut()
.map(std::io::Write::flush)
.transpose()?;
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)?;
}
let p = self.path.clone();
self.swap_writer(|| Self::new_writer(&p))?;
self.meta = self
.writer
.as_mut()
.map(|w| LogFileMeta::try_from_file(w.get_ref()))
.transpose()?
.unwrap_or_default();
Ok(())
}
pub(crate) fn make_qualified_pattern(path: &Utf8Path, pat_opt: Option<&str>) -> String {
let parent = path.parent().unwrap_or_else(|| Utf8Path::new("/"));
if let Some(pat) = pat_opt {
parent.join(pat).to_string()
} else {
let file_name = path.file_name().unwrap_or(Self::DEFAULT_FILE_NAME);
let file_name_pattern =
Self::DEFAULT_ROLL_PATTERN.replacen(Self::FILE_NAME_TOKEN, file_name, 1);
parent.join(file_name_pattern).to_string()
}
}
fn swap_writer(&mut self, f: impl Fn() -> io::Result<LineWriter<fs::File>>) -> io::Result<()> {
self.writer.take();
self.writer = Some(f()?);
Ok(())
}
fn new_writer(path: &Utf8Path) -> io::Result<LineWriter<fs::File>> {
let f = fs::File::options().append(true).create(true).open(path)?;
Ok(LineWriter::new(f))
}
fn maybe_roll(&mut self) -> io::Result<()> {
if self.trigger.should_roll(&self.meta) {
self.roller.roll(&self.path, &mut self.writer)?;
self.meta = self
.writer
.as_mut()
.map(|w| LogFileMeta::try_from_file(w.get_ref()))
.transpose()?
.unwrap_or_default();
}
Ok(())
}
}
impl io::Write for RollingFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Some(w) = &mut self.writer {
let bs_written = w.write(buf)?;
self.meta.wrote(bs_written);
self.maybe_roll()?;
Ok(bs_written)
} else {
Ok(0)
}
}
fn flush(&mut self) -> io::Result<()> {
self.writer
.as_mut()
.map(std::io::Write::flush)
.transpose()?;
Ok(())
}
}
pub(crate) fn needs_remount(
existing: Option<io::Result<fs::Metadata>>,
correct: io::Result<fs::Metadata>,
) -> bool {
match (existing, correct) {
(Some(Ok(_)) | None, Err(_)) => true,
(Some(Ok(e)), Ok(c)) => needs_remount_inner(&e, &c),
_ => false,
}
}
fn needs_remount_inner(existing: &fs::Metadata, correct: &fs::Metadata) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
existing.dev() != correct.dev() || existing.ino() != correct.ino()
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
existing.file_size() != correct.file_size()
|| existing.creation_time() != correct.creation_time()
|| existing.last_write_time() != correct.last_write_time()
}
#[cfg(not(any(unix, windows)))]
{
false
}
}