mars-xlog-core 0.1.0

Core runtime building blocks for the Rust-native xlog engine.
Documentation
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

use crate::file_manager::FileManagerError;
use crate::file_naming::{LOG_EXT, LOG_EXT_WITH_DOT};
use crate::file_ops::{append_file_to_file, file_mtime};
use crate::metrics::{record_cache_move, record_expired_delete};

#[derive(Debug, Clone, Copy)]
pub(crate) struct CacheMaintenance<'a> {
    pub(crate) log_dir: &'a Path,
    pub(crate) cache_dir: Option<&'a Path>,
    pub(crate) name_prefix: &'a str,
    pub(crate) cache_days: i32,
}

pub(crate) fn move_old_cache_files(
    config: CacheMaintenance<'_>,
) -> Result<Vec<PathBuf>, FileManagerError> {
    let Some(cache_dir) = config.cache_dir else {
        return Ok(Vec::new());
    };
    if cache_dir == config.log_dir {
        return Ok(Vec::new());
    }
    if !cache_dir.is_dir() {
        return Ok(Vec::new());
    }

    let now = SystemTime::now();
    let mut removed_paths = Vec::new();
    let entries = fs::read_dir(cache_dir)
        .map_err(|e| FileManagerError::ReadDir(cache_dir.to_path_buf(), e))?;
    for entry in entries {
        let entry = entry.map_err(|e| FileManagerError::ReadDir(cache_dir.to_path_buf(), e))?;
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        let Some(file_name) = path.file_name().and_then(OsStr::to_str) else {
            continue;
        };
        if !file_name.starts_with(config.name_prefix) || !file_name.ends_with(LOG_EXT_WITH_DOT) {
            continue;
        }

        if config.cache_days > 0 {
            let modified = file_mtime(&path)?;
            let Ok(age) = now.duration_since(modified) else {
                continue;
            };
            if age < Duration::from_secs(config.cache_days as u64 * 24 * 60 * 60) {
                continue;
            }
        }

        let dest = config.log_dir.join(file_name);
        append_file_to_file(&path, &dest)?;
        fs::remove_file(&path).map_err(|e| FileManagerError::RemoveFile(path.clone(), e))?;
        record_cache_move();
        removed_paths.push(path);
    }

    Ok(removed_paths)
}

pub(crate) fn delete_expired_files(
    log_dir: &Path,
    cache_dir: Option<&Path>,
    max_alive_seconds: i64,
) -> Result<Vec<PathBuf>, FileManagerError> {
    if max_alive_seconds <= 0 {
        return Ok(Vec::new());
    }
    let threshold = Duration::from_secs(max_alive_seconds as u64);
    let mut removed_paths = Vec::new();
    delete_expired_under(log_dir, threshold, &mut removed_paths)?;
    if let Some(cache_dir) = cache_dir {
        delete_expired_under(cache_dir, threshold, &mut removed_paths)?;
    }
    Ok(removed_paths)
}

fn delete_expired_under(
    dir: &Path,
    threshold: Duration,
    removed_paths: &mut Vec<PathBuf>,
) -> Result<(), FileManagerError> {
    if !dir.is_dir() {
        return Ok(());
    }

    let now = SystemTime::now();
    let entries = fs::read_dir(dir).map_err(|e| FileManagerError::ReadDir(dir.to_path_buf(), e))?;
    for entry in entries {
        let entry = entry.map_err(|e| FileManagerError::ReadDir(dir.to_path_buf(), e))?;
        let path = entry.path();
        let modified = file_mtime(&path)?;
        let Ok(age) = now.duration_since(modified) else {
            continue;
        };
        if age <= threshold {
            continue;
        }

        if path.is_file() {
            if path.extension().and_then(OsStr::to_str) == Some(LOG_EXT) {
                fs::remove_file(&path)
                    .map_err(|e| FileManagerError::RemoveFile(path.clone(), e))?;
                record_expired_delete();
                removed_paths.push(path);
            }
            continue;
        }

        if path.is_dir() {
            let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default();
            let is_day_folder = name.len() == 8 && name.chars().all(|c| c.is_ascii_digit());
            if is_day_folder {
                fs::remove_dir_all(&path).map_err(|e| FileManagerError::RemoveDir(path, e))?;
                record_expired_delete();
            }
        }
    }
    Ok(())
}