use super::AUDIT_FILE;
use super::TAG;
use crate::diagnostic_log::debug;
use std::ffi::OsStr;
use std::io::BufRead;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum Retention {
OneGeneration,
Generations,
}
pub(super) fn retention_of(target: &Path) -> Retention {
if target.file_name() == Some(OsStr::new(AUDIT_FILE)) {
return Retention::Generations;
}
return Retention::OneGeneration;
}
fn numbered(target: &Path, number: u64) -> PathBuf {
let mut name: std::ffi::OsString = target.as_os_str().to_os_string();
name.push(format!(".{number}"));
return PathBuf::from(name);
}
struct Piece {
path: PathBuf,
number: Option<u64>,
length: u64,
identity: (u64, u64),
}
fn list_pieces(target: &Path) -> std::io::Result<Vec<Piece>> {
let directory: &Path = target.parent().unwrap_or(Path::new("."));
let stem: String = target.file_name().map(|name| { return name.to_string_lossy().into_owned(); }).unwrap_or_default();
let mut pieces: Vec<Piece> = Vec::new();
for listed in std::fs::read_dir(directory)? {
let entry: std::fs::DirEntry = listed?;
let name: String = entry.file_name().to_string_lossy().into_owned();
let Some(rest) = name.strip_prefix(stem.as_str()).and_then(|after| { return after.strip_prefix('.'); }) else { continue };
let number: Option<u64> = if !rest.is_empty() && rest.bytes().all(|byte| { return byte.is_ascii_digit(); }) { rest.parse::<u64>().ok() } else { None };
if number.is_none() && !rest.starts_with("moving-") {
continue;
}
let Ok(found) = std::fs::symlink_metadata(entry.path()) else { continue };
if !found.is_file() {
continue;
}
pieces.push(Piece { path: entry.path(), number, length: found.len(), identity: (found.dev(), found.ino()) });
}
return Ok(pieces);
}
pub(super) fn install_generation(taken: &Path, target: &Path) -> std::io::Result<()> {
let highest: u64 = list_pieces(target)?.iter().filter_map(|piece| { return piece.number; }).max().unwrap_or(0);
let mut number: u64 = highest.saturating_add(1);
loop {
match std::fs::hard_link(taken, numbered(target, number)) {
Ok(()) => break,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => number = number.saturating_add(1),
Err(error) => return Err(error),
}
}
if let Err(error) = std::fs::remove_file(taken) {
debug(TAG, &format!("generation {number} of {} is in place but its private name {} stays: {error}", target.display(), taken.display()));
}
return Ok(());
}
const SAVED_CONFLICT_MARK: &[u8] = b"\"outcome\":\"saved-conflict\"";
const REPLACEMENT: char = '\u{FFFD}';
fn line_names_a_copy(line: &[u8]) -> bool {
let Ok(value) = serde_json::from_slice::<serde_json::Value>(line) else {
return true;
};
let Some(copies) = value.get("copies").and_then(|found| { return found.as_array(); }) else {
return false;
};
for copy in copies.iter().filter_map(|entry| { return entry.as_str(); }) {
for (index, _) in copy.match_indices(" => ") {
let place: &str = ©[index + " => ".len()..];
if place == "none" {
continue;
}
if place.contains(REPLACEMENT) || std::fs::symlink_metadata(place).is_ok() {
return true;
}
}
}
return false;
}
fn names_a_copy_on_disk(path: &Path) -> std::io::Result<bool> {
let mut reader: std::io::BufReader<std::fs::File> = std::io::BufReader::new(std::fs::File::open(path)?);
let mut line: Vec<u8> = Vec::new();
loop {
line.clear();
if reader.read_until(b'\n', &mut line)? == 0 {
return Ok(false);
}
if line.windows(SAVED_CONFLICT_MARK.len()).any(|window| { return window == SAVED_CONFLICT_MARK; }) && line_names_a_copy(line.as_slice()) {
return Ok(true);
}
}
}
pub(super) fn trim_generations(target: &Path, budget: u64) -> std::io::Result<()> {
let pieces: Vec<Piece> = list_pieces(target)?;
let mut generations: Vec<&Piece> = pieces.iter().filter(|piece| { return piece.number.is_some(); }).collect();
generations.sort_by_key(|piece| { return piece.number; });
let mut total: u64 = 0;
for piece in pieces.iter().filter(|piece| { return piece.number.is_none(); }) {
if generations.iter().any(|generation| { return generation.identity == piece.identity; }) {
if let Err(error) = std::fs::remove_file(&piece.path) {
debug(TAG, &format!("the second name {} of a generation could not be removed: {error}", piece.path.display()));
}
} else {
total = total.saturating_add(piece.length);
}
}
let mut candidates: Vec<&Piece> = Vec::new();
for generation in generations {
match names_a_copy_on_disk(generation.path.as_path()) {
Ok(false) => {
total = total.saturating_add(generation.length);
candidates.push(generation);
}
Ok(true) => {}
Err(error) => debug(TAG, &format!("{} was kept: it could not be read to see which copies it names: {error}", generation.path.display())),
}
}
for candidate in candidates {
if total <= budget {
break;
}
match std::fs::remove_file(&candidate.path) {
Ok(()) => total = total.saturating_sub(candidate.length),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => total = total.saturating_sub(candidate.length),
Err(error) => debug(TAG, &format!("{} was kept: it could not be removed: {error}", candidate.path.display())),
}
}
return Ok(());
}