1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 13:05: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:
Hayden 2022-08-14 11:06:35 -08:00 committed by GitHub
parent 7adcc86d03
commit 3985713cbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 15 deletions

View file

@ -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("");

View file

@ -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

View file

@ -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:

View file

@ -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)