use super::archive_numbering::physical_archive_names;
use super::recovery::open_archive_numbers_for_writing;
use crate::error::Result;
use crate::tar_archive::archive::TarArchiveReader;
use crate::tar_archive::file_name::{ArchiveFileName, select_newest_file_generations};
use std::path::Path;
pub(super) fn check_and_update_manifest(directory: &Path) -> Result<()> {
#[allow(
clippy::case_sensitive_file_extension_comparisons,
reason = "the Java store matches \".tar\" case-sensitively"
)]
let archives_exist = std::fs::read_dir(directory)?.any(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.is_some_and(|name| name.ends_with(".tar"))
});
let archives = if archives_exist {
crate::store::ArchivePresence::Present
} else {
crate::store::ArchivePresence::Absent
};
crate::store::check_manifest(directory, archives)?;
std::fs::write(
directory.join("manifest"),
"#written by froe\nstore.version=2\n",
)?;
Ok(())
}
#[allow(
clippy::case_sensitive_file_extension_comparisons,
reason = "the Java store matches \".tar\" case-sensitively"
)]
pub(super) fn initialize_archives_for_writing(
directory: &Path,
observer: &mut dyn crate::progress::ProgressObserver,
) -> Result<Vec<TarArchiveReader>> {
let mut file_names = Vec::new();
for entry in std::fs::read_dir(directory)? {
let name = entry?.file_name();
if let Ok(name) = name.into_string()
&& name.ends_with(".tar")
{
file_names.push(name);
}
}
select_newest_file_generations(&file_names)?;
let mut by_number: std::collections::BTreeMap<u32, Vec<ArchiveFileName>> =
std::collections::BTreeMap::new();
for file_name in &file_names {
if let Some(parsed) = ArchiveFileName::parse(file_name) {
by_number
.entry(parsed.archive_number)
.or_default()
.push(parsed);
}
}
let archive_numbers = by_number.len();
let mut archives = crate::progress::observe(
observer,
&crate::progress::Step::new(
"opening archives for writing",
crate::progress::WorkUnit::Archives,
)
.with_total(crate::progress::count(archive_numbers)),
|observer| open_archive_numbers_for_writing(directory, by_number, observer),
)?;
archives.reverse();
Ok(archives)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RepairedArchive {
pub(crate) file_name: String,
pub(crate) reason: String,
pub(crate) bytes: u64,
}
pub(super) fn install_target_generation<'a>(
directory: &Path,
generations: &'a [ArchiveFileName],
) -> &'a ArchiveFileName {
generations
.iter()
.find(|generation| {
std::fs::metadata(directory.join(&generation.file_name))
.is_ok_and(|metadata| metadata.len() != 0)
})
.unwrap_or(&generations[0])
}
pub(super) fn existing_staging_residue(
directory: &Path,
generations: &[ArchiveFileName],
) -> Option<String> {
generations.iter().find_map(|generation| {
let name = format!("{}.recovering", generation.file_name);
std::fs::symlink_metadata(directory.join(&name))
.ok()
.filter(|metadata| metadata.is_file() && metadata.len() != 0)
.map(|_| name)
})
}
pub(crate) fn reject_duplicate_archive_generations(directory: &Path) -> Result<()> {
let names: Vec<String> = physical_archive_names(directory)?
.into_iter()
.map(|parsed| parsed.file_name)
.collect();
select_newest_file_generations(&names)?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::store::Repository;
use crate::writer::store_writer::repository::*;
use crate::writer::store_writer::test_support::*;
#[test]
fn bootstraps_a_fresh_store_that_the_reader_opens() {
let directory = TestDirectory::new("bootstrap");
let store = WritableRepository::open(&directory.path).expect("open fresh store");
store.close().expect("close");
let manifest =
std::fs::read_to_string(directory.path.join("manifest")).expect("manifest exists");
assert!(manifest.contains("store.version=2"));
assert!(directory.path.join("repo.lock").exists());
let journal = std::fs::read_to_string(directory.path.join("journal.log")).expect("journal");
assert_eq!(journal.lines().count(), 1, "exactly one bootstrap revision");
assert!(journal.contains(" root "));
let repository = Repository::open(&directory.path).expect("reader opens");
assert!(
!repository.archives()[0].is_recovered(),
"the archive has a valid index"
);
let content_root = repository.content_root().expect("content root exists");
assert_eq!(content_root.child_node_count().expect("count"), 0);
assert!(content_root.properties().expect("properties").is_empty());
}
#[test]
fn refuses_to_bootstrap_over_a_populated_store_with_no_resolvable_journal() {
let directory = TestDirectory::new("refuse-bootstrap");
{
let store = WritableRepository::open(&directory.path).expect("bootstrap");
crate::writer::commit::create_checkpoint(&store, 10_000_000, &[]).expect("checkpoint");
store.close().expect("close");
}
std::fs::write(directory.path.join("journal.log"), b"").expect("truncate journal");
assert!(
WritableRepository::open(&directory.path).is_err(),
"a populated store with no resolvable journal must not bootstrap an empty head"
);
crate::writer::backup::recover_journal(&directory.path).expect("recover");
let store = WritableRepository::open(&directory.path).expect("open after recovery");
store.close().expect("close");
}
#[test]
fn stale_generation_letters_are_deleted_at_write_open() {
let directory = TestDirectory::new("stale-letters");
{
let store = WritableRepository::open(&directory.path).expect("bootstrap");
store.close().expect("close");
}
let valid = std::fs::read(directory.path.join("data00000a.tar")).expect("read");
std::fs::write(directory.path.join("data00000b.tar"), &valid).expect("write copy");
{
let store = WritableRepository::open(&directory.path).expect("reopen");
assert!(store.head().record_number > 0 || store.head().record_number == 0);
store.close().expect("close");
}
assert!(
!directory.path.join("data00000a.tar").exists(),
"the lower letter is deleted"
);
assert!(directory.path.join("data00000b.tar").exists());
}
#[test]
fn an_empty_archive_file_is_never_deleted_by_opening_for_writing() {
let directory = TestDirectory::new("empty-archive-retained");
{
let store = WritableRepository::open(&directory.path).expect("bootstrap");
store.close().expect("close");
}
let empty = directory.path.join("data00500a.tar");
std::fs::write(&empty, b"").expect("create the empty archive");
let store = WritableRepository::open(&directory.path).expect("write open");
store.close().expect("close");
assert!(
empty.exists(),
"opening for writing must not delete the empty archive; cleanup removes it \
under its own plan-and-confirm contract"
);
}
#[test]
fn an_empty_archive_number_is_never_reallocated_over_its_own_residue() {
let directory = TestDirectory::new("empty-archive-namespace");
{
let store = WritableRepository::open(&directory.path).expect("bootstrap");
store.close().expect("close");
}
std::fs::write(directory.path.join("data00001.tar"), b"").expect("empty residue");
{
let store = WritableRepository::open(&directory.path).expect("write open");
let generation = store.writing_generation().expect("generation");
let mut writer = store.record_writer(generation);
writer.write_string("forces a new archive").expect("string");
writer.finish().expect("finish");
store.close().expect("close");
}
assert!(
!directory.path.join("data00001a.tar").exists(),
"allocation must skip the number the letterless residue claims"
);
Repository::open(&directory.path).expect("the store is still openable");
WritableRepository::open(&directory.path)
.expect("and still writable")
.close()
.expect("close");
}
}