use std::fs;
use std::fs::remove_file;
use std::iter::{once, zip};
use std::mem::take;
use std::path::{Path, PathBuf};
use crate::logs::completed_log::CompletedLog;
use eyre::Result;
use log::{debug, warn};
use uuid::Uuid;
use crate::mar::CompressionAlgorithm;
use crate::util::fs::get_files_sorted_by_mtime;
use crate::util::path::file_prefix;
struct FileInfo {
path: PathBuf,
uuid: Option<Uuid>,
size: Option<u64>,
}
#[derive(Debug, PartialEq)]
struct LogFileToRecover {
path: PathBuf,
cid: Uuid,
next_cid: Uuid,
}
#[derive(Debug, PartialEq)]
struct Recovery {
to_delete: Vec<PathBuf>,
to_recover: Vec<LogFileToRecover>,
next_cid: Uuid,
}
fn should_recover(file_info: &FileInfo) -> bool {
match file_info {
FileInfo { uuid: None, .. } => false,
FileInfo { size: None, .. } => false,
FileInfo {
size: Some(size), ..
} if *size == 0 => false,
_ => true,
}
}
fn get_recovery(file_infos: Vec<FileInfo>, gen_uuid: fn() -> Uuid) -> Recovery {
let last_cid = file_infos
.iter()
.filter_map(|info| match info {
FileInfo {
uuid: Some(uuid), ..
} => match should_recover(info) {
true => Some(gen_uuid()),
false => Some(*uuid),
},
_ => None,
})
.next_back()
.unwrap_or_else(gen_uuid);
let (mut to_recover_infos, to_delete_infos): (Vec<FileInfo>, Vec<FileInfo>) =
file_infos.into_iter().partition(should_recover);
#[allow(clippy::needless_collect)]
let next_cids: Vec<Uuid> = to_recover_infos
.iter()
.skip(1)
.map(|i| i.uuid.expect("No UUID present"))
.chain(once(last_cid))
.collect();
Recovery {
to_delete: to_delete_infos.into_iter().map(|info| info.path).collect(),
to_recover: zip(to_recover_infos.iter_mut(), next_cids)
.map(|(info, next_cid)| LogFileToRecover {
path: take(&mut info.path),
cid: info.uuid.expect("No UUID present"),
next_cid,
})
.collect(),
next_cid: last_cid,
}
}
pub fn recover_old_logs<R: FnMut(CompletedLog) -> Result<()> + Send + 'static>(
tmp_logs: &Path,
on_log_recovery: &mut R,
) -> Result<Uuid> {
let file_infos = get_files_sorted_by_mtime(tmp_logs)?
.into_iter()
.map(|path| {
let uuid = file_prefix(&path)
.and_then(|prefix| Uuid::parse_str(&prefix.to_string_lossy()).ok());
let size = fs::metadata(&path).ok().map(|metadata| metadata.len());
FileInfo { path, uuid, size }
})
.collect();
let Recovery {
next_cid,
to_delete,
to_recover,
} = get_recovery(file_infos, Uuid::new_v4);
for path in to_delete {
if let Err(e) = remove_file(&path) {
warn!(
"Unable to delete bogus log file: {} - {}.",
path.display(),
e
);
}
}
for LogFileToRecover {
path,
cid,
next_cid,
} in to_recover
{
debug!("Recovering logfile: {:?}", path.display());
if let Err(e) = (on_log_recovery)(CompletedLog {
path: path.clone(),
cid,
next_cid,
compression: CompressionAlgorithm::Zlib,
}) {
warn!("Unable to recover log file, deleting instead: {}", e);
if let Err(e) = remove_file(&path) {
warn!("Unable to delete log file: {}", e);
}
}
}
Ok(next_cid)
}
#[cfg(test)]
mod tests {
use super::*;
const UUID_A: Uuid = Uuid::from_u128(1);
const UUID_B: Uuid = Uuid::from_u128(2);
const UUID_NEW: Uuid = Uuid::from_u128(3);
const PATH_A: &str = "/tmp_log/11111111-1111-1111-1111-111111111111";
const PATH_B: &str = "/tmp_log/22222222-2222-2222-2222-222222222222";
#[test]
fn empty_logging_directory() {
let file_infos: Vec<FileInfo> = vec![];
let expected = Recovery {
to_delete: vec![],
to_recover: vec![],
next_cid: UUID_NEW,
};
assert_eq!(get_recovery(file_infos, gen_uuid), expected);
}
#[test]
fn delete_improperly_named_files() {
let file_infos = vec![FileInfo {
path: PathBuf::from("/tmp_log/foo"),
uuid: None,
size: Some(1),
}];
let expected = Recovery {
to_delete: vec![PathBuf::from("/tmp_log/foo")],
to_recover: vec![],
next_cid: UUID_NEW,
};
assert_eq!(get_recovery(file_infos, gen_uuid), expected);
}
#[test]
fn use_empty_trailing_uuid_named_file_as_next_cid() {
let file_infos = vec![
FileInfo {
path: PATH_A.into(),
uuid: Some(UUID_A),
size: Some(1),
},
FileInfo {
path: PATH_B.into(),
uuid: Some(UUID_B),
size: Some(0),
},
];
let expected = Recovery {
to_delete: vec![PATH_B.into()],
to_recover: vec![LogFileToRecover {
path: PATH_A.into(),
cid: UUID_A,
next_cid: UUID_B,
}],
next_cid: UUID_B,
};
assert_eq!(get_recovery(file_infos, gen_uuid), expected);
}
#[test]
fn dont_use_non_trailing_empty_uuid_named_file_as_next_cid() {
let file_infos = vec![
FileInfo {
path: PATH_A.into(),
uuid: Some(UUID_A),
size: Some(0),
},
FileInfo {
path: PATH_B.into(),
uuid: Some(UUID_B),
size: Some(1),
},
];
let expected = Recovery {
to_delete: vec![PATH_A.into()],
to_recover: vec![LogFileToRecover {
path: PATH_B.into(),
cid: UUID_B,
next_cid: UUID_NEW,
}],
next_cid: UUID_NEW,
};
assert_eq!(get_recovery(file_infos, gen_uuid), expected);
}
#[test]
fn chain_cids() {
let file_infos = vec![
FileInfo {
path: PATH_A.into(),
uuid: Some(UUID_A),
size: Some(1),
},
FileInfo {
path: PATH_B.into(),
uuid: Some(UUID_B),
size: Some(1),
},
];
let expected = Recovery {
to_delete: vec![],
to_recover: vec![
LogFileToRecover {
path: PATH_A.into(),
cid: UUID_A,
next_cid: UUID_B,
},
LogFileToRecover {
path: PATH_B.into(),
cid: UUID_B,
next_cid: UUID_NEW,
},
],
next_cid: UUID_NEW,
};
assert_eq!(get_recovery(file_infos, gen_uuid), expected);
}
fn gen_uuid() -> Uuid {
UUID_NEW
}
}