diff --git a/backend/app/services/github_restore.py b/backend/app/services/github_restore.py index 0b8293219..3a10e3d1b 100644 --- a/backend/app/services/github_restore.py +++ b/backend/app/services/github_restore.py @@ -1017,6 +1017,7 @@ class GitHubRestoreService: # column was added to fix) and silently un-delete a row the user # 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: created_by_id = entry.get("created_by_id") if created_by_id is not None and created_by_id not in valid_users: @@ -1030,6 +1031,7 @@ class GitHubRestoreService: "visible only to users with the archives:read_all permission until an admin reassigns them", ) created_by_id = None + owner_cleared = True fields["created_by_id"] = created_by_id if "deleted_at" in entry: # A soft-deleted archive is still in the backup (its row is kept @@ -1065,6 +1067,24 @@ class GitHubRestoreService: ) warned_files = True + # Insert-only, and the mirror of the "absent is not null" rule above: + # on overwrite an unknown owner correctly leaves the local one alone, + # but there is no local row here to fall back on, so the archive + # lands ownerless — a 404 for everyone without archives:read_all. + # Two ways to get here: a commit taken before the collector recorded + # the column (every pre-#2656 backup), or an archive that genuinely + # had no owner on the source instance. Both restore fine and both + # were silent, so the tally said "N archives restored" while the user + # who asked for them saw none. The stale-id case above already said + # its piece; don't say it twice for the same row. + if fields.get("created_by_id") is None and not owner_cleared: + tally.note( + "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", + ) + row = PrintArchive( filename=entry.get("filename") or "restored-from-backup", file_path="", diff --git a/backend/tests/unit/test_github_restore.py b/backend/tests/unit/test_github_restore.py index 62f2a84c4..35b67e2e2 100644 --- a/backend/tests/unit/test_github_restore.py +++ b/backend/tests/unit/test_github_restore.py @@ -2063,8 +2063,14 @@ class TestRestoredArchiveOwnership: assert sum(1 for note in _messages(tally) if "owner cleared" in note) == 1 @pytest.mark.asyncio - async def test_a_backup_without_the_key_still_restores(self, db_session): - """Backups taken before the collector recorded it just can't know the owner.""" + async def test_a_backup_without_the_key_still_restores_and_says_so(self, db_session): + """Backups taken before the collector recorded it just can't know the owner. + + The archive is worth restoring anyway, but it lands ownerless — which is + a 404 for everyone without ``archives:read_all``. Reporting N restored + while the user who asked for them sees none is the failure mode the note + exists to prevent. + """ tally = _CategoryTally() await _service()._restore_archives(db_session, {"archives": [self._entry()]}, False, tally, {}) @@ -2072,7 +2078,70 @@ class TestRestoredArchiveOwnership: row = (await db_session.execute(select(PrintArchive))).scalar_one() assert row.created_by_id is None + assert tally.restored == 1 assert not any("owner cleared" in note for note in _messages(tally)) + assert any("without an owner" in note and "archives:read_all" in note for note in _messages(tally)) + + @pytest.mark.asyncio + async def test_an_explicitly_ownerless_archive_is_reported_too(self, db_session): + """Same consequence, so the same note: the source row had no owner either.""" + tally = _CategoryTally() + + await _service()._restore_archives( + db_session, {"archives": [self._entry(created_by_id=None)]}, False, tally, {} + ) + await db_session.commit() + + assert any("without an owner" in note for note in _messages(tally)) + + @pytest.mark.asyncio + async def test_a_stale_owner_is_not_reported_twice(self, db_session): + """One row, one cause, one note — the cleared-owner branch already spoke.""" + tally = _CategoryTally() + + await _service()._restore_archives( + db_session, {"archives": [self._entry(created_by_id=4242)]}, False, tally, {} + ) + await db_session.commit() + + assert any("owner cleared" 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_known_owner_is_not_reported(self, db_session): + user = await self._user(db_session) + tally = _CategoryTally() + + await _service()._restore_archives( + db_session, {"archives": [self._entry(created_by_id=user.id)]}, False, tally, {} + ) + await db_session.commit() + + assert not any("without an owner" in note for note in _messages(tally)) + + @pytest.mark.asyncio + async def test_the_unknown_owner_note_is_not_emitted_on_overwrite(self, db_session): + """Overwrite keeps the local owner, so there is nothing to warn about.""" + 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() + tally = _CategoryTally() + + await _service()._restore_archives(db_session, {"archives": [self._entry()]}, True, tally, {}) + await db_session.commit() + + row = (await db_session.execute(select(PrintArchive))).scalar_one() + assert row.created_by_id == bob.id + assert not any("without an owner" in note for note in _messages(tally)) @pytest.mark.asyncio async def test_overwrite_makes_the_local_owner_match_the_backup(self, db_session): diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 45328baa0..255d95fc7 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} Verbrauchseinträge übersprungen - ihre Spule ist nicht in der Spulenliste dieses Backups, es gibt also nichts, woran sie hängen könnten.', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 88134f085..40630d494 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} usage record(s) skipped - their spool is not in this backup\'s spool list, so there is nothing to attach them to.', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 540aa2823..f623b61fd 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} registros de consumo omitidos - su bobina no está en la lista de bobinas de esta copia, así que no hay nada a lo que asociarlos.', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index c7da19984..7ec1fe339 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} enregistrements de consommation ignorés - leur bobine ne figure pas dans la liste des bobines de cette sauvegarde, il n\'y a donc rien à quoi les rattacher.', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 3d46e6d94..a906329eb 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} record di consumo saltati - la loro bobina non è nell\'elenco bobine di questo backup, quindi non c\'è nulla a cui collegarli.', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 1b828f6c0..d7984efd6 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -4912,6 +4912,7 @@ export default { archivesPrinterMissing: '一部のアーカイブが存在しないプリンターを参照していました - リンクを解除しました', archivesProjectMissing: '一部のアーカイブが存在しないプロジェクトを参照していました - リンクを解除しました', archivesOwnerCleared: '一部のアーカイブが存在しないユーザーを参照していました - 所有者を解除したため、管理者が割り当て直すまで archives:read_all 権限を持つユーザーにしか表示されません', + archivesOwnerUnknown: '一部のアーカイブは所有者なしで復元されました - このバックアップに所有者が記録されていないため、管理者が割り当てるまで archives:read_all 権限を持つユーザーにしか表示されません', archivesUndeleted: 'バックアップ後に削除されたアーカイブが再び表示されます - 上書きが有効でした', archivesMetadataOnly: '復元されたアーカイブはメタデータのみです - 3MF ファイルとサムネイルは Git バックアップに含まれません', spoolUsageUnresolved: '使用履歴 {{count}} 件をスキップしました - 対応するスプールがこのバックアップのスプール一覧にないため、紐付ける先がありません。', diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index d68c397a3..54d38c0b1 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -4677,6 +4677,7 @@ export default { archivesPrinterMissing: '일부 아카이브가 더 이상 존재하지 않는 프린터를 참조했습니다 - 연결을 해제했습니다', archivesProjectMissing: '일부 아카이브가 더 이상 존재하지 않는 프로젝트를 참조했습니다 - 연결을 해제했습니다', archivesOwnerCleared: '일부 아카이브가 더 이상 존재하지 않는 사용자를 참조했습니다 - 소유자를 비웠으므로 관리자가 다시 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다', + archivesOwnerUnknown: '일부 아카이브가 소유자 없이 복원되었습니다 - 이 백업에 소유자가 기록되어 있지 않으므로 관리자가 지정하기 전까지 archives:read_all 권한이 있는 사용자에게만 보입니다', archivesUndeleted: '백업 이후 삭제된 아카이브가 다시 표시됩니다 - 덮어쓰기가 켜져 있었습니다', archivesMetadataOnly: '복원된 아카이브에는 메타데이터만 있습니다 - 3MF 파일과 썸네일은 Git 백업에 포함되지 않습니다', spoolUsageUnresolved: '사용 기록 {{count}}건을 건너뛰었습니다 - 해당 스풀이 이 백업의 스풀 목록에 없어 연결할 대상이 없습니다.', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 27b9a6758..34bbd825d 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} registros de consumo ignorados - o carretel deles não está na lista de carretéis deste backup, então não há a que vinculá-los.', diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index fd43da898..3acdfea90 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/frontend/src/i18n/locales/ru.ts @@ -4669,6 +4669,7 @@ export default { archivesPrinterMissing: 'Некоторые архивы ссылались на несуществующие принтеры - связь очищена', archivesProjectMissing: 'Некоторые архивы ссылались на несуществующие проекты - связь очищена', archivesOwnerCleared: 'Некоторые архивы ссылались на несуществующих пользователей - владелец очищен, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца заново', + archivesOwnerUnknown: 'Некоторые архивы восстановлены без владельца - в этой резервной копии он не записан, поэтому они видны только пользователям с правом archives:read_all, пока администратор не назначит владельца', archivesUndeleted: 'Архивы, удалённые после резервного копирования, снова видны - перезапись была включена', archivesMetadataOnly: 'Восстановленные архивы содержат только метаданные - файлы 3MF и миниатюры не входят в резервную копию Git', spoolUsageUnresolved: 'Записей расхода пропущено: {{count}} - их катушки нет в списке катушек этой резервной копии, поэтому привязать их не к чему.', diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index 22852c733..11e5789ed 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -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', + 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', spoolUsageUnresolved: '{{count}} kullanım kaydı atlandı - makaraları bu yedeğin makara listesinde olmadığı için bağlanacak bir şey yok.', diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index 56d7d5bea..3e7cbc57d 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/frontend/src/i18n/locales/uk.ts @@ -4955,6 +4955,7 @@ export default { archivesPrinterMissing: "Деякі архіви посилалися на принтери, яких більше немає - зв'язок очищено", archivesProjectMissing: "Деякі архіви посилалися на проєкти, яких більше немає - зв'язок очищено", archivesOwnerCleared: "Деякі архіви посилалися на користувачів, яких більше немає - власника очищено, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника знову", + archivesOwnerUnknown: "Деякі архіви відновлено без власника - у цій резервній копії його не записано, тому вони видимі лише користувачам із дозволом archives:read_all, доки адміністратор не призначить власника", archivesUndeleted: "Архіви, видалені після резервного копіювання, знову видимі - перезапис був увімкнений", archivesMetadataOnly: "Відновлені архіви містять лише метадані - файли 3MF і мініатюри не входять до резервної копії Git", spoolUsageUnresolved: "Записів використання пропущено: {{count}} - їхньої котушки немає у списку котушок цієї резервної копії, тож немає до чого їх прив'язати.", diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 297161b20..faea04afe 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4900,6 +4900,7 @@ export default { archivesPrinterMissing: '部分归档引用了已不存在的打印机 - 已清除关联', archivesProjectMissing: '部分归档引用了已不存在的项目 - 已清除关联', archivesOwnerCleared: '部分归档引用了已不存在的用户 - 已清除归属,因此在管理员重新指派之前,只有拥有 archives:read_all 权限的用户才能看到它们', + archivesOwnerUnknown: '部分归档在恢复时没有归属 - 此备份未记录归属,因此在管理员指派之前,只有拥有 archives:read_all 权限的用户才能看到它们', archivesUndeleted: '备份之后被删除的归档重新可见 - 当时启用了覆盖', archivesMetadataOnly: '恢复的归档仅含元数据 - 3MF 文件和缩略图不在 Git 备份中', spoolUsageUnresolved: '已跳过 {{count}} 条使用记录 - 其耗材卷不在此备份的耗材列表中,没有可挂接的对象。', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index de1a2cb2b..be917326f 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -4900,6 +4900,7 @@ export default { archivesPrinterMissing: '部分封存參照了已不存在的印表機 - 已清除連結', archivesProjectMissing: '部分封存參照了已不存在的專案 - 已清除連結', archivesOwnerCleared: '部分封存參照了已不存在的使用者 - 已清除擁有者,因此在管理員重新指派之前,只有具備 archives:read_all 權限的使用者才看得到', + archivesOwnerUnknown: '部分封存還原時沒有擁有者 - 此備份未記錄擁有者,因此在管理員指派之前,只有具備 archives:read_all 權限的使用者才看得到', archivesUndeleted: '備份之後刪除的封存重新可見 - 當時啟用了覆寫', archivesMetadataOnly: '還原的封存僅含中繼資料 - 3MF 檔案與縮圖不在 Git 備份中', spoolUsageUnresolved: '已略過 {{count}} 筆使用紀錄 - 其耗材捲不在此備份的耗材清單中,沒有可掛接的對象。',