App data: serialise restores of a package with a lock beside its data

Two clients restoring the same package could each swap directories and then
delete the other's pre-restore copy. The swap and retention cleanup now run
under flock on .<package>.restore.lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 22:30:08 +10:00
1 parent a7261b1601
commit 6c41a341e5
2 files changed
+49 -11

No files matched your search

+24 -11
View File
@@ -1,4 +1,5 @@
"""Private-data archives, run under podman unshare on the Frame. Stdlib only."""
import contextlib
import json
import os
from pathlib import Path, PurePosixPath
@@ -113,21 +114,33 @@ def restore(root, package, instance, input_stream):
apply_metadata(target, member)
for target, member in reversed(directories):
apply_metadata(target, member)
previous = root / ('.' + package + '.before-restore-' + str(time.time_ns()))
source.rename(previous)
try:
(stage / 'data').rename(source)
except BaseException:
previous.rename(source)
raise
# Keep only the newest pre-restore copy of this package's data.
for old in root.glob('.' + package + '.before-restore-*'):
if old != previous and not old.is_symlink():
shutil.rmtree(str(old), ignore_errors=True)
with package_lock(root, package): # another restore of this package must not delete our copy
previous = root / ('.' + package + '.before-restore-' + str(time.time_ns()))
source.rename(previous)
try:
(stage / 'data').rename(source)
except BaseException:
previous.rename(source)
raise
# Keep only the newest pre-restore copy of this package's data.
for old in root.glob('.' + package + '.before-restore-*'):
if old != previous and not old.is_symlink():
shutil.rmtree(str(old), ignore_errors=True)
result['previous'] = str(previous)
return result
@contextlib.contextmanager
def package_lock(root, package):
import fcntl
fd = os.open(str(root / ('.' + package + '.restore.lock')), os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
finally:
os.close(fd) # releases the lock
def apply_metadata(path, member):
os.chown(str(path), member.uid, member.gid)
os.chmod(str(path), member.mode & 0o777)