@@ -47,9 +47,12 @@
Backups a total snapshots of the database and data directory of the site. This includes all data and cannot
be set to exclude subsets of data. You can think off this as a snapshot of Mealie at a specific time.
- Currently, this backup mechanism is not cross-version and therefore cannot be used to migrate data between
- versions (data migrations are not done automatically). These serve as a database agnostic way to export and
- import data or backup the site to an external location.
+ Currently,
+
+ this backup mechanism is not cross-version and therefore cannot be used to migrate data between versions
+
+ (data migrations are not done automatically). These serve as a database agnostic way to export and import
+ data or backup the site to an external location.
{{ $t("settings.backup.create-heading") }}
@@ -78,7 +81,7 @@
>
{{ $globals.icons.delete }}
-
+ {}" />
@@ -137,11 +140,15 @@ export default defineComponent({
}
async function restoreBackup(fileName: string) {
- const { data } = await adminApi.backups.restore(fileName);
+ const { error } = await adminApi.backups.restore(fileName);
- if (!data?.error) {
- $auth.logout();
+ if (error) {
+ console.log(error);
+ state.importDialog = false;
+ return;
}
+
+ $auth.logout();
}
const deleteTarget = ref("");
diff --git a/mealie/routes/admin/admin_backups.py b/mealie/routes/admin/admin_backups.py
index 7b33cbf6f..46c21f581 100644
--- a/mealie/routes/admin/admin_backups.py
+++ b/mealie/routes/admin/admin_backups.py
@@ -9,8 +9,8 @@ from mealie.core.security import create_file_token
from mealie.pkgs.stats.fs_stats import pretty_size
from mealie.routes._base import BaseAdminController, controller
from mealie.schema.admin.backup import AllBackups, BackupFile
-from mealie.schema.response.responses import FileTokenResponse, SuccessResponse
-from mealie.services.backups_v2.backup_v2 import BackupV2
+from mealie.schema.response.responses import ErrorResponse, FileTokenResponse, SuccessResponse
+from mealie.services.backups_v2.backup_v2 import BackupSchemaMismatch, BackupV2
router = APIRouter(prefix="/backups")
@@ -100,6 +100,11 @@ class AdminBackupController(BaseAdminController):
try:
backup.restore(file)
+ except BackupSchemaMismatch as e:
+ raise HTTPException(
+ status.HTTP_400_BAD_REQUEST,
+ ErrorResponse.respond("database backup schema version does not match current database"),
+ ) from e
except Exception as e:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) from e
diff --git a/mealie/services/backups_v2/backup_file.py b/mealie/services/backups_v2/backup_file.py
index 187ed3b3e..ce7d9ad53 100644
--- a/mealie/services/backups_v2/backup_file.py
+++ b/mealie/services/backups_v2/backup_file.py
@@ -5,6 +5,8 @@ from pathlib import Path
class BackupContents:
+ _tables: dict = None
+
def __init__(self, file: Path) -> None:
self.base = file
self.data_directory = self.base / "data"
@@ -22,9 +24,22 @@ class BackupContents:
return True
+ def schema_version(self) -> str:
+ tables = self.read_tables()
+
+ alembic_version = tables.get("alembic_version", [])
+
+ if not alembic_version:
+ return ""
+
+ return alembic_version[0].get("version_num", "")
+
def read_tables(self) -> dict:
- with open(self.tables) as f:
- return json.loads(f.read())
+ if self._tables is None:
+ with open(self.tables, "r") as f:
+ self._tables = json.load(f)
+
+ return self._tables
class BackupFile:
diff --git a/mealie/services/backups_v2/backup_v2.py b/mealie/services/backups_v2/backup_v2.py
index 65a4b44e2..4a5a9b034 100644
--- a/mealie/services/backups_v2/backup_v2.py
+++ b/mealie/services/backups_v2/backup_v2.py
@@ -9,6 +9,10 @@ from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
from mealie.services.backups_v2.backup_file import BackupFile
+class BackupSchemaMismatch(Exception):
+ ...
+
+
class BackupV2(BaseService):
def __init__(self, db_url: str = None) -> None:
super().__init__()
@@ -73,18 +77,28 @@ class BackupV2(BaseService):
self._postgres()
with backup as contents:
+ # ================================
+ # Validation
if not contents.validate():
self.logger.error(
"Invalid backup file. file does not contain required elements (data directory and database.json"
)
raise ValueError("Invalid backup file")
- # Purge the Database
+ database_json = contents.read_tables()
+
+ if not AlchemyExporter.validate_schemas(database_json, self.db_exporter.dump()):
+ self.logger.error("Invalid backup file. Database schemas do not match")
+ raise BackupSchemaMismatch("Invalid backup file. Database schemas do not match")
+
+ # ================================
+ # Purge Database
self.logger.info("dropping all database tables")
self.db_exporter.drop_all()
- database_json = contents.read_tables()
+ # ================================
+ # Restore Database
self.logger.info("importing database tables")
self.db_exporter.restore(database_json)