fix(backup): resolve a restored archive's owner by username, not by id (#2656)

created_by_id is only meaningful on the instance that wrote it. Restoring
onto a rebuilt instance - this feature's main use case - renumbers the
users table, so a live id can land on a different person and hand one
user's print history to another under archives:read_own. The id path
cannot even detect that: archivesOwnerCleared fires only for an id that
is absent, so a valid-but-wrong id produced no note at all.

The collector now records created_by_username alongside the id, and the
restore prefers it. username is unique on users, so a match is the same
person; the one case it cannot resolve - a user renamed since the backup
- falls through to ownerless with a note rather than guessing from the
id. The id stays as the fallback for backups taken before this change.
This commit is contained in:
jmoore-skild
2026-08-05 16:09:20 -04:00
parent f85e3e7fa1
commit 4ec0f3f9f2
16 changed files with 297 additions and 14 deletions
+14
View File
@@ -828,6 +828,14 @@ class GitHubBackupService:
if not archives:
return
# The natural key for an owner. created_by_id alone is only meaningful on
# the instance that wrote it: restoring onto a rebuilt instance — this
# feature's main use case — renumbers the users table, so a live id can
# land on a different person. username is unique on users, so the restore
# can resolve on it and treat a rename as unknown rather than guess.
# One query for the map; archives outnumber users by orders of magnitude.
user_names = dict((await db.execute(select(User.id, User.username))).all())
archive_list = []
for a in archives:
archive_data = {
@@ -876,6 +884,12 @@ class GitHubBackupService:
# restored row without it is invisible to everyone but an admin —
# while the restore reports it restored.
"created_by_id": a.created_by_id,
# Preferred over the id on restore; the id stays as the fallback
# for an owner whose row has since gone. Null when the archive
# has no owner, or when it points at a user row that no longer
# exists locally — the same "absent is not null" rule the restore
# applies, so a backup can't claim an owner it cannot name.
"created_by_username": user_names.get(a.created_by_id),
}
archive_list.append(archive_data)