use crate::{core::context, core::error::TookaError};
use chrono::Local;
use flexi_logger::writers::LogWriter;
use flexi_logger::{LogSpecification, Logger, Record, WriteMode};
use log::Record as LogRecord;
use std::path::Path;
use std::{
fs::{OpenOptions, create_dir_all},
io::{self, Write},
path::PathBuf,
sync::{Mutex, OnceLock},
};
static LOG_MUTEX: Mutex<()> = Mutex::new(());
static LOGGER_HANDLE: OnceLock<flexi_logger::LoggerHandle> = OnceLock::new();
const MAX_LOG_FILES: usize = 10;
struct DualWriter {
main_dir: PathBuf,
ops_dir: PathBuf,
}
pub fn init_logger() -> Result<(), TookaError> {
let config = context::get_locked_config()
.map_err(|e| TookaError::ConfigError(format!("Failed to get config: {e}")))?;
let logs_folder = &config.logs_folder;
create_dir_all(logs_folder.join("ops"))?;
let log_spec = LogSpecification::parse("debug, file_ops=info")?;
let logger = Logger::with(log_spec)
.log_to_writer(Box::new(DualWriter::new(logs_folder)))
.write_mode(WriteMode::BufferAndFlush)
.format(custom_format)
.start()?;
LOGGER_HANDLE
.set(logger)
.map_err(|_| TookaError::ConfigError("Logger already initialized".into()))?;
Ok(())
}
pub fn log_file_operation(msg: &str) {
log::info!(target: "file_ops", "{msg}");
}
fn custom_format(
w: &mut dyn Write,
now: &mut flexi_logger::DeferredNow,
record: &LogRecord,
) -> io::Result<()> {
writeln!(
w,
"{} [{}] {} - {}",
now.format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args()
)
}
impl DualWriter {
fn new(base: &Path) -> Self {
Self {
main_dir: base.to_path_buf(),
ops_dir: base.join("ops"),
}
}
fn get_main_log_path(&self) -> PathBuf {
self.main_dir.join("main.log")
}
fn get_ops_log_path(&self) -> std::path::PathBuf {
let date_str = Local::now().format("%Y-%m-%d").to_string();
let base_path = self.ops_dir.join(format!("{date_str}.log"));
if !base_path.exists() {
return base_path;
}
for i in 1..=MAX_LOG_FILES {
let candidate = self.ops_dir.join(format!("{date_str}-{i}.log"));
if !candidate.exists() {
return candidate;
}
}
self.ops_dir.join(format!("{date_str}-{MAX_LOG_FILES}.log"))
}
fn is_file_recent(path: &Path) -> std::io::Result<bool> {
if !path.exists() {
return Ok(false);
}
let metadata = std::fs::metadata(path)?;
let modified = metadata.modified()?;
let age = Local::now().signed_duration_since(chrono::DateTime::<Local>::from(modified));
Ok(age.num_minutes() < 60)
}
}
impl LogWriter for DualWriter {
fn write(
&self,
now: &mut flexi_logger::DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
let Ok(_guard) = LOG_MUTEX.try_lock() else {
return Ok(());
};
if record.target() == "file_ops" {
let path = self.get_ops_log_path();
if let Some(dir) = path.parent() {
let mut log_files: Vec<_> = std::fs::read_dir(dir)?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("log") {
Some(path)
} else {
None
}
})
.collect();
log_files.sort();
while log_files.len() > MAX_LOG_FILES {
if let Some(oldest) = log_files.first() {
let _ = std::fs::remove_file(oldest);
log_files.remove(0);
}
}
}
let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
let mut buf = Vec::new();
custom_format(&mut buf, now, record)?;
file.write_all(&buf)?;
} else {
let path = self.get_main_log_path();
let recent = Self::is_file_recent(&path)?;
let mut open_opts = OpenOptions::new();
open_opts.create(true);
if recent {
open_opts.append(true);
} else {
open_opts.write(true).truncate(true);
}
let mut file = open_opts.open(&path)?;
let mut buf = Vec::new();
custom_format(&mut buf, now, record)?;
file.write_all(&buf)?;
}
Ok(())
}
fn flush(&self) -> std::io::Result<()> {
Ok(())
}
}