From 120e97e882a9a12552363b5ba97965c9c2700669 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:08:13 +0100 Subject: [PATCH] Feature/restore backup inform about missing sources (#523) * Validate backup before starting restore process In case sources were missing, the user was never informed and thus, did not know which sources were required for all manga to work * Reset file input after restoring Otherwise, the same backup file can't be restored until the page gets refreshed/re-opened --- src/i18n/locale/en.json | 49 +++++++++--- src/screens/settings/Backup.tsx | 136 +++++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 29 deletions(-) diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index af68aee1..7ecfc1cd 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -258,6 +258,7 @@ "popular": "Popular", "reset": "Reset", "reset_to_default": "Reset to Default", + "restore": "Restore", "resume": "Resume", "select": "Select", "select_all": "Select all", @@ -617,6 +618,44 @@ } }, "backup": { + "action": { + "create": { + "label": { + "description": "Back up library as a Tachiyomi backup", + "title": "Create backup" + } + }, + "restore": { + "button": "Restore", + "error": { + "label": { + "failure": "Could not restore backup", + "legacy_backup_unsupported": "legacy backups are not supported!" + } + }, + "label": { + "description": "You can also drag and drop the backup file here to restore it", + "in_progress": "Restoring backup…", + "success": "Backup restored.", + "title": "Restore Backup" + } + }, + "validate": { + "dialog": { + "content": { + "label": { + "missing_sources": "The following sources are not installed:" + } + }, + "title": "Backup validation" + }, + "error": { + "label": { + "failure": "Could not validate backup" + } + } + } + }, "automated": { "cleanup": { "label": { @@ -635,16 +674,6 @@ } } }, - "label": { - "backup_restore_failed": "Could not restore backup", - "create_backup": "Create backup", - "create_backup_info": "Back up library as a Tachiyomi backup", - "legacy_backup_unsupported": "legacy backups are not supported!", - "restore_backup": "Restore Backup", - "restore_backup_info": "You can also drag and drop the backup file here to restore it", - "restored_backup": "Backup restored.", - "restoring_backup": "Restoring backup…" - }, "title": "Backup" }, "clear_cache": { diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index d4be0c3b..d94c4a61 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -15,11 +15,18 @@ import { ListItemButton } from '@mui/material'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListSubheader from '@mui/material/ListSubheader'; import { t as translate } from 'i18next'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import ListItem from '@mui/material/ListItem'; +import { Link } from 'react-router-dom'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/components/util/Toast'; import { ListItemLink } from '@/components/util/ListItemLink'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; -import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts'; +import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts'; import { Progress } from '@/components/util/Progress.tsx'; import { TextSetting } from '@/components/settings/TextSetting.tsx'; import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; @@ -69,6 +76,10 @@ export function Backup() { pollInterval: 1000, }); + const [currentBackupFile, setCurrentBackupFile] = useState(null); + const [isInvalidBackupDialogOpen, setIsInvalidBackupDialogOpen] = useState(false); + const [missingSources, setMissingSources] = useState([]); + const [, setTriggerReRender] = useState(0); const restoreProgress = (() => { @@ -98,11 +109,11 @@ export function Backup() { const isRestoreFinished = isSuccess || isFailure; if (isRestoreFinished) { if (isSuccess) { - makeToast(t('settings.backup.label.restored_backup'), 'success'); + makeToast(t('settings.backup.action.restore.label.success'), 'success'); } if (isFailure) { - makeToast(t('settings.backup.label.backup_restore_failed'), 'error'); + makeToast(t('settings.backup.action.restore.error.label.failure'), 'error'); } backupRestoreId = undefined; @@ -110,21 +121,66 @@ export function Backup() { } }, [data?.restoreStatus?.state]); - const submitBackup = async (file: File) => { - if (file.name.toLowerCase().match(/proto\.gz$|tachibk$/g)) { - makeToast(t('settings.backup.label.restoring_backup'), 'info'); + const resetBackupState = () => { + setCurrentBackupFile(null); - try { - const response = await requestManager.restoreBackupFile(file).response; - backupRestoreId = response.data?.restoreBackup.id; - setTriggerReRender(Date.now()); - } catch (e) { - makeToast(t('settings.backup.label.backup_restore_failed'), 'error'); + const input = document.getElementById('backup-file') as HTMLInputElement; + if (input) { + input.value = ''; + } + }; + + const validateBackup = async (file: File) => { + try { + const { + data: { validateBackup: validateBackupData }, + } = await requestManager.validateBackupFile(file, { fetchPolicy: 'network-only' }).response; + + if (validateBackupData.missingSources.length) { + setMissingSources([...validateBackupData.missingSources]); + setIsInvalidBackupDialogOpen(true); + return false; } - } else if (file.name.toLowerCase().endsWith('json')) { - makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error'); - } else { + + return true; + } catch (e) { + makeToast(t('settings.backup.action.validate.error.label.failure'), 'error'); + resetBackupState(); + } + + return false; + }; + + const restoreBackup = async (file: File) => { + try { + makeToast(t('settings.backup.action.restore.label.in_progress'), 'info'); + + const response = await requestManager.restoreBackupFile(file).response; + backupRestoreId = response.data?.restoreBackup.id; + setTriggerReRender(Date.now()); + } catch (e) { + makeToast(t('settings.backup.action.restore.error.label.failure'), 'error'); + } finally { + resetBackupState(); + } + }; + + const submitBackup = async (file: File) => { + if (file.name.toLowerCase().endsWith('json')) { + makeToast(t('settings.backup.action.restore.error.label.legacy_backup_unsupported'), 'error'); + return; + } + + const isValidFilename = file.name.toLowerCase().match(/proto\.gz$|tachibk$/g); + if (!isValidFilename) { makeToast(t('global.error.label.invalid_file_type'), 'error'); + return; + } + + setCurrentBackupFile(file); + const isBackupValid = await validateBackup(file); + if (isBackupValid) { + await restoreBackup(file); } }; @@ -139,6 +195,11 @@ export function Backup() { e.preventDefault(); }; + const closeInvalidBackupDialog = () => { + setIsInvalidBackupDialogOpen(false); + resetBackupState(); + }; + useEffect(() => { document.addEventListener('drop', dropHandler); document.addEventListener('dragover', dragOverHandler); @@ -163,8 +224,8 @@ export function Backup() { {backupRestoreId ? ( @@ -236,6 +297,43 @@ export function Backup() { + + {t('settings.backup.action.validate.dialog.title')} + + + {missingSources.map(({ id, name }) => ( + + {`${name} (${id})`} + + ))} + + + + + + + + ); }