Files
suwayomi-material-you-webui/src/screens/settings/Backup.tsx
schroda 9b96560a17 Feature/settings backup (#453)
* Show loading indicator for "NumberSetting"

* [i18n] Add lowercase interpolation format

* Add required dependencies for time settings

* Dynamically import dayjs locales for languages

* Add backup settings
2023-11-12 01:41:10 +01:00

242 lines
9.4 KiB
TypeScript

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useContext, useEffect, useState } from 'react';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import { fromEvent } from 'file-selector';
import { useTranslation } from 'react-i18next';
import { ListItemButton } from '@mui/material';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListSubheader from '@mui/material/ListSubheader';
import { t as translate } from 'i18next';
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 { Progress } from '@/components/util/Progress.tsx';
import { ServerDirSetting } from '@/components/settings/ServerDirSetting.tsx';
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
import { TimeSetting } from '@/components/settings/TimeSetting.tsx';
import { ServerSettings } from '@/typings.ts';
type BackupSettingsType = Pick<ServerSettings, 'backupPath' | 'backupTime' | 'backupInterval' | 'backupTTL'>;
const extractBackupSettings = (settings: ServerSettings): BackupSettingsType => ({
backupPath: settings.backupPath,
backupTime: settings.backupTime,
backupInterval: settings.backupInterval,
backupTTL: settings.backupTTL,
});
const getBackupCleanupDisplayValue = (ttl?: number) => {
if (ttl === undefined) {
return undefined;
}
if (ttl === 0) {
return translate('global.label.never');
}
return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl });
};
let backupRestoreId: string | undefined;
export function Backup() {
const { t } = useTranslation();
const { setTitle, setAction } = useContext(NavBarContext);
useEffect(() => {
setTitle(t('settings.backup.title'));
setAction(null);
}, [t]);
useSetDefaultBackTo('settings');
const { data: settingsData } = requestManager.useGetServerSettings();
const [mutateSettings] = requestManager.useUpdateServerSettings();
const backupSettings = settingsData ? extractBackupSettings(settingsData.settings) : undefined;
const { data } = requestManager.useGetBackupRestoreStatus(backupRestoreId ?? '', {
skip: !backupRestoreId,
pollInterval: 1000,
});
const [, setTriggerReRender] = useState(0);
const restoreProgress = (() => {
if (!data?.restoreStatus) {
return 0;
}
const progress = 100 * (data.restoreStatus.mangaProgress / data.restoreStatus.totalManga);
return Number.isNaN(progress) ? 0 : progress;
})();
const updateSetting = <Setting extends keyof BackupSettingsType>(
setting: Setting,
value: BackupSettingsType[Setting],
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
};
useEffect(() => {
if (!data?.restoreStatus) {
return;
}
const isSuccess = data.restoreStatus.state === BackupRestoreState.Success;
const isFailure = data.restoreStatus.state === BackupRestoreState.Failure;
const isRestoreFinished = isSuccess || isFailure;
if (isRestoreFinished) {
if (isSuccess) {
makeToast(t('settings.backup.label.restored_backup'), 'success');
}
if (isFailure) {
makeToast(t('settings.backup.label.backup_restore_failed'), 'error');
}
backupRestoreId = undefined;
setTriggerReRender(Date.now());
}
}, [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');
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');
}
} else if (file.name.toLowerCase().endsWith('json')) {
makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error');
} else {
makeToast(t('global.error.label.invalid_file_type'), 'error');
}
};
const dropHandler = async (e: Event) => {
e.preventDefault();
const files = await fromEvent(e);
submitBackup(files[0] as File);
};
const dragOverHandler = (e: Event) => {
e.preventDefault();
};
useEffect(() => {
document.addEventListener('drop', dropHandler);
document.addEventListener('dragover', dragOverHandler);
const handleFileSelection = async (event: Event) => {
const files = await fromEvent(event);
submitBackup(files[0] as File);
};
const input = document.getElementById('backup-file');
input?.addEventListener('change', handleFileSelection);
return () => {
document.removeEventListener('drop', dropHandler);
document.removeEventListener('dragover', dragOverHandler);
input?.removeEventListener('change', handleFileSelection);
};
}, []);
return (
<>
<List sx={{ padding: 0 }}>
<ListItemLink to={requestManager.getExportBackupUrl()} directLink>
<ListItemText
primary={t('settings.backup.label.create_backup')}
secondary={t('settings.backup.label.create_backup_info')}
/>
</ListItemLink>
<ListItemButton
onClick={() => document.getElementById('backup-file')?.click()}
disabled={!!backupRestoreId}
>
<ListItemText
primary={t('settings.backup.label.restore_backup')}
secondary={t('settings.backup.label.restore_backup_info')}
/>
{backupRestoreId ? (
<ListItemIcon>
<Progress progress={restoreProgress} />
</ListItemIcon>
) : null}
</ListItemButton>
<List
subheader={
<ListSubheader component="div" id="backup-settings">
Automated backup
</ListSubheader>
}
>
<ServerDirSetting
settingName={t('settings.backup.automated.location.label.title')}
dialogDescription={t('settings.backup.automated.location.label.description')}
dirPath={backupSettings?.backupPath}
handlePathChange={(path) => updateSetting('backupPath', path)}
/>
<TimeSetting
settingName={t('settings.backup.automated.label.time')}
value={backupSettings?.backupTime}
defaultValue="00:00"
handleChange={(time: string) => updateSetting('backupTime', time)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.label.interval')}
settingValue={
backupSettings?.backupInterval
? t('global.date.value.label.day', {
days: backupSettings.backupInterval,
count: backupSettings.backupInterval,
})
: undefined
}
value={backupSettings?.backupInterval ?? 1}
defaultValue={1}
minValue={1}
maxValue={31}
stepSize={1}
dialogTitle="Interval"
valueUnit="Day"
showSlider
handleUpdate={(interval: number) => updateSetting('backupInterval', interval)}
/>
<NumberSetting
settingTitle={t('settings.backup.automated.cleanup.label.title')}
settingValue={getBackupCleanupDisplayValue(backupSettings?.backupTTL)}
value={backupSettings?.backupTTL ?? 14}
defaultValue={14}
minValue={0}
maxValue={1000}
stepSize={1}
dialogTitle="Cleanup"
valueUnit="Day"
showSlider
handleUpdate={(ttl: number) => updateSetting('backupTTL', ttl)}
/>
</List>
</List>
<input type="file" id="backup-file" style={{ display: 'none' }} />
</>
);
}