mechutil 0.8.1

Utility structures and functions for mechatronics applications.
Documentation
//! Size-limited rotating file writer.
//!
//! [`RotatingFileWriter`] wraps a [`std::fs::File`] and implements [`std::io::Write`].
//! When accumulated writes exceed the configured byte limit, it rotates the active
//! log file to a backup (same path with `.old` appended to the extension) and opens
//! a fresh file. Only one backup is kept, so maximum disk usage is ~2x the limit.
//!
//! Designed to be passed directly to loggers that accept a generic `Write`
//! (e.g. `simplelog::WriteLogger`).
//!
//! # Example
//!
//! ```no_run
//! use std::fs::OpenOptions;
//! use std::path::PathBuf;
//! use mechutil::RotatingFileWriter;
//!
//! let path = PathBuf::from("/var/log/myapp.log");
//! let file = OpenOptions::new()
//!     .create(true)
//!     .append(true)
//!     .open(&path)
//!     .expect("open log file");
//!
//! // 10 MB limit, one backup kept automatically
//! let writer = RotatingFileWriter::new(path, file, 10 * 1024 * 1024);
//! // Pass `writer` to WriteLogger::new(...) or any Write-accepting sink.
//! ```

use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;

/// A [`Write`] wrapper that rotates the underlying file when it exceeds a size limit.
///
/// On rotation the current file is renamed to `<name>.old` (any previous `.old` is
/// deleted first) and a fresh file is opened at the original path in append mode.
pub struct RotatingFileWriter {
    file: File,
    path: PathBuf,
    bytes_written: u64,
    max_bytes: u64,
}

impl RotatingFileWriter {
    /// Create a new `RotatingFileWriter`.
    ///
    /// * `path`      - canonical path of the log file (used for rename/reopen on rotation).
    /// * `file`      - an already-opened `File` handle (typically opened with append mode).
    /// * `max_bytes` - size threshold in bytes that triggers rotation.
    ///
    /// The existing file size is read from disk so that an append-mode open
    /// mid-file is accounted for correctly.
    pub fn new(path: PathBuf, file: File, max_bytes: u64) -> Self {
        let current_size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
        Self {
            file,
            path,
            bytes_written: current_size,
            max_bytes,
        }
    }

    /// Rotate the log file: rename current -> `*.old`, open a fresh file.
    fn rotate(&mut self) -> io::Result<()> {
        let backup = self.path.with_extension("log.old");
        let _ = fs::remove_file(&backup); // OK if it doesn't exist
        fs::rename(&self.path, &backup)?;
        self.file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.path)?;
        self.bytes_written = 0;
        Ok(())
    }
}

impl Write for RotatingFileWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let n = self.file.write(buf)?;
        self.bytes_written += n as u64;
        if self.bytes_written >= self.max_bytes {
            if let Err(e) = self.rotate() {
                eprintln!("Warning: log rotation failed: {}", e);
            }
        }
        Ok(n)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.file.flush()
    }
}