mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-04 04:55:21 +02:00
fix: user-feedback-on-schema-mismatch (#1558)
* validate schema version on restore * show user error on backup failure
This commit is contained in:
parent
7adcc86d03
commit
3985713cbd
4 changed files with 56 additions and 15 deletions
|
@ -5,7 +5,7 @@
|
||||||
<!-- Delete Dialog -->
|
<!-- Delete Dialog -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="deleteDialog"
|
v-model="deleteDialog"
|
||||||
:title="$t('settings.backup.delete-backup')"
|
:title="$tc('settings.backup.delete-backup')"
|
||||||
color="error"
|
color="error"
|
||||||
:icon="$globals.icons.alertCircle"
|
:icon="$globals.icons.alertCircle"
|
||||||
@confirm="deleteBackup()"
|
@confirm="deleteBackup()"
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<p class="caption pb-0 mb-1 text-center">
|
<p class="caption pb-0 mb-1 text-center">
|
||||||
{{ selected.name }}
|
{{ selected }}
|
||||||
</p>
|
</p>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
|
@ -47,9 +47,12 @@
|
||||||
<v-card-text class="py-0 px-1">
|
<v-card-text class="py-0 px-1">
|
||||||
Backups a total snapshots of the database and data directory of the site. This includes all data and cannot
|
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.
|
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
|
Currently,
|
||||||
versions (data migrations are not done automatically). These serve as a database agnostic way to export and
|
<b>
|
||||||
import data or backup the site to an external location.
|
this backup mechanism is not cross-version and therefore cannot be used to migrate data between versions
|
||||||
|
</b>
|
||||||
|
(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.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseCardSectionTitle>
|
</BaseCardSectionTitle>
|
||||||
<BaseButton @click="createBackup"> {{ $t("settings.backup.create-heading") }} </BaseButton>
|
<BaseButton @click="createBackup"> {{ $t("settings.backup.create-heading") }} </BaseButton>
|
||||||
|
@ -78,7 +81,7 @@
|
||||||
>
|
>
|
||||||
<v-icon> {{ $globals.icons.delete }} </v-icon>
|
<v-icon> {{ $globals.icons.delete }} </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<BaseButton small download :download-url="backupsFileNameDownload(item.name)" @click.stop />
|
<BaseButton small download :download-url="backupsFileNameDownload(item.name)" @click.stop="() => {}" />
|
||||||
</template>
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
@ -137,11 +140,15 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreBackup(fileName: string) {
|
async function restoreBackup(fileName: string) {
|
||||||
const { data } = await adminApi.backups.restore(fileName);
|
const { error } = await adminApi.backups.restore(fileName);
|
||||||
|
|
||||||
if (!data?.error) {
|
if (error) {
|
||||||
$auth.logout();
|
console.log(error);
|
||||||
|
state.importDialog = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$auth.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteTarget = ref("");
|
const deleteTarget = ref("");
|
||||||
|
|
|
@ -9,8 +9,8 @@ from mealie.core.security import create_file_token
|
||||||
from mealie.pkgs.stats.fs_stats import pretty_size
|
from mealie.pkgs.stats.fs_stats import pretty_size
|
||||||
from mealie.routes._base import BaseAdminController, controller
|
from mealie.routes._base import BaseAdminController, controller
|
||||||
from mealie.schema.admin.backup import AllBackups, BackupFile
|
from mealie.schema.admin.backup import AllBackups, BackupFile
|
||||||
from mealie.schema.response.responses import FileTokenResponse, SuccessResponse
|
from mealie.schema.response.responses import ErrorResponse, FileTokenResponse, SuccessResponse
|
||||||
from mealie.services.backups_v2.backup_v2 import BackupV2
|
from mealie.services.backups_v2.backup_v2 import BackupSchemaMismatch, BackupV2
|
||||||
|
|
||||||
router = APIRouter(prefix="/backups")
|
router = APIRouter(prefix="/backups")
|
||||||
|
|
||||||
|
@ -100,6 +100,11 @@ class AdminBackupController(BaseAdminController):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
backup.restore(file)
|
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:
|
except Exception as e:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class BackupContents:
|
class BackupContents:
|
||||||
|
_tables: dict = None
|
||||||
|
|
||||||
def __init__(self, file: Path) -> None:
|
def __init__(self, file: Path) -> None:
|
||||||
self.base = file
|
self.base = file
|
||||||
self.data_directory = self.base / "data"
|
self.data_directory = self.base / "data"
|
||||||
|
@ -22,9 +24,22 @@ class BackupContents:
|
||||||
|
|
||||||
return True
|
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:
|
def read_tables(self) -> dict:
|
||||||
with open(self.tables) as f:
|
if self._tables is None:
|
||||||
return json.loads(f.read())
|
with open(self.tables, "r") as f:
|
||||||
|
self._tables = json.load(f)
|
||||||
|
|
||||||
|
return self._tables
|
||||||
|
|
||||||
|
|
||||||
class BackupFile:
|
class BackupFile:
|
||||||
|
|
|
@ -9,6 +9,10 @@ from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
|
||||||
from mealie.services.backups_v2.backup_file import BackupFile
|
from mealie.services.backups_v2.backup_file import BackupFile
|
||||||
|
|
||||||
|
|
||||||
|
class BackupSchemaMismatch(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class BackupV2(BaseService):
|
class BackupV2(BaseService):
|
||||||
def __init__(self, db_url: str = None) -> None:
|
def __init__(self, db_url: str = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -73,18 +77,28 @@ class BackupV2(BaseService):
|
||||||
self._postgres()
|
self._postgres()
|
||||||
|
|
||||||
with backup as contents:
|
with backup as contents:
|
||||||
|
# ================================
|
||||||
|
# Validation
|
||||||
if not contents.validate():
|
if not contents.validate():
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"Invalid backup file. file does not contain required elements (data directory and database.json"
|
"Invalid backup file. file does not contain required elements (data directory and database.json"
|
||||||
)
|
)
|
||||||
raise ValueError("Invalid backup file")
|
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.logger.info("dropping all database tables")
|
||||||
self.db_exporter.drop_all()
|
self.db_exporter.drop_all()
|
||||||
|
|
||||||
database_json = contents.read_tables()
|
# ================================
|
||||||
|
# Restore Database
|
||||||
|
|
||||||
self.logger.info("importing database tables")
|
self.logger.info("importing database tables")
|
||||||
self.db_exporter.restore(database_json)
|
self.db_exporter.restore(database_json)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue