118 lines
4.5 KiB
Python
118 lines
4.5 KiB
Python
"""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)
|