use chrono::{DateTime, Local};
use fs4::fs_std::FileExt;
use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::{fs, io};
use crate::util;
pub const RECORD: &str = ".record";
#[derive(Debug)]
pub struct RecordItem {
pub time: String,
pub orig: PathBuf,
pub dest: PathBuf,
}
impl RecordItem {
pub fn new(line: &str) -> Self {
let mut tokens = line.split('\t');
let time = tokens.next().expect("Bad format: column 1").to_string();
let orig = tokens.next().expect("Bad format: column 2").to_string();
let dest = tokens.next().expect("Bad format: column 3").to_string();
Self {
time,
orig: PathBuf::from(orig),
dest: PathBuf::from(dest),
}
}
fn parse_timestamp(&self) -> Result<DateTime<Local>, Error> {
if let Ok(dt) = DateTime::parse_from_rfc3339(&self.time) {
return Ok(dt.with_timezone(&Local));
}
let is_old_format = self.time.split_whitespace().count() == 5
&& self
.time
.chars()
.all(|c| c.is_ascii_alphanumeric() || c.is_whitespace() || c == ':');
if is_old_format {
Err(Error::new(
ErrorKind::InvalidData,
format!(
"Found timestamp '{}' from old rip format. \
You will need to delete the `.record` file \
and start over with rip2. \
You can see the path with `rip graveyard`.",
self.time
),
))
} else {
Err(Error::new(
ErrorKind::InvalidData,
format!("Failed to parse time '{}' as RFC3339 format", self.time),
))
}
}
pub fn format_time_for_display(&self) -> Result<String, Error> {
self.parse_timestamp()
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string())
}
}
#[derive(Debug)]
pub struct Record<const FILE_LOCK: bool> {
path: PathBuf,
}
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_FILE_LOCK: bool = true;
#[cfg(target_os = "windows")]
pub const DEFAULT_FILE_LOCK: bool = false;
impl<const FILE_LOCK: bool> Record<FILE_LOCK> {
const HEADER: &'static str = "Time\tOriginal\tDestination";
pub fn new(graveyard: &Path) -> Self {
let path = graveyard.join(RECORD);
if !path.exists() {
let mut record_file = fs::OpenOptions::new()
.truncate(true)
.create(true)
.write(true)
.open(&path)
.expect("Failed to open record file");
if FILE_LOCK {
record_file.lock_exclusive().unwrap();
}
writeln!(record_file, "{}", Self::HEADER)
.expect("Failed to write header to record file");
}
Self { path }
}
pub fn open(&self) -> Result<fs::File, Error> {
let file = fs::File::open(&self.path)
.map_err(|_| Error::new(ErrorKind::NotFound, "Failed to read record!"))?;
if FILE_LOCK {
file.lock_exclusive().unwrap();
}
Ok(file)
}
pub fn get_last_bury(&self) -> Result<PathBuf, Error> {
let record_file = self.open()?;
let mut contents = String::new();
{
let mut reader = self.skip_header(BufReader::new(&record_file))?;
reader.read_to_string(&mut contents)?;
}
let mut graves_to_exhume: Vec<PathBuf> = Vec::new();
for entry in contents.lines().rev().map(RecordItem::new) {
if util::symlink_exists(&entry.dest) {
if !graves_to_exhume.is_empty() {
self.delete_lines(record_file, &graves_to_exhume)?;
}
return Ok(entry.dest);
}
graves_to_exhume.push(entry.dest);
}
if !graves_to_exhume.is_empty() {
self.delete_lines(record_file, &graves_to_exhume)?;
}
Err(Error::new(ErrorKind::NotFound, "No files in graveyard"))
}
fn delete_lines(&self, record_file: fs::File, graves: &[PathBuf]) -> Result<(), Error> {
let reader = self.skip_header(BufReader::new(record_file))?;
let lines_to_write: Vec<String> = reader
.lines()
.map_while(Result::ok)
.filter(|line| !graves.iter().any(|y| *y == RecordItem::new(line).dest))
.collect();
let mut new_record_file = fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&self.path)?;
if FILE_LOCK {
new_record_file.lock_exclusive().unwrap();
}
writeln!(new_record_file, "{}", Self::HEADER)?; for line in lines_to_write {
writeln!(new_record_file, "{line}")?;
}
Ok(())
}
pub fn log_exhumed_graves(&self, graves_to_exhume: &[PathBuf]) -> Result<(), Error> {
let record_file = self.open()?;
self.delete_lines(record_file, graves_to_exhume)
.map_err(|e| {
Error::new(
e.kind(),
format!("Failed to remove unburied files from record: {e}"),
)
})
}
pub fn lines_of_graves<'a>(
&'a self,
graves: &'a [PathBuf],
) -> impl Iterator<Item = String> + 'a {
let record_file = self.open().unwrap();
let reader = self.skip_header(BufReader::new(record_file)).unwrap();
reader
.lines()
.map_while(Result::ok)
.filter(move |line| graves.iter().any(|y| *y == RecordItem::new(line).dest))
}
pub fn seance<'a>(
&'a self,
gravepath: &'a PathBuf,
) -> io::Result<impl Iterator<Item = RecordItem> + 'a> {
let record_file = self.open()?;
let reader = self.skip_header(BufReader::new(record_file))?;
Ok(reader
.lines()
.map_while(Result::ok)
.map(|line| RecordItem::new(&line))
.filter(move |record_item| record_item.dest.starts_with(gravepath)))
}
pub fn write_log(&self, source: impl AsRef<Path>, dest: impl AsRef<Path>) -> io::Result<()> {
let (source, dest) = (source.as_ref(), dest.as_ref());
let mut record_file = fs::OpenOptions::new().append(true).open(&self.path)?;
if FILE_LOCK {
record_file.lock_exclusive().unwrap();
}
writeln!(
record_file,
"{}\t{}\t{}",
Local::now().to_rfc3339(),
source.display(),
dest.display()
)
.map_err(|e| {
Error::new(
e.kind(),
format!("Failed to write record at {}", &self.path.display()),
)
})?;
Ok(())
}
fn skip_header<R: BufRead>(&self, mut reader: R) -> io::Result<R> {
let mut header = String::new();
reader.read_line(&mut header)?;
if header.trim() != Self::HEADER {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Invalid record file header at {}:\n Expected: '{}'\n Got: '{}'",
&self.path.display(),
Self::HEADER,
header.trim()
),
));
}
Ok(reader)
}
}
impl<const FILE_LOCK: bool> Clone for Record<FILE_LOCK> {
fn clone(&self) -> Self {
Self {
path: self.path.clone(),
}
}
}