dotlock-bin 1.0.1

Encrypted project-local environment variables manager
use std::{
    fs,
    path::Path,
    process::Command,
    time::{SystemTime, UNIX_EPOCH},
};

use crate::domain::{error::DotLockError, model::DotLockResult};

const MAX_LOG_BYTES: u64 = 10 * 1024 * 1024;
const MAX_LOG_AGE_SECS: u64 = 90 * 24 * 60 * 60;

pub fn rotate_if_needed(path: &Path) -> DotLockResult<()> {
    if !path.exists() {
        return Ok(());
    }
    let metadata = fs::metadata(path).map_err(DotLockError::from)?;
    let too_large = metadata.len() >= MAX_LOG_BYTES;
    let too_old = metadata
        .modified()
        .ok()
        .and_then(|modified| SystemTime::now().duration_since(modified).ok())
        .is_some_and(|age| age.as_secs() >= MAX_LOG_AGE_SECS);

    if too_large || too_old {
        rotate_current_log_at(path, now_secs())?;
    }
    Ok(())
}

pub fn rotate_current_log(path: &Path) -> DotLockResult<Option<std::path::PathBuf>> {
    if !path.exists() {
        return Ok(None);
    }
    rotate_current_log_at(path, now_secs()).map(Some)
}

fn rotate_current_log_at(path: &Path, ts: u64) -> DotLockResult<std::path::PathBuf> {
    let rotated = path.with_file_name(format!("audit.log.{ts}"));
    fs::rename(path, &rotated).map_err(DotLockError::from)?;
    let status = Command::new("gzip")
        .args(["-f", rotated.to_str().unwrap_or_default()])
        .status()
        .map_err(|err| DotLockError::Io(format!("failed to run gzip: {err}")))?;
    if !status.success() {
        return Err(DotLockError::Io(
            "gzip failed while rotating audit log".to_string(),
        ));
    }
    Ok(rotated.with_extension(format!(
        "{}gz",
        rotated
            .extension()
            .and_then(|extension| extension.to_str())
            .map(|extension| format!("{extension}."))
            .unwrap_or_default()
    )))
}

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or(0)
}