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
Create a Separate Folder for the Backup and Manifest
SetupCreate 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.
mkdir -p integrity-check/backup/exportstouch integrity-check/backup/settings.initouch integrity-check/backup/exports/users.csvcd integrity-checkNew-Item -ItemType Directory -Force integrity-check/backup/exportsNew-Item -ItemType File -Force integrity-check/backup/settings.iniNew-Item -ItemType File -Force integrity-check/backup/exports/users.csvSet-Location integrity-check⯠View Expected Console Output
integrity-check/âââ manifest.pyâââ backup/ âââ settings.ini âââ exports/users.csvStep 2: Hash Files and Create the Manifest
Record SHA-256 Hashes in a JSON Manifest
Python ScriptAdd 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 Pathimport hashlibimport jsonimport 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
Detect Changed, Missing, and Unexpected Files
VerificationAdd 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 refrom 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.csvOK settings.iniVerified 2 file(s).Step 4: Add the Command-Line Interface and Run It
Create and Verify Manifests from the Terminal
ExecutionAdd 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())python3 manifest.py create backup backup-manifest.jsonpython3 manifest.py verify backup backup-manifest.jsonpython manifest.py create backup backup-manifest.jsonpython manifest.py verify backup backup-manifest.json⯠View Expected Console Output
Created manifest for 2 file(s): .../backup-manifest.jsonOK exports/users.csvOK settings.iniVerified 2 file(s).