feat(delivery): deduplicate bounded cold backups
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""Private content storage; every logical source byte is read before deduplication."""
|
||||
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from . import file_copy
|
||||
from .policy import DeliveryError
|
||||
|
||||
CHUNK = 1024 * 1024
|
||||
|
||||
|
||||
def identity(info: os.stat_result) -> tuple[int, ...]:
|
||||
return file_copy._identity(info)
|
||||
|
||||
|
||||
def read_stream(incoming, before, deadline: float, outgoing=None) -> str:
|
||||
if identity(os.fstat(incoming.fileno())) != identity(before):
|
||||
raise DeliveryError("backup source changed before capture")
|
||||
count, digest = 0, hashlib.sha256()
|
||||
while block := incoming.read(CHUNK):
|
||||
count += len(block)
|
||||
if count > before.st_size or time.monotonic() >= deadline:
|
||||
raise DeliveryError("backup source or deadline changed")
|
||||
digest.update(block)
|
||||
if outgoing is not None:
|
||||
outgoing.write(block)
|
||||
if (
|
||||
count != before.st_size
|
||||
or time.monotonic() >= deadline
|
||||
or identity(os.fstat(incoming.fileno())) != identity(before)
|
||||
):
|
||||
raise DeliveryError("backup source changed during capture")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def store(
|
||||
source: Path,
|
||||
before,
|
||||
objects: Path,
|
||||
seen: dict[str, int],
|
||||
deadline: float,
|
||||
*,
|
||||
remaining_bytes: int,
|
||||
) -> tuple[str, int]:
|
||||
"""Hash every source; copy only new content, with a second full byte check.
|
||||
|
||||
The one fixed pending file is exclusive. An interrupted copy remains private
|
||||
and prevents adoption of the incomplete snapshot. Publishing never replaces
|
||||
an existing object, and the final object has exactly one link.
|
||||
"""
|
||||
if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
|
||||
raise DeliveryError("special or linked backup source refused")
|
||||
with os.fdopen(
|
||||
os.open(source, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC),
|
||||
"rb",
|
||||
) as incoming:
|
||||
digest = read_stream(incoming, before, deadline)
|
||||
added = 0
|
||||
if digest in seen:
|
||||
if seen[digest] != before.st_size:
|
||||
raise DeliveryError("backup content size conflict")
|
||||
else:
|
||||
if before.st_size > remaining_bytes:
|
||||
raise DeliveryError("private backup physical capacity exhausted")
|
||||
pending = objects / ".pending"
|
||||
with os.fdopen(
|
||||
os.open(
|
||||
pending,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
|
||||
0o600,
|
||||
),
|
||||
"wb",
|
||||
) as outgoing:
|
||||
os.fchmod(outgoing.fileno(), 0o600)
|
||||
incoming.seek(0)
|
||||
if read_stream(incoming, before, deadline, outgoing) != digest:
|
||||
raise DeliveryError("backup source bytes changed between reads")
|
||||
outgoing.flush()
|
||||
written = os.fstat(outgoing.fileno())
|
||||
if identity(pending.lstat()) != identity(written):
|
||||
raise DeliveryError("private backup pending object changed")
|
||||
os.link(pending, objects / digest, follow_symlinks=False)
|
||||
# Only this operation's completed, exclusively created staging link.
|
||||
pending.unlink()
|
||||
seen[digest] = before.st_size
|
||||
added = before.st_size
|
||||
if identity(source.lstat()) != identity(before):
|
||||
raise DeliveryError("backup source path changed during capture")
|
||||
return digest, added
|
||||
|
||||
|
||||
def sync_filesystem(directory: Path, deadline: float) -> None:
|
||||
"""Flush the dedicated backup filesystem before publishing its manifest.
|
||||
|
||||
Linux syncfs reports delayed write errors and waits for data and metadata.
|
||||
The existing service deadline bounds a blocked filesystem operation too.
|
||||
"""
|
||||
if time.monotonic() >= deadline:
|
||||
raise DeliveryError("private backup capture deadline before synchronization")
|
||||
descriptor = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC)
|
||||
try:
|
||||
libc = ctypes.CDLL(None, use_errno=True)
|
||||
try:
|
||||
syncfs = libc.syncfs
|
||||
except AttributeError as error:
|
||||
raise DeliveryError("private backup filesystem synchronization unavailable") from error
|
||||
syncfs.argtypes, syncfs.restype = [ctypes.c_int], ctypes.c_int
|
||||
if syncfs(descriptor) != 0:
|
||||
raise DeliveryError("private backup filesystem synchronization failed")
|
||||
if time.monotonic() >= deadline:
|
||||
raise DeliveryError("private backup capture deadline after synchronization")
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
Reference in New Issue
Block a user