#[cfg(any(windows, test))]
use std::path::Path;
#[cfg(windows)]
use std::{collections::BTreeSet, path::PathBuf};
#[cfg(windows)]
use tracing::{debug, warn};
#[cfg(any(windows, test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub journal_id: u64,
pub usn: i64,
}
#[cfg(any(windows, test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Coverage {
Covered,
Recreated,
Wrapped,
}
#[cfg(any(windows, test))]
pub fn coverage(since: Position, journal_id: u64, first_usn: i64) -> Coverage {
if journal_id != since.journal_id {
Coverage::Recreated
} else if since.usn < first_usn {
Coverage::Wrapped
} else {
Coverage::Covered
}
}
#[cfg(any(windows, test))]
fn drive_letter(volume: &Path) -> Option<char> {
volume
.to_str()?
.chars()
.next()
.filter(char::is_ascii_alphabetic)
}
#[cfg(windows)]
pub async fn position(volume: &Path) -> Option<Position> {
let volume = volume.to_path_buf();
tokio::task::spawn_blocking(move || {
let letter = drive_letter(&volume)?;
let handle = usn_journal_rs::volume::Volume::from_drive_letter(letter)
.inspect_err(|err| debug!("no change journal on {letter}: {err}"))
.ok()?;
let data = handle
.journal()
.query(false)
.inspect_err(|err| debug!("no change journal on {letter}: {err}"))
.ok()?;
let position = Position {
journal_id: data.journal_id,
usn: data.next_usn,
};
debug!(?position, "read {letter}'s change journal position");
Some(position)
})
.await
.ok()
.flatten()
}
#[cfg(windows)]
pub async fn changed_since(
volume: &Path,
since: Position,
root: &Path,
) -> Option<BTreeSet<PathBuf>> {
use usn_journal_rs::{journal::EnumOptions, volume::Volume};
let volume = volume.to_path_buf();
let root = root.to_path_buf();
tokio::task::spawn_blocking(move || {
let letter = drive_letter(&volume)?;
let handle = Volume::from_drive_letter(letter)
.inspect_err(|err| {
warn!("{letter} has no readable change journal, so the restore compared the trees: {err}")
})
.ok()?;
let data = handle.journal().query(false).ok()?;
match coverage(since, data.journal_id, data.first_usn) {
Coverage::Covered => {}
Coverage::Recreated => {
warn!(
"{letter}'s change journal has been recreated since the capture, \
so it no longer says what changed"
);
return None;
}
Coverage::Wrapped => {
warn!(
"{letter}'s change journal has wrapped past the capture, so it no \
longer holds every change since"
);
return None;
}
}
let journal = handle.journal();
let entries = journal
.iter_with_options(EnumOptions {
start_usn: since.usn,
..EnumOptions::default()
})
.ok()?;
let mut resolver = handle.path_resolver_with_cache();
let mut paths = BTreeSet::new();
let mut records = 0usize;
for entry in entries {
let entry = entry
.inspect_err(|err| {
warn!(
"{letter}'s change journal stopped partway, so the restore \
compared the trees instead: {err}"
)
})
.ok()?;
let Some(path) = resolver.resolve_path(&entry) else {
continue;
};
records += 1;
if let Ok(rel) = path.strip_prefix(&root) {
paths.insert(rel.to_path_buf());
}
}
if paths.is_empty() && records > 0 {
warn!(
records,
root = %root.display(),
"{letter}'s change journal resolved paths but none under the tree being \
restored, so the restore compared the trees instead",
);
return None;
}
debug!(
changed = paths.len(),
"{letter}'s change journal named what diverged from the capture"
);
Some(paths)
})
.await
.ok()
.flatten()
}
#[cfg(test)]
mod tests {
use super::*;
const AT_CAPTURE: Position = Position {
journal_id: 0x01d5_f4e2_c3b1_a098,
usn: 0x0012_3456,
};
#[test]
fn a_journal_still_holding_the_capture_can_answer() {
assert_eq!(
coverage(AT_CAPTURE, AT_CAPTURE.journal_id, 0x0001_0000),
Coverage::Covered
);
}
#[test]
fn a_position_at_the_very_first_record_is_still_covered() {
assert_eq!(
coverage(AT_CAPTURE, AT_CAPTURE.journal_id, AT_CAPTURE.usn),
Coverage::Covered
);
}
#[test]
fn a_ring_that_wrapped_past_the_capture_cannot_answer() {
assert_eq!(
coverage(AT_CAPTURE, AT_CAPTURE.journal_id, AT_CAPTURE.usn + 1),
Coverage::Wrapped
);
}
#[test]
fn a_recreated_journal_cannot_answer_however_its_numbering_looks() {
assert_eq!(coverage(AT_CAPTURE, 999, 0), Coverage::Recreated);
assert_eq!(coverage(AT_CAPTURE, 999, i64::MAX), Coverage::Recreated);
}
#[test]
fn reads_the_drive_letter_the_journal_api_wants() {
assert_eq!(drive_letter(Path::new("C:")), Some('C'));
assert_eq!(drive_letter(Path::new(r"D:\")), Some('D'));
assert_eq!(drive_letter(Path::new(r"\\?\Volume{abc}")), None);
assert_eq!(drive_letter(Path::new("")), None);
}
}