Files sitting on a backup drive or NAS are rarely opened, so silent corruption (bit rot) or an accidental delete can go unnoticed for months. Record a SHA-256 hash for every file and compare regularly, and you catch those changes right away. The script below uses only the standard library and runs on Python 3.8+ with nothing to install.
1. How it works
- First run: stores size, modification time and SHA-256 for every file in
manifest.json(your baseline) - Later runs: recalculates, compares with the baseline and reports four categories
| Category | Rule | Meaning |
|---|---|---|
| missing | In the baseline, not on disk | Deleted or moved |
| added | On disk, not in the baseline | New file |
| changed | Hash and mtime both changed | Probably a normal edit |
| corrupted | Hash changed but mtime did not | Possible corruption, check now |
2. The full script
"""Folder integrity check: the first run creates a baseline manifest; later runs report changed, missing and new files."""
import hashlib, json, sys, time
from pathlib import Path
CHUNK = 1024 * 1024 # read 1 MB at a time so huge files use little memory
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
while block := f.read(CHUNK):
h.update(block)
return h.hexdigest()
def scan(root: Path) -> dict:
result = {}
for p in root.rglob("*"):
if p.is_file():
st = p.stat()
result[str(p.relative_to(root))] = {"size": st.st_size, "mtime": int(st.st_mtime), "sha256": sha256(p)}
return result
def main(root: str, manifest: str = "manifest.json"):
root_p, man_p = Path(root), Path(manifest)
now = scan(root_p)
if not man_p.exists():
man_p.write_text(json.dumps(now, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"Baseline created: {len(now)} files -> {man_p}")
return 0
base = json.loads(man_p.read_text(encoding="utf-8"))
missing = sorted(set(base) - set(now))
added = sorted(set(now) - set(base))
changed, corrupted = [], []
for k in set(base) & set(now):
if base[k]["sha256"] != now[k]["sha256"]:
# content changed but mtime did not: possible corruption (bit rot)
(corrupted if base[k]["mtime"] == now[k]["mtime"] else changed).append(k)
report = {"time": time.strftime("%Y-%m-%d %H:%M:%S"), "missing": missing, "added": added,
"changed": sorted(changed), "corrupted": sorted(corrupted)}
print(json.dumps(report, ensure_ascii=False, indent=1))
return 1 if (missing or corrupted) else 0
if __name__ == "__main__":
sys.exit(main(*sys.argv[1:]))
Files are read in 1 MB chunks, so even multi-gigabyte video files use almost no memory. The script exits with code 1 when anything is missing or possibly corrupted, so schedulers and wrapper scripts can treat it as a failure.
3. Running it
# First run: create the baseline (keep the manifest outside the folder you check)
python integrity_check.py "D:\Backup\Photos" "C:\check\photos_manifest.json"
# Every run after that prints the comparison
python integrity_check.py "D:\Backup\Photos" "C:\check\photos_manifest.json"
If the manifest lives inside the checked folder, it shows up as “changed” every time, so keep it outside. After legitimate additions or edits, delete the manifest and run again to create a fresh baseline.
4. Run it daily
Windows Task Scheduler: register it from an elevated Command Prompt and append results to a log file.
schtasks /Create /TN "IntegrityCheck" /SC DAILY /ST 03:00 /TR "cmd /c python C:\check\integrity_check.py D:\Backup\Photos C:\check\photos_manifest.json >> C:\check\integrity.log 2>&1"
Linux or NAS (cron): add a line with crontab -e.
0 3 * * * /usr/bin/python3 /opt/check/integrity_check.py /volume1/photos /opt/check/photos_manifest.json >> /opt/check/integrity.log 2>&1
5. Tips
- Large folders: hashing runs at roughly 100–200 MB/s on a hard drive, so several TB takes hours. Split folders and check a different one each day.
- Alerts: wrap the script so it sends an email or chat webhook only when the exit code is 1.
- Verify backups: build the manifest from the source folder and use it to check the backup copy. You will know whether the backup is a faithful copy.