use std::fs::{File, OpenOptions};
use std::io::{self, BufWriter, Write};
use std::path::Path;
use super::LogSink;
use crate::level::Level;
const FILE_BUF_CAPACITY: usize = 64 * 1024;
pub struct FileSink {
writer: BufWriter<File>,
}
impl FileSink {
fn from_file(file: File) -> Self {
Self {
writer: BufWriter::with_capacity(FILE_BUF_CAPACITY, file),
}
}
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = OpenOptions::new().create(true).append(true).open(path)?;
Ok(Self::from_file(file))
}
pub fn truncate<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
Ok(Self::from_file(file))
}
}
impl LogSink for FileSink {
fn accept(&mut self, line: &[u8], _level: Level) -> io::Result<()> {
self.writer.write_all(line)?;
self.writer.write_all(b"\n")
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Read;
fn temp_path(tag: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("ticklog_filesink_{tag}.log"));
let _ = fs::remove_file(&p);
p
}
fn read_to_string(path: &Path) -> String {
let mut s = String::new();
File::open(path)
.unwrap()
.read_to_string(&mut s)
.unwrap();
s
}
#[test]
fn accept_writes_line_and_newline_after_flush() {
let path = temp_path("write");
let mut sink = FileSink::new(&path).unwrap();
sink.accept(b"hello world", Level::Info).unwrap();
sink.flush().unwrap();
assert_eq!(read_to_string(&path), "hello world\n");
fs::remove_file(&path).unwrap();
}
#[test]
fn flush_persists_the_tail() {
let path = temp_path("flush_tail");
let mut sink = FileSink::new(&path).unwrap();
for i in 0..3 {
sink.accept(format!("line {i}").as_bytes(), Level::Info).unwrap();
}
sink.flush().unwrap();
assert_eq!(read_to_string(&path), "line 0\nline 1\nline 2\n");
fs::remove_file(&path).unwrap();
}
#[test]
fn new_appends_to_existing_content() {
let path = temp_path("append");
{
let mut sink = FileSink::new(&path).unwrap();
sink.accept(b"first", Level::Info).unwrap();
sink.flush().unwrap();
}
{
let mut sink = FileSink::new(&path).unwrap();
sink.accept(b"second", Level::Info).unwrap();
sink.flush().unwrap();
}
assert_eq!(read_to_string(&path), "first\nsecond\n");
fs::remove_file(&path).unwrap();
}
#[test]
fn truncate_discards_existing_content() {
let path = temp_path("truncate");
{
let mut sink = FileSink::new(&path).unwrap();
sink.accept(b"old", Level::Info).unwrap();
sink.flush().unwrap();
}
{
let mut sink = FileSink::truncate(&path).unwrap();
sink.accept(b"new", Level::Info).unwrap();
sink.flush().unwrap();
}
assert_eq!(read_to_string(&path), "new\n");
fs::remove_file(&path).unwrap();
}
}