Streaming File Checksums With SHA-256
Compute file checksums in fixed-size chunks, support alternate hashlib algorithms, and verify expected digests in constant time.
Code
from __future__ import annotations
import hashlib
import hmac
from pathlib import Path
class ChecksumError(ValueError):
"""Raised when a checksum cannot be computed."""
def stream_checksum(
path: str | Path,
*,
algorithm: str = "sha256",
chunk_size: int = 1024 * 1024,
) -> str:
if not isinstance(chunk_size, int) or isinstance(chunk_size, bool):
raise TypeError("chunk_size must be an integer")
if chunk_size < 1:
raise ValueError("chunk_size must be positive")
try:
digest = hashlib.new(algorithm)
except ValueError as error:
raise ChecksumError(f"unsupported algorithm: {algorithm}") from error
file_path = Path(path)
try:
with file_path.open("rb") as handle:
while chunk := handle.read(chunk_size):
digest.update(chunk)
except OSError as error:
raise ChecksumError(f"cannot read {file_path}: {error}") from error
return digest.hexdigest()
def verify_checksum(
path: str | Path,
expected: str,
*,
algorithm: str = "sha256",
chunk_size: int = 1024 * 1024,
) -> bool:
if not isinstance(expected, str) or not expected.strip():
raise ValueError("expected checksum must be a non-empty string")
actual = stream_checksum(
path,
algorithm=algorithm,
chunk_size=chunk_size,
)
return hmac.compare_digest(actual, expected.strip().lower())
def main() -> None:
import tempfile
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "payload.bin"
path.write_bytes(b"devexamples" * 1000)
digest = stream_checksum(path, chunk_size=256)
print(digest)
print(verify_checksum(path, digest))
print(verify_checksum(path, "0" * 64))
try:
stream_checksum(path, algorithm="not-a-hash")
except ChecksumError as error:
print(error)
if __name__ == "__main__":
main()Report a problem with this exampleOpens a pre-filled issue on GitHub.