mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -942,14 +942,17 @@ class GitHubRestoreService:
|
||||
# (_ensure_archive_visible fails closed on it) and never appears in the
|
||||
# ownership-scoped list queries. Hoisted like the two above.
|
||||
#
|
||||
# Note this is the one place a raw backup id is reused, against the
|
||||
# module's own rule at the top of the file. Users have no natural key the
|
||||
# backup carries today, and the id is validated rather than trusted, so a
|
||||
# *stale* id clears instead of pointing somewhere wrong. What it cannot
|
||||
# catch is a live id belonging to a different person on a different
|
||||
# instance. Collecting username and resolving on that would close it;
|
||||
# raised with the maintainer rather than decided here.
|
||||
valid_users = set((await db.execute(select(User.id))).scalars().all())
|
||||
# username is the natural key and wins, per the module's rule at the top
|
||||
# of the file; created_by_id is the fallback for a pre-#2656 commit that
|
||||
# carries no username. That ordering is what makes restoring onto a
|
||||
# rebuilt instance safe: the users table renumbers there, so a live id
|
||||
# can land on a different person, and the id path alone cannot tell that
|
||||
# from a correct match. Resolving on the name instead means the one case
|
||||
# it cannot resolve — a user renamed since the backup — falls through to
|
||||
# ownerless-with-a-note below rather than misattributing in silence.
|
||||
users = (await db.execute(select(User.id, User.username))).all()
|
||||
valid_users = {user_id for user_id, _ in users}
|
||||
users_by_name = {username: user_id for user_id, username in users}
|
||||
|
||||
# Only metadata is backed up, never the 3MF/thumbnail bytes, and
|
||||
# print_archives.file_path is NOT NULL — so inserted rows get an empty
|
||||
@@ -1008,7 +1011,7 @@ class GitHubRestoreService:
|
||||
fields["printer_id"] = printer_id
|
||||
fields["project_id"] = project_id
|
||||
|
||||
# created_by_id and deleted_at are the two late arrivals — a backup
|
||||
# The ownership pair and deleted_at are the late arrivals — a backup
|
||||
# commit taken before the collector wrote them carries neither key.
|
||||
# Absent is NOT the same as null here, because the overwrite branch
|
||||
# below is a blanket setattr: treating a missing key as None would
|
||||
@@ -1018,13 +1021,33 @@ class GitHubRestoreService:
|
||||
# deleted. So only carry a column the backup actually knows about;
|
||||
# on insert, an absent key just takes the model default.
|
||||
owner_cleared = False
|
||||
if "created_by_id" in entry:
|
||||
backup_username = entry.get("created_by_username")
|
||||
if isinstance(backup_username, str) and backup_username:
|
||||
# The natural-key path. A miss here is a user renamed or deleted
|
||||
# since the backup, and there is nothing else to resolve on: the
|
||||
# id alongside it is from the source instance's numbering, so
|
||||
# trusting it is exactly the misattribution the name is here to
|
||||
# prevent. Cleared rather than failing the row — the archive is
|
||||
# still worth having, and an admin can reassign it — but said out
|
||||
# loud, because a cleared owner is not silent-safe.
|
||||
created_by_id = users_by_name.get(backup_username)
|
||||
if created_by_id is None:
|
||||
tally.note(
|
||||
"archivesOwnerUnmatched",
|
||||
"Some archives name an owner this instance does not have — owner cleared rather than "
|
||||
"guessed from the backup's user id, so they are visible only to users with the "
|
||||
"archives:read_all permission until an admin reassigns them",
|
||||
)
|
||||
owner_cleared = True
|
||||
fields["created_by_id"] = created_by_id
|
||||
elif "created_by_id" in entry:
|
||||
# Fallback for a commit taken before the collector recorded the
|
||||
# username. Validated rather than trusted, so a *stale* id clears
|
||||
# instead of pointing somewhere wrong; a live id belonging to a
|
||||
# different person on a rebuilt instance is the case this path
|
||||
# cannot see, and is why the branch above exists.
|
||||
created_by_id = entry.get("created_by_id")
|
||||
if created_by_id is not None and created_by_id not in valid_users:
|
||||
# Coerced rather than failing the row: the archive is still
|
||||
# worth having, and an admin can reassign it. Said out loud
|
||||
# because a cleared owner is not silent-safe — the archive
|
||||
# becomes visible only to archives:read_all until someone does.
|
||||
tally.note(
|
||||
"archivesOwnerCleared",
|
||||
"Some archives referenced users that no longer exist — owner cleared, so they are "
|
||||
|
||||
@@ -2390,6 +2390,239 @@ class TestRestoredArchiveOwnership:
|
||||
assert row.created_by_id == user.id, "a restored archive its owner cannot see is not restored"
|
||||
|
||||
|
||||
class TestArchiveOwnerNaturalKey:
|
||||
"""``created_by_username`` decides the owner; the id is only the fallback.
|
||||
|
||||
Restoring onto a rebuilt instance is this feature's main use case, and the
|
||||
users table renumbers there. A raw ``created_by_id`` cannot tell a correct
|
||||
match from a live id that now belongs to somebody else, so the id path hands
|
||||
one person's print history to another under ``ARCHIVES_READ_OWN`` — silently,
|
||||
because ``archivesOwnerCleared`` only fires for an id that is *absent*.
|
||||
``username`` is unique on ``users``, so resolving on it turns that silent
|
||||
misattribution into an ownerless row with a note.
|
||||
"""
|
||||
|
||||
def _entry(self, **overrides):
|
||||
entry = {
|
||||
"id": 77,
|
||||
"filename": "benchy.3mf",
|
||||
"file_size": 2048,
|
||||
"content_hash": "abc123",
|
||||
"started_at": "2026-03-01 10:00:00",
|
||||
"created_at": "2026-03-01 10:00:00",
|
||||
}
|
||||
entry.update(overrides)
|
||||
return entry
|
||||
|
||||
async def _user(self, db, username):
|
||||
user = User(username=username, role="operator")
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
return user
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_name_resolves_across_a_renumbered_users_table(self, db_session):
|
||||
"""The whole point: same person, different id, restore still finds them."""
|
||||
alice = await self._user(db_session, "alice")
|
||||
tally = _CategoryTally()
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session,
|
||||
{"archives": [self._entry(created_by_id=alice.id + 500, created_by_username="alice")]},
|
||||
False,
|
||||
tally,
|
||||
{},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id == alice.id
|
||||
assert not any("owner cleared" in note for note in _messages(tally))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_name_beats_a_live_id_belonging_to_someone_else(self, db_session):
|
||||
"""The misattribution case, and the one the id path cannot even detect.
|
||||
|
||||
Both ids exist locally, so the id path would write bob's — a valid row,
|
||||
no note, alice's print history readable by bob.
|
||||
"""
|
||||
alice = await self._user(db_session, "alice")
|
||||
bob = await self._user(db_session, "bob")
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session,
|
||||
{"archives": [self._entry(created_by_id=bob.id, created_by_username="alice")]},
|
||||
False,
|
||||
_CategoryTally(),
|
||||
{},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id == alice.id, "the name is the natural key; the id is from another instance"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_renamed_owner_lands_ownerless_with_a_note(self, db_session):
|
||||
"""No local match, so nothing to resolve — and the id is not a fallback here.
|
||||
|
||||
Falling back to it is exactly the guess the name exists to prevent, so
|
||||
the row is cleared and said out loud instead.
|
||||
"""
|
||||
bob = await self._user(db_session, "bob")
|
||||
tally = _CategoryTally()
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session,
|
||||
{"archives": [self._entry(created_by_id=bob.id, created_by_username="alice")]},
|
||||
False,
|
||||
tally,
|
||||
{},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id is None
|
||||
assert tally.restored == 1 and tally.failed == 0
|
||||
assert any("does not have" in note and "archives:read_all" in note for note in _messages(tally))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_unmatched_note_is_emitted_once_for_many_rows(self, db_session):
|
||||
tally = _CategoryTally()
|
||||
archives = [
|
||||
self._entry(id=1, content_hash="h1", filename="a.3mf", created_by_username="alice"),
|
||||
self._entry(id=2, content_hash="h2", filename="b.3mf", created_by_username="carol"),
|
||||
]
|
||||
|
||||
await _service()._restore_archives(db_session, {"archives": archives}, False, tally, {})
|
||||
await db_session.commit()
|
||||
|
||||
assert sum(1 for note in _messages(tally) if "does not have" in note) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_unmatched_name_does_not_also_claim_no_owner_was_recorded(self, db_session):
|
||||
"""One row, one cause, one note — as with the stale-id branch."""
|
||||
tally = _CategoryTally()
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session, {"archives": [self._entry(created_by_username="alice")]}, False, tally, {}
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
assert any("does not have" in note for note in _messages(tally))
|
||||
assert not any("without an owner" in note for note in _messages(tally))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_pre_username_backup_still_resolves_on_the_id(self, db_session):
|
||||
"""The fallback has to keep working — every backup taken before this change."""
|
||||
alice = await self._user(db_session, "alice")
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session, {"archives": [self._entry(created_by_id=alice.id)]}, False, _CategoryTally(), {}
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id == alice.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_explicitly_ownerless_archive_reads_as_no_owner_not_as_unmatched(self, db_session):
|
||||
"""A current-format backup of an unowned archive writes both keys null."""
|
||||
tally = _CategoryTally()
|
||||
|
||||
await _service()._restore_archives(
|
||||
db_session,
|
||||
{"archives": [self._entry(created_by_id=None, created_by_username=None)]},
|
||||
False,
|
||||
tally,
|
||||
{},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
assert any("without an owner" in note for note in _messages(tally))
|
||||
assert not any("does not have" in note for note in _messages(tally))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_overwrite_leaves_the_owner_alone_when_neither_key_is_present(self, db_session):
|
||||
"""The absent-is-not-null rule still holds now that there are two keys."""
|
||||
bob = await self._user(db_session, "bob")
|
||||
db_session.add(
|
||||
PrintArchive(
|
||||
filename="benchy.3mf",
|
||||
file_path="/data/benchy.3mf",
|
||||
file_size=2048,
|
||||
content_hash="abc123",
|
||||
started_at=datetime(2026, 3, 1, 10, 0, 0),
|
||||
created_by_id=bob.id,
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
await _service()._restore_archives(db_session, {"archives": [self._entry()]}, True, _CategoryTally(), {})
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id == bob.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_name_survives_collect_then_restore(self, db_session):
|
||||
"""Both halves, because the collector writing nothing looks harmless alone."""
|
||||
from backend.app.services.github_backup import github_backup_service
|
||||
|
||||
alice = await self._user(db_session, "alice")
|
||||
db_session.add(
|
||||
PrintArchive(
|
||||
filename="owned.3mf",
|
||||
file_path="",
|
||||
file_size=1024,
|
||||
content_hash="hash-owned",
|
||||
started_at=datetime(2026, 3, 1, 10, 0, 0),
|
||||
created_by_id=alice.id,
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
files: dict = {}
|
||||
await github_backup_service._collect_archives(db_session, files)
|
||||
payload = files[ARCHIVES_PATH]
|
||||
assert payload["archives"][0]["created_by_username"] == "alice"
|
||||
|
||||
# Rebuilt instance: same person, and nothing else holds their old id.
|
||||
await db_session.execute(PrintArchive.__table__.delete())
|
||||
await db_session.execute(User.__table__.delete())
|
||||
await db_session.commit()
|
||||
rebuilt = await self._user(db_session, "alice")
|
||||
await db_session.commit()
|
||||
|
||||
await _service()._restore_archives(db_session, payload, False, _CategoryTally(), {})
|
||||
await db_session.commit()
|
||||
|
||||
row = (await db_session.execute(select(PrintArchive))).scalar_one()
|
||||
assert row.created_by_id == rebuilt.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_collector_names_no_owner_for_an_unowned_archive(self, db_session):
|
||||
"""Null rather than absent, so a restore can tell "none" from "not recorded"."""
|
||||
from backend.app.services.github_backup import github_backup_service
|
||||
|
||||
db_session.add(
|
||||
PrintArchive(
|
||||
filename="unowned.3mf",
|
||||
file_path="",
|
||||
file_size=1024,
|
||||
content_hash="hash-unowned",
|
||||
started_at=datetime(2026, 3, 1, 10, 0, 0),
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
files: dict = {}
|
||||
await github_backup_service._collect_archives(db_session, files)
|
||||
|
||||
entry = files[ARCHIVES_PATH]["archives"][0]
|
||||
assert entry["created_by_username"] is None
|
||||
assert "created_by_username" in entry
|
||||
|
||||
|
||||
class TestCategoryPathMapping:
|
||||
def setup_method(self):
|
||||
self.service = _service()
|
||||
|
||||
@@ -4912,6 +4912,7 @@ export default {
|
||||
archivesPrinterMissing: 'Einige Archive verwiesen auf nicht mehr vorhandene Drucker - Verknüpfung entfernt',
|
||||
archivesProjectMissing: 'Einige Archive verwiesen auf nicht mehr vorhandene Projekte - Verknüpfung entfernt',
|
||||
archivesOwnerCleared: 'Einige Archive verwiesen auf nicht mehr vorhandene Benutzer - Eigentümer entfernt. Sie sind daher nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',
|
||||
archivesOwnerUnmatched: 'Einige Archive nennen einen Eigentümer, den es auf dieser Instanz nicht gibt - der Eigentümer wurde entfernt statt aus der Benutzer-ID der Sicherung geraten. Sie sind daher nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',
|
||||
archivesOwnerUnknown: 'Einige Archive wurden ohne Eigentümer wiederhergestellt - diese Sicherung enthält keinen, daher sind sie nur für Benutzer mit der Berechtigung archives:read_all sichtbar, bis ein Administrator sie neu zuweist',
|
||||
archivesUndeleted: 'Seit dem Backup gelöschte Archive sind wieder sichtbar - Überschreiben war aktiv',
|
||||
archivesMetadataOnly: 'Wiederhergestellte Archive enthalten nur Metadaten - die 3MF- und Vorschaudateien sind nicht im Git-Backup enthalten',
|
||||
|
||||
@@ -4960,6 +4960,7 @@ export default {
|
||||
archivesPrinterMissing: 'Some archives referenced printers that no longer exist - link cleared',
|
||||
archivesProjectMissing: 'Some archives referenced projects that no longer exist - link cleared',
|
||||
archivesOwnerCleared: 'Some archives referenced users that no longer exist - owner cleared, so they are visible only to users with the archives:read_all permission until an admin reassigns them',
|
||||
archivesOwnerUnmatched: 'Some archives name an owner this instance does not have - owner cleared rather than guessed from the backup\'s user id, so they are visible only to users with the archives:read_all permission until an admin reassigns them',
|
||||
archivesOwnerUnknown: 'Some archives were restored without an owner - this backup does not record one, so they are visible only to users with the archives:read_all permission until an admin reassigns them',
|
||||
archivesUndeleted: 'Archive(s) deleted since the backup are visible again - overwrite was on',
|
||||
archivesMetadataOnly: 'Restored archives carry metadata only - the 3MF and thumbnail files are not in a Git backup',
|
||||
|
||||
@@ -4920,6 +4920,7 @@ export default {
|
||||
archivesPrinterMissing: 'Algunos archivos hacían referencia a impresoras que ya no existen - enlace eliminado',
|
||||
archivesProjectMissing: 'Algunos archivos hacían referencia a proyectos que ya no existen - enlace eliminado',
|
||||
archivesOwnerCleared: 'Algunos archivos hacían referencia a usuarios que ya no existen - se ha borrado el propietario, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',
|
||||
archivesOwnerUnmatched: 'Algunos archivos indican un propietario que no existe en esta instancia - se ha borrado el propietario en lugar de deducirlo del id de usuario de la copia de seguridad, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',
|
||||
archivesOwnerUnknown: 'Algunos archivos se restauraron sin propietario - esta copia de seguridad no registra ninguno, por lo que solo son visibles para usuarios con el permiso archives:read_all hasta que un administrador los reasigne',
|
||||
archivesUndeleted: 'Los archivos eliminados desde la copia vuelven a estar visibles - la sobrescritura estaba activada',
|
||||
archivesMetadataOnly: 'Los archivos restaurados solo contienen metadatos - los ficheros 3MF y las miniaturas no están en una copia de Git',
|
||||
|
||||
@@ -4901,6 +4901,7 @@ export default {
|
||||
archivesPrinterMissing: 'Certaines archives référençaient des imprimantes qui n\'existent plus - lien effacé',
|
||||
archivesProjectMissing: 'Certaines archives référençaient des projets qui n\'existent plus - lien effacé',
|
||||
archivesOwnerCleared: 'Certaines archives référençaient des utilisateurs qui n\'existent plus - propriétaire effacé, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',
|
||||
archivesOwnerUnmatched: 'Certaines archives désignent un propriétaire absent de cette instance - le propriétaire a été effacé plutôt que déduit de l\'identifiant utilisateur de la sauvegarde, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',
|
||||
archivesOwnerUnknown: 'Certaines archives ont été restaurées sans propriétaire - cette sauvegarde n\'en enregistre aucun, elles ne sont donc visibles que par les utilisateurs disposant de la permission archives:read_all jusqu\'à ce qu\'un administrateur les réattribue',
|
||||
archivesUndeleted: 'Les archives supprimées depuis la sauvegarde sont de nouveau visibles - l\'écrasement était activé',
|
||||
archivesMetadataOnly: 'Les archives restaurées ne contiennent que des métadonnées - les fichiers 3MF et les miniatures ne sont pas dans une sauvegarde Git',
|
||||
|
||||
@@ -4900,6 +4900,7 @@ export default {
|
||||
archivesPrinterMissing: 'Alcuni archivi facevano riferimento a stampanti non più esistenti - collegamento rimosso',
|
||||
archivesProjectMissing: 'Alcuni archivi facevano riferimento a progetti non più esistenti - collegamento rimosso',
|
||||
archivesOwnerCleared: 'Alcuni archivi facevano riferimento a utenti non più esistenti - proprietario rimosso, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',
|
||||
archivesOwnerUnmatched: 'Alcuni archivi indicano un proprietario che questa istanza non ha - il proprietario è stato rimosso anziché dedotto dall\'id utente del backup, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',
|
||||
archivesOwnerUnknown: 'Alcuni archivi sono stati ripristinati senza proprietario - questo backup non ne registra alcuno, quindi sono visibili solo agli utenti con il permesso archives:read_all finché un amministratore non li riassegna',
|
||||
archivesUndeleted: 'Gli archivi eliminati dopo il backup sono di nuovo visibili - la sovrascrittura era attiva',
|
||||
archivesMetadataOnly: 'Gli archivi ripristinati contengono solo metadati - i file 3MF e le miniature non sono in un backup Git',
|
||||
|
||||
@@ -4912,6 +4912,7 @@ export default {
|
||||
archivesPrinterMissing: '一部のアーカイブが存在しないプリンターを参照していました - リンクを解除しました',
|
||||
archivesProjectMissing: '一部のアーカイブが存在しないプロジェクトを参照していました - リンクを解除しました',
|
||||
archivesOwnerCleared: '一部のアーカイブが存在しないユーザーを参照していました - 所有者を解除したため、管理者が割り当て直すまで archives:read_all 権限を持つユーザーにしか表示されません',
|
||||
archivesOwnerUnmatched: '一部のアーカイブはこのインスタンスに存在しない所有者を指しています - バックアップのユーザー ID から推測せずに所有者を解除したため、管理者が割り当て直すまで archives:read_all 権限を持つユーザーにしか表示されません',
|
||||
archivesOwnerUnknown: '一部のアーカイブは所有者なしで復元されました - このバックアップに所有者が記録されていないため、管理者が割り当てるまで archives:read_all 権限を持つユーザーにしか表示されません',
|
||||
archivesUndeleted: 'バックアップ後に削除されたアーカイブが再び表示されます - 上書きが有効でした',
|
||||
archivesMetadataOnly: '復元されたアーカイブはメタデータのみです - 3MF ファイルとサムネイルは Git バックアップに含まれません',
|
||||
|
||||
@@ -4677,6 +4677,7 @@ export default {
|
||||
archivesPrinterMissing: '일부 아카이브가 더 이상 존재하지 않는 프린터를 참조했습니다 - 연결을 해제했습니다',
|
||||
archivesProjectMissing: '일부 아카이브가 더 이상 존재하지 않는 프로젝트를 참조했습니다 - 연결을 해제했습니다',
|
||||
archivesOwnerCleared: '일부 아카이브가 더 이상 존재하지 않는 사용자를 참조했습니다 - 소유자를 비웠으므로 관리자가 다시 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',
|
||||
archivesOwnerUnmatched: '일부 아카이브가 이 인스턴스에 없는 소유자를 가리킵니다 - 백업의 사용자 ID로 추측하지 않고 소유자를 비웠으므로 관리자가 다시 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',
|
||||
archivesOwnerUnknown: '일부 아카이브가 소유자 없이 복원되었습니다 - 이 백업에 소유자가 기록되어 있지 않으므로 관리자가 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다',
|
||||
archivesUndeleted: '백업 이후 삭제된 아카이브가 다시 표시됩니다 - 덮어쓰기가 켜져 있었습니다',
|
||||
archivesMetadataOnly: '복원된 아카이브에는 메타데이터만 있습니다 - 3MF 파일과 썸네일은 Git 백업에 포함되지 않습니다',
|
||||
|
||||
@@ -4900,6 +4900,7 @@ export default {
|
||||
archivesPrinterMissing: 'Alguns arquivos referenciavam impressoras que não existem mais - vínculo removido',
|
||||
archivesProjectMissing: 'Alguns arquivos referenciavam projetos que não existem mais - vínculo removido',
|
||||
archivesOwnerCleared: 'Alguns arquivos referenciavam usuários que não existem mais - o proprietário foi limpo, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os reatribua',
|
||||
archivesOwnerUnmatched: 'Alguns arquivos apontam para um proprietário que esta instância não tem - o proprietário foi limpo em vez de deduzido do id de usuário da cópia, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os reatribua',
|
||||
archivesOwnerUnknown: 'Alguns arquivos foram restaurados sem proprietário - este backup não registra nenhum, então eles só ficam visíveis para usuários com a permissão archives:read_all até que um administrador os atribua',
|
||||
archivesUndeleted: 'Arquivos excluídos desde o backup voltaram a ficar visíveis - a sobrescrita estava ligada',
|
||||
archivesMetadataOnly: 'Os arquivos restaurados contêm apenas metadados - os arquivos 3MF e as miniaturas não ficam em um backup Git',
|
||||
|
||||
@@ -4669,6 +4669,7 @@ export default {
|
||||
archivesPrinterMissing: 'Некоторые архивы ссылались на несуществующие принтеры - связь очищена',
|
||||
archivesProjectMissing: 'Некоторые архивы ссылались на несуществующие проекты - связь очищена',
|
||||
archivesOwnerCleared: 'Некоторые архивы ссылались на несуществующих пользователей - владелец очищен, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца заново',
|
||||
archivesOwnerUnmatched: 'Некоторые архивы указывают владельца, которого нет в этом экземпляре - владелец очищен, а не угадан по идентификатору пользователя из резервной копии, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца заново',
|
||||
archivesOwnerUnknown: 'Некоторые архивы восстановлены без владельца - в этой резервной копии он не записан, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца',
|
||||
archivesUndeleted: 'Архивы, удалённые после резервного копирования, снова видны - перезапись была включена',
|
||||
archivesMetadataOnly: 'Восстановленные архивы содержат только метаданные - файлы 3MF и миниатюры не входят в резервную копию Git',
|
||||
|
||||
@@ -4890,6 +4890,7 @@ export default {
|
||||
archivesPrinterMissing: 'Bazı arşivler artık var olmayan yazıcılara işaret ediyordu - bağlantı temizlendi',
|
||||
archivesProjectMissing: 'Bazı arşivler artık var olmayan projelere işaret ediyordu - bağlantı temizlendi',
|
||||
archivesOwnerCleared: 'Bazı arşivler artık var olmayan kullanıcılara işaret ediyordu - sahip temizlendi, bu yüzden bir yönetici yeniden atayana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',
|
||||
archivesOwnerUnmatched: 'Bazı arşivler bu örnekte bulunmayan bir sahibi belirtiyor - sahip, yedekteki kullanıcı kimliğinden tahmin edilmek yerine temizlendi, bu yüzden bir yönetici yeniden atayana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',
|
||||
archivesOwnerUnknown: 'Bazı arşivler sahipsiz geri yüklendi - bu yedek sahip bilgisi içermiyor, bu yüzden bir yönetici atama yapana kadar yalnızca archives:read_all iznine sahip kullanıcılara görünürler',
|
||||
archivesUndeleted: 'Yedekten sonra silinen arşivler yeniden görünür oldu - üzerine yazma açıktı',
|
||||
archivesMetadataOnly: 'Geri yüklenen arşivler yalnızca üst veri içerir - 3MF dosyaları ve küçük resimler Git yedeğinde yer almaz',
|
||||
|
||||
@@ -4955,6 +4955,7 @@ export default {
|
||||
archivesPrinterMissing: "Деякі архіви посилалися на принтери, яких більше немає - зв'язок очищено",
|
||||
archivesProjectMissing: "Деякі архіви посилалися на проєкти, яких більше немає - зв'язок очищено",
|
||||
archivesOwnerCleared: "Деякі архіви посилалися на користувачів, яких більше немає - власника очищено, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника знову",
|
||||
archivesOwnerUnmatched: "Деякі архіви вказують власника, якого немає в цьому екземплярі - власника очищено, а не вгадано за ідентифікатором користувача з резервної копії, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника знову",
|
||||
archivesOwnerUnknown: "Деякі архіви відновлено без власника - у цій резервній копії його не записано, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника",
|
||||
archivesUndeleted: "Архіви, видалені після резервного копіювання, знову видимі - перезапис був увімкнений",
|
||||
archivesMetadataOnly: "Відновлені архіви містять лише метадані - файли 3MF і мініатюри не входять до резервної копії Git",
|
||||
|
||||
@@ -4900,6 +4900,7 @@ export default {
|
||||
archivesPrinterMissing: '部分归档引用了已不存在的打印机 - 已清除关联',
|
||||
archivesProjectMissing: '部分归档引用了已不存在的项目 - 已清除关联',
|
||||
archivesOwnerCleared: '部分归档引用了已不存在的用户 - 已清除归属,因此在管理员重新指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',
|
||||
archivesOwnerUnmatched: '部分归档指向本实例没有的用户 - 已清除归属,而不是根据备份中的用户 ID 猜测,因此在管理员重新指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',
|
||||
archivesOwnerUnknown: '部分归档在恢复时没有归属 - 此备份未记录归属,因此在管理员指派之前,只有拥有 archives:read_all 权限的用户才能看到它们',
|
||||
archivesUndeleted: '备份之后被删除的归档重新可见 - 当时启用了覆盖',
|
||||
archivesMetadataOnly: '恢复的归档仅含元数据 - 3MF 文件和缩略图不在 Git 备份中',
|
||||
|
||||
@@ -4900,6 +4900,7 @@ export default {
|
||||
archivesPrinterMissing: '部分封存參照了已不存在的印表機 - 已清除連結',
|
||||
archivesProjectMissing: '部分封存參照了已不存在的專案 - 已清除連結',
|
||||
archivesOwnerCleared: '部分封存參照了已不存在的使用者 - 已清除擁有者,因此在管理員重新指派之前,只有具備 archives:read_all 權限的使用者才看得到',
|
||||
archivesOwnerUnmatched: '部分封存指向本執行個體沒有的使用者 - 已清除擁有者,而非依備份中的使用者 ID 推測,因此在管理員重新指派之前,只有具備 archives:read_all 權限的使用者才看得到',
|
||||
archivesOwnerUnknown: '部分封存還原時沒有擁有者 - 此備份未記錄擁有者,因此在管理員指派之前,只有具備 archives:read_all 權限的使用者才看得到',
|
||||
archivesUndeleted: '備份之後刪除的封存重新可見 - 當時啟用了覆寫',
|
||||
archivesMetadataOnly: '還原的封存僅含中繼資料 - 3MF 檔案與縮圖不在 Git 備份中',
|
||||
|
||||
Reference in New Issue
Block a user