NAME

App::FileSync::Verify - check a tree against its recorded manifest

SYNOPSIS

my $report = App::FileSync::Verify::verify(
	root => $master, entries => $index, ignore => $ignore, sample => 50,
);
print $report->{ok} ? "clean\n" : "drift\n";

FUNCTIONS

verify

Purpose: report where a tree no longer matches its record.

Args: root (required, a directory), entries (the recorded manifest), ignore (an App::FileSync::Ignore), deep (rehash every file), sample (without deep, rehash this many unchanged-looking files chosen at random).

Without deep, entries whose size and mtime still match are trusted, which is what makes a verify of a large tree cheap.

Returns: a hashref:

root, ok (1 when nothing differs), deep, files (regular files found),
known (entries in the record), rehashed, trusted, sampled,
modified => [ { p, expected, got } | { p, reason } | { p, expected, got, sampled } ],
missing  => [ { p, t } ],
extra    => [ { p, t } ]

Each list is sorted by path.

Side Effects: reads files; never writes.

Usage:

my $report = App::FileSync::Verify::verify(root => $root, entries => $base, deep => 1);

EXAMPLE

my $report = App::FileSync::Verify::verify(root => '/srv/filesync/master/docs', entries => $index);
print "missing: $_->{p}\n" for @{ $report->{missing} };

API SPECIFICATION

INPUT

{
	root    => { type => 'string', min => 1 },
	entries => { type => 'hashref', optional => 1 },
	ignore  => { type => 'object', optional => 1 },
	deep    => { type => 'boolean', optional => 1 },
	sample  => { type => 'integer', min => 0, optional => 1 },
}

OUTPUT

{
	type => 'hashref',
	schema => {
		ok       => { type => 'integer', min => 0, max => 1 },
		modified => { type => 'arrayref' },
		missing  => { type => 'arrayref' },
		extra    => { type => 'arrayref' },
	},
}

MESSAGES

Key                    Meaning                        Resolution
verify.no_root         root is not a directory        check the path
verify.type_changed    (a reason in "modified")       a file became a directory or back

FORMAL SPECIFICATION

let M = scan(root).manifest ; E = entries
missing  = { p ∈ dom E | p ¬in; dom M ∧ ¬(E(p).t = d ∧ isdir(root/p)) }
modified = { p ∈ dom E ∩ dom M | E(p).t ≠ M(p).t ∨ (E(p).t = f ∧ E(p).h ≠ M(p).h) }
extra    = dom M \ dom E \ (missing ∪ modified)
ok ⇔ missing = modified = extra = ∅

PSEUDOCODE

scan the tree, reusing recorded hashes unless deep
for each recorded path: missing, type changed, or hash differs
every scanned path not recorded is extra
if sampling: rehash that many trusted files and add any that differ
sort the three lists and count

LIMITATIONS

  • A shallow verify trusts size and mtime, so it cannot see a change that kept both; use deep or sample for that.

  • The report says what differs, not which side is right. After a restore from backup the index, not the tree, is usually the stale one; see filesync-admin reindex.

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

LICENCE AND COPYRIGHT

Copyright 2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.