Skip to content

Create and Verify File Integrity Manifests with Python

When you move an archive, export, or configuration bundle between systems, file names and sizes alone cannot tell you whether its contents changed. A SHA-256 manifest records a digest for each file so you can check the files again later.

This guide builds a small command-line tool using only Python’s standard library. It creates a JSON manifest and verifies files against it, reporting changed, missing, and unexpected files.


Script Architecture


Step 1: Set Up a Working Directory

01

Create a Separate Folder for the Backup and Manifest

Setup

Create a project folder with a backup directory for the files you want to check. Keep the manifest beside that directory so the scan does not include the manifest itself. Save the Python script as manifest.py in the project folder. Python 3.9 or newer is recommended.

Terminal window
mkdir -p integrity-check/backup/exports
touch integrity-check/backup/settings.ini
touch integrity-check/backup/exports/users.csv
cd integrity-check
❯ View Expected Console Output
integrity-check/
├── manifest.py
└── backup/
├── settings.ini
└── exports/users.csv

Step 2: Hash Files and Create the Manifest

02

Record SHA-256 Hashes in a JSON Manifest

Python Script

Add this code to manifest.py. It reads files in 1 MiB chunks, then writes each file’s relative path and digest to backup-manifest.json in a stable order.

from pathlib import Path
import hashlib
import json
import sys
CHUNK_SIZE = 1024 * 1024
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def files_under(root: Path):
return sorted(
(path for path in root.rglob("*") if path.is_file() and not path.is_symlink()),
key=lambda path: path.relative_to(root).as_posix(),
)
def create_manifest(root: Path, manifest_path: Path) -> int:
root = root.resolve()
manifest_path = manifest_path.resolve()
if not root.is_dir():
print(f"Error: directory not found: {root}", file=sys.stderr)
return 2
if manifest_path.is_relative_to(root):
print("Error: save the manifest outside the directory being scanned.", file=sys.stderr)
return 2
entries = [
{"path": path.relative_to(root).as_posix(), "sha256": sha256_file(path)}
for path in files_under(root)
]
data = {"algorithm": "sha256", "files": entries}
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(f"Created manifest for {len(entries)} file(s): {manifest_path}")
return 0
❯ View Expected Console Output
{
"algorithm": "sha256",
"files": [
{
"path": "exports/users.csv",
"sha256": "<64-character SHA-256 digest>"
},
{
"path": "settings.ini",
"sha256": "<64-character SHA-256 digest>"
}
]
}

Step 3: Verify the Files Against the Manifest

03

Detect Changed, Missing, and Unexpected Files

Verification

Add this function to manifest.py. It checks each recorded digest, reports files that are missing or changed, and flags files that were added after the manifest was created. Invalid manifest paths are rejected before they are checked.

import re
from pathlib import PurePosixPath
def verify_manifest(root: Path, manifest_path: Path) -> int:
root = root.resolve()
try:
data = json.loads(manifest_path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError("manifest must contain a JSON object")
if data.get("algorithm") != "sha256" or not isinstance(data.get("files"), list):
raise ValueError("unsupported or invalid manifest format")
expected = {}
for entry in data["files"]:
if not isinstance(entry, dict):
raise ValueError("each manifest file entry must be a JSON object")
relative_path = entry["path"]
digest = entry["sha256"]
if not isinstance(relative_path, str) or not relative_path:
raise ValueError("manifest contains an invalid relative path")
parsed_path = PurePosixPath(relative_path)
if parsed_path.is_absolute() or ".." in parsed_path.parts:
raise ValueError("manifest contains an invalid relative path")
if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise ValueError(f"invalid SHA-256 digest for {relative_path}")
if relative_path in expected:
raise ValueError(f"duplicate path in manifest: {relative_path}")
expected[relative_path] = digest
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error:
print(f"Error reading manifest: {error}", file=sys.stderr)
return 2
if not root.is_dir():
print(f"Error: directory not found: {root}", file=sys.stderr)
return 2
actual_paths = {
path.relative_to(root).as_posix(): path for path in files_under(root)
}
failed = False
for relative_path, expected_hash in sorted(expected.items()):
path = actual_paths.get(relative_path)
if path is None:
print(f"MISSING {relative_path}")
failed = True
elif sha256_file(path) != expected_hash:
print(f"CHANGED {relative_path}")
failed = True
else:
print(f"OK {relative_path}")
for relative_path in sorted(actual_paths.keys() - expected.keys()):
print(f"UNEXPECTED {relative_path}")
failed = True
if failed:
print("Verification failed.")
return 1
print(f"Verified {len(expected)} file(s).")
return 0
❯ View Expected Console Output
OK exports/users.csv
OK settings.ini
Verified 2 file(s).

Step 4: Add the Command-Line Interface and Run It

04

Create and Verify Manifests from the Terminal

Execution

Add the entry point below to the end of manifest.py. Create a baseline manifest, then run the verify command whenever you want to check the backup. Verification exits with status 1 when files differ and 2 when the directory or manifest is invalid.

import argparse
def main() -> int:
parser = argparse.ArgumentParser(description="Create or verify a SHA-256 file manifest.")
commands = parser.add_subparsers(dest="command", required=True)
create = commands.add_parser("create", help="create a manifest")
create.add_argument("directory", type=Path, help="directory to scan")
create.add_argument("manifest", type=Path, help="output JSON file, outside the scanned directory")
verify = commands.add_parser("verify", help="verify a directory against a manifest")
verify.add_argument("directory", type=Path, help="directory to scan")
verify.add_argument("manifest", type=Path, help="JSON manifest to check")
args = parser.parse_args()
if args.command == "create":
return create_manifest(args.directory, args.manifest)
return verify_manifest(args.directory, args.manifest)
if __name__ == "__main__":
raise SystemExit(main())
Terminal window
python3 manifest.py create backup backup-manifest.json
python3 manifest.py verify backup backup-manifest.json
❯ View Expected Console Output
Created manifest for 2 file(s): .../backup-manifest.json
OK exports/users.csv
OK settings.ini
Verified 2 file(s).

Comments