use std::cell::RefCell;
use std::fs::{DirEntry, File, OpenOptions};
use std::io::{Seek, SeekFrom, Write};
use crate::appender::{Command, FastLogRecord, LogAppender};
use crate::consts::LogSize;
use crate::error::LogError;
use crate::{chan, Receiver, Sender};
use std::str::FromStr;
use std::time::Duration;
pub trait Packer: Send {
fn pack_name(&self) -> &'static str;
fn do_pack(&self, log_file: File, log_file_path: &str) -> Result<bool, LogError>;
fn retry(&self) -> i32 {
return 0;
}
}
pub struct FileSplitAppender {
cell: RefCell<FileSplitAppenderData>,
}
pub struct LogPack {
pub dir: String,
pub rolling: RollingType,
pub new_log_name: String,
}
#[derive(Copy, Clone, Debug)]
pub enum RollingType {
All,
KeepTime(Duration),
KeepNum(i64),
}
impl RollingType {
fn read_paths(&self, dir: &str, temp_name: &str) -> Vec<DirEntry> {
let paths = std::fs::read_dir(dir);
if let Ok(paths) = paths {
let mut paths_vec = vec![];
for path in paths {
match path {
Ok(path) => {
if let Some(v) = path.file_name().to_str() {
if (v.ends_with(".log")
&& v.trim_end_matches(".log").ends_with(temp_name))
|| !v.starts_with(temp_name)
{
continue;
}
}
paths_vec.push(path);
}
_ => {}
}
}
paths_vec.sort_by(|a, b| b.file_name().cmp(&a.file_name()));
return paths_vec;
}
return vec![];
}
pub fn do_rolling(&self, temp_name: &str, dir: &str) {
match self {
RollingType::KeepNum(n) => {
let paths_vec = self.read_paths(dir, temp_name);
for index in 0..paths_vec.len() {
if index >= (*n) as usize {
let item = &paths_vec[index];
std::fs::remove_file(item.path());
}
}
}
RollingType::KeepTime(duration) => {
let paths_vec = self.read_paths(dir, temp_name);
let now = fastdate::DateTime::now();
for index in 0..paths_vec.len() {
let item = &paths_vec[index];
let file_name = item.file_name();
let name = file_name.to_str().unwrap_or("").to_string();
if let Some(time) = self.file_name_parse_time(&name, temp_name) {
if now.sub(duration.clone()) > time {
std::fs::remove_file(item.path());
}
}
}
}
_ => {}
}
}
fn file_name_parse_time(&self, name: &str, temp_name: &str) -> Option<fastdate::DateTime> {
if name.starts_with(temp_name) {
let mut time_str = name.replace(temp_name, "");
if let Some(v) = time_str.find(".") {
time_str = time_str[0..v].to_string();
}
let time = fastdate::DateTime::from_str(&time_str);
if let Ok(time) = time {
return Some(time);
}
}
return None;
}
}
pub struct FileSplitAppenderData {
max_split_bytes: usize,
dir_path: String,
file: File,
sender: Sender<LogPack>,
rolling_type: RollingType,
temp_bytes: usize,
temp_name: String,
}
impl FileSplitAppenderData {
pub fn send_pack(&mut self) {
let first_file_path = format!("{}{}.log", self.dir_path, &self.temp_name);
let new_log_name = format!(
"{}{}{}.log",
self.dir_path,
&self.temp_name,
format!("{:29}", fastdate::DateTime::now())
);
std::fs::copy(&first_file_path, &new_log_name);
self.sender.send(LogPack {
dir: self.dir_path.clone(),
rolling: self.rolling_type.clone(),
new_log_name: new_log_name,
});
self.truncate();
}
pub fn truncate(&mut self) {
self.file.set_len(0);
self.file.seek(SeekFrom::Start(0));
self.temp_bytes = 0;
}
}
impl FileSplitAppender {
pub fn new(
file_path: &str,
max_temp_size: LogSize,
rolling_type: RollingType,
packer: Box<dyn Packer>,
) -> FileSplitAppender {
let mut dir_path = file_path.to_owned();
let mut temp_file_name = dir_path.to_string();
if dir_path.contains("/") {
let new_dir_path =
dir_path[0..dir_path.rfind("/").unwrap_or_default()].to_string() + "/";
std::fs::create_dir_all(&new_dir_path);
temp_file_name = dir_path.trim_start_matches(&new_dir_path).to_string();
dir_path = new_dir_path;
}
if temp_file_name.is_empty() {
temp_file_name = "temp.log".to_string();
}
if !dir_path.is_empty() && dir_path.ends_with(".log") {
panic!("FileCompactionAppender only support new from path,for example: 'logs/xx/'");
}
if !dir_path.is_empty() && !dir_path.ends_with("/") {
panic!("FileCompactionAppender only support new from path,for example: 'logs/xx/'");
}
if !dir_path.is_empty() {
std::fs::create_dir_all(&dir_path);
}
let file_name = temp_file_name.trim_end_matches(".log");
let first_file_path = format!("{}{}.log", &dir_path, file_name);
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(first_file_path.as_str());
if file.is_err() {
panic!(
"[fast_log] open and create file fail:{}",
file.err().unwrap()
);
}
let mut file = file.unwrap();
let mut temp_bytes = 0;
if let Ok(m) = file.metadata() {
temp_bytes = m.len() as usize;
}
file.seek(SeekFrom::Start(temp_bytes as u64));
let (sender, receiver) = chan(None);
spawn_saver(file_name, receiver, packer);
Self {
cell: RefCell::new(FileSplitAppenderData {
max_split_bytes: max_temp_size.get_len(),
temp_bytes: temp_bytes,
dir_path: dir_path.to_string(),
file: file,
sender: sender,
rolling_type: rolling_type,
temp_name: file_name.to_string(),
}),
}
}
}
impl LogAppender for FileSplitAppender {
fn do_logs(&self, records: &[FastLogRecord]) {
let mut data = self.cell.borrow_mut();
if data.temp_bytes >= data.max_split_bytes {
data.send_pack();
}
let temp_log = {
let mut limit = data.max_split_bytes - data.temp_bytes;
let mut temp = String::with_capacity(100);
for x in records {
match x.command {
Command::CommandRecord => {
if (temp.as_bytes().len() + x.formated.as_bytes().len()) < limit {
temp.push_str(&x.formated);
} else {
data.file.write(temp.as_bytes());
data.send_pack();
limit = data.max_split_bytes;
temp.clear();
temp.push_str(&x.formated);
}
}
Command::CommandExit => {}
Command::CommandFlush(_) => {}
}
}
temp
};
if !temp_log.is_empty() {
if (data.temp_bytes + temp_log.as_bytes().len()) > data.max_split_bytes {
data.send_pack();
}
data.temp_bytes += {
let bytes = temp_log.as_bytes();
let w = data.file.write(bytes);
if let Ok(w) = w {
w
} else {
0
}
};
if data.temp_bytes > data.max_split_bytes {
data.send_pack();
}
}
}
fn flush(&self) {
let mut data = self.cell.borrow_mut();
data.file.flush();
}
}
fn spawn_saver(temp_name: &str, r: Receiver<LogPack>, packer: Box<dyn Packer>) {
let temp = temp_name.to_string();
std::thread::spawn(move || {
loop {
if let Ok(pack) = r.recv() {
pack.rolling.do_rolling(&temp, &pack.dir);
let log_file_path = pack.new_log_name.clone();
let remove = do_pack(&packer, pack);
if let Ok(remove) = remove {
if remove {
std::fs::remove_file(log_file_path);
}
}
}
}
});
}
pub fn do_pack(packer: &Box<dyn Packer>, mut pack: LogPack) -> Result<bool, LogPack> {
let log_file_path = pack.new_log_name.as_str();
if log_file_path.is_empty() {
return Err(pack);
}
let log_file = OpenOptions::new().read(true).open(log_file_path);
if log_file.is_err() {
return Err(pack);
}
let log_file = log_file.unwrap();
let r = packer.do_pack(log_file, log_file_path);
if r.is_err() && packer.retry() > 0 {
let mut retry = 1;
while let Err(packs) = do_pack(packer, pack) {
pack = packs;
retry += 1;
if retry > packer.retry() {
break;
}
}
}
if let Ok(b) = r {
return Ok(b);
}
return Ok(false);
}