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
This commit is contained in:
@@ -258,6 +258,7 @@
|
|||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"reset_to_default": "Reset to Default",
|
"reset_to_default": "Reset to Default",
|
||||||
|
"restore": "Restore",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
@@ -617,6 +618,44 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backup": {
|
"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": {
|
"automated": {
|
||||||
"cleanup": {
|
"cleanup": {
|
||||||
"label": {
|
"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"
|
"title": "Backup"
|
||||||
},
|
},
|
||||||
"clear_cache": {
|
"clear_cache": {
|
||||||
|
|||||||
@@ -15,11 +15,18 @@ import { ListItemButton } from '@mui/material';
|
|||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import { t as translate } from 'i18next';
|
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 { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { makeToast } from '@/components/util/Toast';
|
import { makeToast } from '@/components/util/Toast';
|
||||||
import { ListItemLink } from '@/components/util/ListItemLink';
|
import { ListItemLink } from '@/components/util/ListItemLink';
|
||||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
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 { Progress } from '@/components/util/Progress.tsx';
|
||||||
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
import { TextSetting } from '@/components/settings/TextSetting.tsx';
|
||||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
@@ -69,6 +76,10 @@ export function Backup() {
|
|||||||
pollInterval: 1000,
|
pollInterval: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [currentBackupFile, setCurrentBackupFile] = useState<File | null>(null);
|
||||||
|
const [isInvalidBackupDialogOpen, setIsInvalidBackupDialogOpen] = useState(false);
|
||||||
|
const [missingSources, setMissingSources] = useState<ValidateBackupQuery['validateBackup']['missingSources']>([]);
|
||||||
|
|
||||||
const [, setTriggerReRender] = useState(0);
|
const [, setTriggerReRender] = useState(0);
|
||||||
|
|
||||||
const restoreProgress = (() => {
|
const restoreProgress = (() => {
|
||||||
@@ -98,11 +109,11 @@ export function Backup() {
|
|||||||
const isRestoreFinished = isSuccess || isFailure;
|
const isRestoreFinished = isSuccess || isFailure;
|
||||||
if (isRestoreFinished) {
|
if (isRestoreFinished) {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
makeToast(t('settings.backup.label.restored_backup'), 'success');
|
makeToast(t('settings.backup.action.restore.label.success'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFailure) {
|
if (isFailure) {
|
||||||
makeToast(t('settings.backup.label.backup_restore_failed'), 'error');
|
makeToast(t('settings.backup.action.restore.error.label.failure'), 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
backupRestoreId = undefined;
|
backupRestoreId = undefined;
|
||||||
@@ -110,21 +121,66 @@ export function Backup() {
|
|||||||
}
|
}
|
||||||
}, [data?.restoreStatus?.state]);
|
}, [data?.restoreStatus?.state]);
|
||||||
|
|
||||||
const submitBackup = async (file: File) => {
|
const resetBackupState = () => {
|
||||||
if (file.name.toLowerCase().match(/proto\.gz$|tachibk$/g)) {
|
setCurrentBackupFile(null);
|
||||||
makeToast(t('settings.backup.label.restoring_backup'), 'info');
|
|
||||||
|
|
||||||
try {
|
const input = document.getElementById('backup-file') as HTMLInputElement;
|
||||||
const response = await requestManager.restoreBackupFile(file).response;
|
if (input) {
|
||||||
backupRestoreId = response.data?.restoreBackup.id;
|
input.value = '';
|
||||||
setTriggerReRender(Date.now());
|
}
|
||||||
} catch (e) {
|
};
|
||||||
makeToast(t('settings.backup.label.backup_restore_failed'), 'error');
|
|
||||||
|
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');
|
return true;
|
||||||
} else {
|
} 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');
|
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();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeInvalidBackupDialog = () => {
|
||||||
|
setIsInvalidBackupDialogOpen(false);
|
||||||
|
resetBackupState();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('drop', dropHandler);
|
document.addEventListener('drop', dropHandler);
|
||||||
document.addEventListener('dragover', dragOverHandler);
|
document.addEventListener('dragover', dragOverHandler);
|
||||||
@@ -163,8 +224,8 @@ export function Backup() {
|
|||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItemLink to={requestManager.getExportBackupUrl()}>
|
<ListItemLink to={requestManager.getExportBackupUrl()}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('settings.backup.label.create_backup')}
|
primary={t('settings.backup.action.create.label.title')}
|
||||||
secondary={t('settings.backup.label.create_backup_info')}
|
secondary={t('settings.backup.action.create.label.description')}
|
||||||
/>
|
/>
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
@@ -172,8 +233,8 @@ export function Backup() {
|
|||||||
disabled={!!backupRestoreId}
|
disabled={!!backupRestoreId}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('settings.backup.label.restore_backup')}
|
primary={t('settings.backup.action.restore.label.title')}
|
||||||
secondary={t('settings.backup.label.restore_backup_info')}
|
secondary={t('settings.backup.action.restore.label.description')}
|
||||||
/>
|
/>
|
||||||
{backupRestoreId ? (
|
{backupRestoreId ? (
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
@@ -236,6 +297,43 @@ export function Backup() {
|
|||||||
</List>
|
</List>
|
||||||
</List>
|
</List>
|
||||||
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
||||||
|
<Dialog open={isInvalidBackupDialogOpen}>
|
||||||
|
<DialogTitle>{t('settings.backup.action.validate.dialog.title')}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<List
|
||||||
|
sx={{ listStyleType: 'initial', listStylePosition: 'inside' }}
|
||||||
|
subheader={t('settings.backup.action.validate.dialog.content.label.missing_sources')}
|
||||||
|
>
|
||||||
|
{missingSources.map(({ id, name }) => (
|
||||||
|
<ListItem sx={{ display: 'list-item' }} key={id}>
|
||||||
|
{`${name} (${id})`}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeInvalidBackupDialog}>{t('global.button.cancel')}</Button>
|
||||||
|
<Button
|
||||||
|
onClick={closeInvalidBackupDialog}
|
||||||
|
component={Link}
|
||||||
|
to="/extensions"
|
||||||
|
autoFocus={!!missingSources.length}
|
||||||
|
variant={missingSources.length ? 'contained' : 'text'}
|
||||||
|
>
|
||||||
|
{t('extension.action.label.install')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
closeInvalidBackupDialog();
|
||||||
|
restoreBackup(currentBackupFile!);
|
||||||
|
}}
|
||||||
|
autoFocus={!missingSources.length}
|
||||||
|
variant={!missingSources.length ? 'contained' : 'text'}
|
||||||
|
>
|
||||||
|
{t('global.button.restore')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user