use std::collections::HashSet;
use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::cache::BoundedCache;
use crate::content::node::NodeState;
use crate::content::provider::SegmentProvider;
use crate::error::{Error, Result};
use crate::progress::{DiscardedProgress, ProgressObserver, Step, WorkUnit};
use crate::segment::identifier::SegmentIdentifier;
use crate::segment::record::RecordIdentifier;
use crate::store::{ArchiveSet, Repository};
use crate::writer::compaction::deep_copy_tree_across_stores_with_progress;
use crate::writer::repository_lock::RepositoryLock;
use crate::writer::segment_builder::GarbageCollectionGeneration;
use crate::writer::store_writer::WritableRepository;
mod consistency;
mod recover;
mod scan;
#[cfg(test)]
mod test_support;
pub(crate) use consistency::*;
pub use recover::*;
pub(crate) use scan::*;
pub fn backup(source_directory: &Path, target_directory: &Path) -> Result<()> {
backup_with_progress(source_directory, target_directory, &mut DiscardedProgress)
}
pub fn backup_with_progress(
source_directory: &Path,
target_directory: &Path,
observer: &mut dyn ProgressObserver,
) -> Result<()> {
let source = Repository::open_with_progress(source_directory, observer)?;
let target = WritableRepository::open_with_progress(target_directory, observer)?;
copy_head_between(
&source,
source.head_record_identifier(),
&target,
"b",
observer,
)?;
target.close()
}
pub fn restore(backup_directory: &Path, target_directory: &Path) -> Result<()> {
restore_with_progress(backup_directory, target_directory, &mut DiscardedProgress)
}
pub fn restore_with_progress(
backup_directory: &Path,
target_directory: &Path,
observer: &mut dyn ProgressObserver,
) -> Result<()> {
let backup = Repository::open_with_progress(backup_directory, observer)?;
let target = WritableRepository::open_with_progress(target_directory, observer)?;
copy_head_between(
&backup,
backup.head_record_identifier(),
&target,
"r",
observer,
)?;
target.close()
}
pub(crate) fn copy_head_between(
source: &Repository,
source_head: RecordIdentifier,
target: &WritableRepository,
writer_identifier: &str,
observer: &mut dyn ProgressObserver,
) -> Result<()> {
let generation = source_head_generation(source, source_head)?;
let mut writer = target.record_writer_with_identifier(generation, writer_identifier);
let (new_head, _) = crate::progress::observe(
observer,
&Step::new("copying nodes", WorkUnit::Nodes),
|observer| {
deep_copy_tree_across_stores_with_progress(source, &mut writer, source_head, observer)
},
)?;
writer.finish()?;
target.replace_head(new_head);
target.flush()
}
pub(crate) fn source_head_generation(
source: &Repository,
source_head: RecordIdentifier,
) -> Result<GarbageCollectionGeneration> {
for archive in source.archives() {
if let Some(entry) = archive.index_entry(source_head.segment) {
return Ok(GarbageCollectionGeneration {
generation: entry.generation,
full_generation: entry.full_generation,
is_compacted: entry.is_compacted,
});
}
}
let view = source.segment(source_head.segment)?;
Ok(GarbageCollectionGeneration {
generation: view.structure.generation,
full_generation: view.structure.full_generation,
is_compacted: view.structure.is_compacted,
})
}
#[cfg(test)]
mod tests {
use super::{backup, restore};
use crate::content::node::PropertyValues;
use crate::content::property::PropertyValue;
use crate::store::Repository;
use crate::writer::backup::test_support::{
TestDirectory, assert_content, populate, populate_with_bulk_binary,
};
use crate::writer::store_writer::WritableRepository;
#[test]
fn backup_copies_content_and_checkpoints() {
let source = TestDirectory::new("backup-source");
let target = TestDirectory::new("backup-target");
populate(&source.path);
backup(&source.path, &target.path).expect("backup");
assert_content(&target.path, "Backup Source");
let repository = Repository::open(&target.path).expect("reader");
assert_eq!(
repository.checkpoints().expect("checkpoints").len(),
1,
"checkpoints are copied with the head"
);
assert_content(&source.path, "Backup Source");
}
#[test]
fn a_backup_carries_binary_content_that_lived_in_a_bulk_segment() {
let source = TestDirectory::new("backup-bulk-source");
let target = TestDirectory::new("backup-bulk-target");
let content = populate_with_bulk_binary(&source.path);
backup(&source.path, &target.path).expect("backup");
let repository = Repository::open(&target.path).expect("open the backup");
let resource = repository
.content_root()
.expect("content root")
.child_node("file")
.expect("read")
.expect("the file node is present");
let data = resource
.property("jcr:data")
.expect("read")
.expect("jcr:data is present");
let PropertyValues::Single(PropertyValue::Binary(binary)) = &data.values else {
panic!("jcr:data did not decode as a single binary: {data:?}");
};
let crate::content::value::BinaryValue::Inline {
record_identifier, ..
} = binary
else {
panic!("expected an inline binary, got {binary:?}");
};
let copied = crate::content::value::read_binary_content(&repository, *record_identifier)
.expect("the backup holds every block of the binary");
assert_eq!(
copied.len(),
content.len(),
"the backup holds the whole binary, not a prefix of it"
);
assert!(
copied == content,
"the binary in the backup is byte-identical to the source's"
);
}
#[test]
fn restore_overwrites_the_target_head() {
let backup_store = TestDirectory::new("restore-backup");
let target = TestDirectory::new("restore-target");
populate(&backup_store.path);
{
let store = WritableRepository::open(&target.path).expect("bootstrap target");
store.close().expect("close");
}
restore(&backup_store.path, &target.path).expect("restore");
assert_content(&target.path, "Backup Source");
}
#[test]
fn backup_stamps_the_source_head_generation_verbatim() {
let source = TestDirectory::new("backup-generation-source");
let target = TestDirectory::new("backup-generation-target");
populate(&source.path);
{
let mut store = WritableRepository::open(&source.path).expect("open");
crate::writer::compaction::compact(
&mut store,
crate::writer::compaction::CompactionKind::Full,
)
.expect("compact");
store.close().expect("close");
}
let source_head_generation = {
let repository = Repository::open(&source.path).expect("reader");
let head_segment = repository.head_record_identifier().segment;
repository
.archives()
.iter()
.find_map(|archive| archive.index_entry(head_segment))
.map(|entry| (entry.generation, entry.full_generation, entry.is_compacted))
.expect("head segment is indexed")
};
assert_ne!(
source_head_generation.0, 0,
"compaction advanced the source generation"
);
backup(&source.path, &target.path).expect("backup");
let repository = Repository::open(&target.path).expect("target reader");
let head_segment = repository.head_record_identifier().segment;
let stamped = repository
.archives()
.iter()
.find_map(|archive| archive.index_entry(head_segment))
.map(|entry| (entry.generation, entry.full_generation, entry.is_compacted))
.expect("target head segment is indexed");
assert_eq!(
stamped, source_head_generation,
"the source head's generation triple is stamped verbatim"
);
}
}