diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 993dbf17dc7..14135736e05 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -635,6 +635,7 @@ "backup-created-at-response-export_path": "Backup Created at {path}", "backup-deleted": "Backup deleted", "restore-success": "Restore successful", + "restore-fail": "Restore failed. Check your server logs for more details", "backup-tag": "Backup Tag", "create-heading": "Create A Backup", "delete-backup": "Delete Backup", diff --git a/frontend/pages/admin/backups.vue b/frontend/pages/admin/backups.vue index 3f253e90756..f25f30f4859 100644 --- a/frontend/pages/admin/backups.vue +++ b/frontend/pages/admin/backups.vue @@ -162,6 +162,8 @@ export default defineComponent({ if (error) { console.log(error); state.importDialog = false; + state.runningRestore = false; + alert.error(i18n.tc("settings.backup.restore-fail")); } else { alert.success(i18n.tc("settings.backup.restore-success")); $auth.logout(); diff --git a/mealie/services/backups_v2/backup_file.py b/mealie/services/backups_v2/backup_file.py index f5321483e6e..aa8f043b9ae 100644 --- a/mealie/services/backups_v2/backup_file.py +++ b/mealie/services/backups_v2/backup_file.py @@ -8,9 +8,39 @@ class BackupContents: _tables: dict | None = None def __init__(self, file: Path) -> None: - self.base = file - self.data_directory = self.base / "data" - self.tables = self.base / "database.json" + self.base = self._find_base(file) + self.data_directory = self._find_data_dir_from_base(self.base) + self.tables = self._find_database_from_base(self.base) + + @classmethod + def _find_base(cls, file: Path) -> Path: + # Safari mangles our ZIP structure and adds a "__MACOSX" directory at the root along with + # an arbitrarily-named directory containing the actual contents. So, if we find a dunder directory + # at the root (i.e. __MACOSX) we traverse down the first non-dunder directory and assume this is the base. + # This works because our backups never contain a directory that starts with "__". + dirs = [d for d in file.iterdir() if d.is_dir()] + dunder_dirs = [d for d in dirs if d.name.startswith("__")] + normal_dirs = [d for d in dirs if not d.name.startswith("__")] + + if not dunder_dirs: + return file + + # If the backup somehow adds a __MACOSX directory alongside the data directory, rather than in the + # parent directory, we don't want to traverse down. We check for our database.json file, and if it exists, + # we're already at the correct base. + if cls._find_database_from_base(file).exists(): + return file + + # This ZIP file was mangled, so we return the first non-dunder directory (if it exists). + return normal_dirs[0] if normal_dirs else file + + @classmethod + def _find_data_dir_from_base(cls, base: Path) -> Path: + return base / "data" + + @classmethod + def _find_database_from_base(cls, base: Path) -> Path: + return base / "database.json" def validate(self) -> bool: if not self.base.is_dir(): diff --git a/tests/data/__init__.py b/tests/data/__init__.py index d3532afdd1a..e872eac86f9 100644 --- a/tests/data/__init__.py +++ b/tests/data/__init__.py @@ -22,6 +22,9 @@ backup_version_bcfdad6b7355_1 = CWD / "backups/backup_version_bcfdad6b7355_1.zip" """bcfdad6b7355: remove tool name and slug unique contraints""" +backup_version_09aba125b57a_1 = CWD / "backups/backup_version_09aba125b57a_1.zip" +"""09aba125b57a: add OIDC auth method (Safari-mangled ZIP structure)""" + migrations_paprika = CWD / "migrations/paprika.zip" migrations_chowdown = CWD / "migrations/chowdown.zip" diff --git a/tests/data/backups/backup_version_09aba125b57a_1.zip b/tests/data/backups/backup_version_09aba125b57a_1.zip new file mode 100644 index 00000000000..dfc71bdf352 Binary files /dev/null and b/tests/data/backups/backup_version_09aba125b57a_1.zip differ diff --git a/tests/unit_tests/services_tests/backup_v2_tests/test_backup_v2.py b/tests/unit_tests/services_tests/backup_v2_tests/test_backup_v2.py index f072d58793d..6c95dc4628a 100644 --- a/tests/unit_tests/services_tests/backup_v2_tests/test_backup_v2.py +++ b/tests/unit_tests/services_tests/backup_v2_tests/test_backup_v2.py @@ -81,6 +81,7 @@ def test_database_restore(): test_data.backup_version_44e8d670719d_4, test_data.backup_version_ba1e4a6cfe99_1, test_data.backup_version_bcfdad6b7355_1, + test_data.backup_version_09aba125b57a_1, ], ids=[ "44e8d670719d_1: add extras to shopping lists, list items, and ingredient foods", @@ -89,6 +90,7 @@ def test_database_restore(): "44e8d670719d_4: add extras to shopping lists, list items, and ingredient foods", "ba1e4a6cfe99_1: added plural names and alias tables for foods and units", "bcfdad6b7355_1: remove tool name and slug unique contraints", + "09aba125b57a: add OIDC auth method (Safari-mangled ZIP structure)", ], ) def test_database_restore_data(backup_path: Path):