Skip to main content

FileSentinel

Integrity monitoring and repair toolkit for long-term file archives

Python versions PyPI Tests Coverage Code style: black

Motivation

Files sitting on disk for years quietly rot: a bit flips, a drive develops a bad sector, a sync tool truncates a file. Nothing reports an error — the file is simply no longer what you put there. Backups don't necessarily help, since a corrupted file gets faithfully backed up right over the good copy once the corruption goes unnoticed long enough.

FileSentinel treats an archive folder as content to be watched rather than merely stored. It records a SHA-256 manifest of every file and tells you exactly what changed since the last time you looked. If you opt into redundancy, it stores par2 recovery data alongside the manifest so corrupted files can be repaired in place, without restoring from a backup.

Intentional changes are just as important as unintentional ones: adding, deleting, and renaming files are all reported, and renames are detected as move operations by digest rather than showing up as an unrelated delete/add pair.

Getting started

First, install from PyPI:

pip install trilium-alchemy

No system-level dependencies are required for the core functionality. FileSentinel just needs the par2 binary on PATH for redundancy and repair, and ddrescue if you want to rip disks:

sudo apt install par2 gddrescue

Point it at a folder to take an initial snapshot, optionally creating par2 redundancy data as it goes:

filesentinel scan --apply --par2 /path/to/archive

Later, check whether anything has changed:

filesentinel scan /path/to/archive

And if a file has been corrupted, restore it from redundancy data:

filesentinel repair /path/to/archive

Concepts

Archive layout

An archive is simply a folder of files, plus a manifest folder holding everything FileSentinel maintains:

archive/
├── docs/
│   └── report.pdf
├── photo.jpg
└── .filesentinel/
    ├── config.yaml
    ├── manifest.yaml
    ├── manifest.yaml.sha256
    └── tree/
        └── ab/
            └── cd/
                └── abcd1234.../
                    ├── redundancy.par2
                    └── redundancy.vol000+01.par2

The .filesentinel folder is excluded from scans. It can also live outside the archive root entirely — pass --manifest to keep it on separate media, for example when the archive itself is read-only or lives on removable storage.

Redundancy data is content-addressed: par2 sets live under tree/ keyed by the digest of the file they protect, sharded 2 levels deep to keep directory sizes manageable. Identical files therefore share a single par2 set, and moving or renaming a file doesn't invalidate its redundancy data.

Change types

A scan compares the files on disk against the manifest and reports:

Change Meaning
add File on disk, not in the manifest
delete File in the manifest, missing from disk
move A delete and an add with the same digest, paired up
edit Same path, different content: possible corruption

If a file is marked with edit, it means the content no longer matches what was recorded. If you did not change the file yourself, this is exactly the case repair addresses.

CLI

Global options apply to all commands and come before the command name:

filesentinel --manifest /media/backup/archive-manifest scan /path/to/archive
  • --manifest, -m: path to the manifest folder (default: <root>/.filesentinel)
  • --config, -c: path to the config file (default: <manifest>/config.yaml)

Most commands accept trailing glob patterns to restrict the paths considered, matched against archive-relative posix paths:

filesentinel scan /path/to/archive 'docs/*.pdf'

list

Show the manifest contents without touching the disk:

filesentinel list /path/to/archive

Use --depth/-d to limit how deep into the tree to report.

scan

Walk the archive, hash its files, and report differences from the manifest:

filesentinel scan /path/to/archive
  • --fast: skip hashing for files whose size and mtime both match the manifest. Much faster for large archives, but blind to corruption that preserves both — use it for routine checks, not for integrity audits.
  • --par2: also scan the par2 redundancy data itself for damage, and maintain it when combined with --apply.
  • --apply: update the manifest to reflect the detected changes, prompting first unless -y/--yes is given.
  • --verify: exit with a nonzero status if anything differs, for use in cron jobs and CI.

Applying with --par2 keeps redundancy data in sync: par2 sets are created for new content and dropped once no manifest entry references their digest.

repair

Scan the archive and restore files reported as edit from their par2 sets:

filesentinel repair /path/to/archive

Each repaired file has its par2 set rebuilt afterward, since par2 repair consumes it. Files that are missing outright, or whose damage exceeds the available redundancy, are reported as unrepairable and cause a nonzero exit status. Files on disk but not in the manifest are reported as extraneous and left alone.

rip

Rip a disk to an .iso inside the archive and add it to the manifest, using ddrescue with a conservative two-pass strategy (a fast pass without scraping, then a scraping pass with retries only if needed):

filesentinel rip /path/to/archive /dev/sr0 discs/backup-2015.iso --parents

Redundancy data

Par2 redundancy is optional and off by default, since generating it costs time and disk space proportional to the archive. Without it, FileSentinel still detects corruption — it just can't undo it.

The recovery target defaults to 10% of file size, meaning roughly that fraction of a file can be damaged and still recovered. Raise it for irreplaceable data or media you don't trust; lower it if space is tight.

Because the par2 files are themselves subject to the same rot they protect against, their digests are recorded in the manifest too. scan --par2 verifies them, and scan --par2 --apply rebuilds any set that has drifted — from the current archive file, after confirming that file still matches its recorded digest. If it doesn't, the rebuild is refused: the user data is suspect and should go through repair first.

Manifest integrity

The manifest is the reference against which everything else is judged, so it gets a sidecar of its own: manifest.yaml.sha256, written whenever changes are applied. Every scan reports whether the manifest still matches its sidecar.

An unexpected mismatch means the manifest itself was modified or damaged, and its verdict about your files can't be trusted — worth noticing before concluding that a hundred files were edited.

Configuration

Optional config.yaml, living in the manifest folder by default:

# par2 redundancy target, as a percentage of file size
recovery_pct: 10

# glob patterns to exclude from scans, relative to the archive root
ignore_globs:
  - "**/*.tmp"
  - "cache/**"

Python API

The CLI is a thin layer over the Archive class, which is usable directly:

from pathlib import Path
from filesentinel import Archive

archive = Archive(Path("/path/to/archive"))

# report differences from the manifest
changes = archive.scan()
for change in changes:
    print(change.change_type, change.new or change.old)

# commit them, maintaining par2 data
archive.apply(changes, par2=True)
archive.manifest.update_index_sha()

# repair corrupted files
report = archive.repair(archive.scan())
assert report.ok

Archive.verify() is a convenience for the common case: it returns True only if a full scan of both files and par2 data comes back clean.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

filesentinel-0.1.0.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

filesentinel-0.1.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file filesentinel-0.1.0.tar.gz.

File metadata

  • Download URL: filesentinel-0.1.0.tar.gz
  • Upload date:
  • Size: 16.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for filesentinel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 02c207d8e914ee63cb5f85da9779ec2e806cdfdaf41891031629329dc33fc64e
MD5 bc7a9aa7f2f674f0d084d482643aa747
BLAKE2b-256 614c4d8530878c54e55d94f6b3943a66a7be57752d9ad0b336bbc46237ff55e7

See more details on using hashes here.

File details

Details for the file filesentinel-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: filesentinel-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for filesentinel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 492f9052baa27aff29f64eb7ff4db83271702d7d0e5a288b141ab84cd3dd28cb
MD5 144a82950ac55b6cab14ef5538e599e7
BLAKE2b-256 7affd4b9a93f13b28d2e6bfa9e041095b46adfc75bfbdc601c6ec54ea189fcf6

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page