feat: backup & restore

This commit is contained in:
Memory 2025-10-22 12:19:16 +08:00 committed by GitHub
parent f67d4150f1
commit 55860af9b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 214 additions and 1 deletions

View File

@ -14,6 +14,9 @@ import {
} from '../utils/dirs'
import { systemLogger } from '../utils/logger'
import { Cron } from 'croner'
import { dialog } from 'electron'
import { existsSync } from 'fs'
import i18next from 'i18next'
let backupCronJob: Cron | null = null
@ -196,3 +199,75 @@ export async function reinitScheduler(): Promise<void> {
await initWebdavBackupScheduler()
await systemLogger.info('WebDAV backup scheduler reinitialized successfully')
}
/**
*
*/
export async function exportLocalBackup(): Promise<boolean> {
const zip = new AdmZip()
if (existsSync(appConfigPath())) {
zip.addLocalFile(appConfigPath())
}
if (existsSync(controledMihomoConfigPath())) {
zip.addLocalFile(controledMihomoConfigPath())
}
if (existsSync(profileConfigPath())) {
zip.addLocalFile(profileConfigPath())
}
if (existsSync(overrideConfigPath())) {
zip.addLocalFile(overrideConfigPath())
}
if (existsSync(themesDir())) {
zip.addLocalFolder(themesDir(), 'themes')
}
if (existsSync(profilesDir())) {
zip.addLocalFolder(profilesDir(), 'profiles')
}
if (existsSync(overrideDir())) {
zip.addLocalFolder(overrideDir(), 'override')
}
if (existsSync(subStoreDir())) {
zip.addLocalFolder(subStoreDir(), 'substore')
}
const date = new Date()
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
const result = await dialog.showSaveDialog({
title: i18next.t('localBackup.export.title'),
defaultPath: zipFileName,
filters: [
{ name: 'ZIP Files', extensions: ['zip'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (!result.canceled && result.filePath) {
zip.writeZip(result.filePath)
await systemLogger.info(`Local backup exported to: ${result.filePath}`)
return true
}
return false
}
/**
*
*/
export async function importLocalBackup(): Promise<boolean> {
const result = await dialog.showOpenDialog({
title: i18next.t('localBackup.import.title'),
filters: [
{ name: 'ZIP Files', extensions: ['zip'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile']
})
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0]
const zip = new AdmZip(filePath)
zip.extractAllTo(dataDir(), true)
await systemLogger.info(`Local backup imported from: ${filePath}`)
return true
}
return false
}

View File

@ -85,7 +85,7 @@ import {
setupFirewall
} from '../sys/misc'
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup'
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore, exportLocalBackup, importLocalBackup } from '../resolve/backup'
import { getInterfaces } from '../sys/interface'
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon, updateTrayIconImmediate } from '../resolve/tray'
import { registerShortcut } from '../resolve/shortcut'
@ -250,6 +250,8 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
ipcMain.handle('webdavDelete', (_e, filename) => ipcErrorWrapper(webdavDelete)(filename))
ipcMain.handle('reinitWebdavBackupScheduler', ipcErrorWrapper(reinitScheduler))
ipcMain.handle('exportLocalBackup', () => ipcErrorWrapper(exportLocalBackup)())
ipcMain.handle('importLocalBackup', () => ipcErrorWrapper(importLocalBackup)())
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
)

View File

@ -0,0 +1,81 @@
import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item'
import { Button, useDisclosure } from '@heroui/react'
import { exportLocalBackup, importLocalBackup } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
import BaseConfirmModal from '../base/base-confirm-modal'
const LocalBackupConfig: React.FC = () => {
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
const [importing, setImporting] = useState(false)
const [exporting, setExporting] = useState(false)
const handleExport = async (): Promise<void> => {
setExporting(true)
try {
const success = await exportLocalBackup()
if (success) {
new window.Notification(t('localBackup.notification.exportSuccess.title'), {
body: t('localBackup.notification.exportSuccess.body')
})
}
} catch (e) {
alert(e)
} finally {
setExporting(false)
}
}
const handleImport = async (): Promise<void> => {
setImporting(true)
try {
const success = await importLocalBackup()
if (success) {
new window.Notification(t('localBackup.notification.importSuccess.title'), {
body: t('localBackup.notification.importSuccess.body')
})
}
} catch (e) {
alert(t('common.error.importFailed', { error: e }))
} finally {
setImporting(false)
onClose()
}
}
return (
<>
<BaseConfirmModal
isOpen={isOpen}
onCancel={onClose}
onConfirm={handleImport}
title={t('localBackup.import.confirm.title')}
content={t('localBackup.import.confirm.body')}
/>
<SettingCard title={t('localBackup.title')}>
<SettingItem title={t('localBackup.export.title')} divider>
<Button
isLoading={exporting}
size="sm"
onPress={handleExport}
>
{t('localBackup.export.button')}
</Button>
</SettingItem>
<SettingItem title={t('localBackup.import.title')}>
<Button
isLoading={importing}
size="sm"
onPress={onOpen}
>
{t('localBackup.import.button')}
</Button>
</SettingItem>
</SettingCard>
</>
)
}
export default LocalBackupConfig

View File

@ -239,6 +239,17 @@
"webdav.restore.noBackups": "No backups available",
"webdav.notification.backupSuccess.title": "Backup Successful",
"webdav.notification.backupSuccess.body": "Backup file has been uploaded to WebDAV",
"localBackup.title": "Backup & Restore",
"localBackup.export.title": "Export Local Backup",
"localBackup.export.button": "Export Backup",
"localBackup.import.title": "Import Local Backup",
"localBackup.import.button": "Import Backup",
"localBackup.import.confirm.title": "Confirm Import",
"localBackup.import.confirm.body": "Importing local backup will overwrite all current configurations. Are you sure you want to continue?",
"localBackup.notification.exportSuccess.title": "Export Successful",
"localBackup.notification.exportSuccess.body": "Local backup has been exported to the specified location",
"localBackup.notification.importSuccess.title": "Import Successful",
"localBackup.notification.importSuccess.body": "Local backup has been successfully imported",
"shortcuts.title": "Keyboard Shortcuts",
"shortcuts.toggleWindow": "Toggle Window",
"shortcuts.toggleFloatingWindow": "Toggle Floating Window",

View File

@ -217,6 +217,17 @@
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
"webdav.notification.backupSuccess.title": "پشتیبان‌گیری موفق",
"webdav.notification.backupSuccess.body": "فایل پشتیبان در WebDAV بارگذاری شد",
"localBackup.title": "پشتیبان‌گیری و بازیابی",
"localBackup.export.title": "صادر کردن پشتیبان محلی",
"localBackup.export.button": "صادر کردن پشتیبان",
"localBackup.import.title": "وارد کردن پشتیبان محلی",
"localBackup.import.button": "وارد کردن پشتیبان",
"localBackup.import.confirm.title": "تایید وارد کردن",
"localBackup.import.confirm.body": "وارد کردن پشتیبان محلی، تمام پیکربندی‌های فعلی را بازنویسی می‌کند. آیا مطمئن هستید که می‌خواهید ادامه دهید؟",
"localBackup.notification.exportSuccess.title": "صادر کردن موفق",
"localBackup.notification.exportSuccess.body": "پشتیبان محلی در مکان مشخص شده صادر شد",
"localBackup.notification.importSuccess.title": "وارد کردن موفق",
"localBackup.notification.importSuccess.body": "پشتیبان محلی با موفقیت وارد شد",
"shortcuts.title": "میانبرهای صفحه کلید",
"shortcuts.toggleWindow": "تغییر وضعیت پنجره",
"shortcuts.toggleFloatingWindow": "تغییر وضعیت پنجره شناور",

View File

@ -217,6 +217,17 @@
"webdav.restore.noBackups": "Нет доступных резервных копий",
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
"webdav.notification.backupSuccess.body": "Файл резервной копии загружен на WebDAV",
"localBackup.title": "Резервное копирование и восстановление",
"localBackup.export.title": "Экспорт локальной резервной копии",
"localBackup.export.button": "Экспорт резервной копии",
"localBackup.import.title": "Импорт локальной резервной копии",
"localBackup.import.button": "Импорт резервной копии",
"localBackup.import.confirm.title": "Подтвердить импорт",
"localBackup.import.confirm.body": "Импорт локальной резервной копии перезапишет все текущие конфигурации. Вы уверены, что хотите продолжить?",
"localBackup.notification.exportSuccess.title": "Экспорт успешен",
"localBackup.notification.exportSuccess.body": "Локальная резервная копия была экспортирована в указанное место",
"localBackup.notification.importSuccess.title": "Импорт успешен",
"localBackup.notification.importSuccess.body": "Локальная резервная копия была успешно импортирована",
"shortcuts.title": "Горячие клавиши",
"shortcuts.toggleWindow": "Показать/скрыть окно",
"shortcuts.toggleFloatingWindow": "Показать/скрыть плавающее окно",

View File

@ -239,6 +239,17 @@
"webdav.restore.noBackups": "还没有备份",
"webdav.notification.backupSuccess.title": "备份成功",
"webdav.notification.backupSuccess.body": "备份文件已上传到 WebDAV",
"localBackup.title": "备份与恢复",
"localBackup.export.title": "导出本地备份",
"localBackup.export.button": "导出备份",
"localBackup.import.title": "导入本地备份",
"localBackup.import.button": "导入备份",
"localBackup.import.confirm.title": "确认导入",
"localBackup.import.confirm.body": "导入本地备份将会覆盖当前所有配置,确定要继续吗?",
"localBackup.notification.exportSuccess.title": "导出成功",
"localBackup.notification.exportSuccess.body": "本地备份已导出到指定位置",
"localBackup.notification.importSuccess.title": "导入成功",
"localBackup.notification.importSuccess.body": "本地备份已成功导入",
"shortcuts.title": "快捷键设置",
"shortcuts.toggleWindow": "打开/关闭窗口",
"shortcuts.toggleFloatingWindow": "打开/关闭悬浮窗",

View File

@ -10,6 +10,7 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config'
import { FaTelegramPlane } from 'react-icons/fa'
import SiderConfig from '@renderer/components/settings/sider-config'
import SubStoreConfig from '@renderer/components/settings/substore-config'
import LocalBackupConfig from '@renderer/components/settings/local-backup-config'
import { useTranslation } from 'react-i18next'
const Settings: React.FC = () => {
@ -65,6 +66,7 @@ const Settings: React.FC = () => {
<WebdavConfig />
<MihomoConfig />
<ShortcutConfig />
<LocalBackupConfig />
<Actions />
</BasePage>
)

View File

@ -353,6 +353,15 @@ export async function reinitWebdavBackupScheduler(): Promise<void> {
)
}
// 本地备份相关 IPC 调用
export async function exportLocalBackup(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('exportLocalBackup'))
}
export async function importLocalBackup(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('importLocalBackup'))
}
export async function setTitleBarOverlay(overlay: TitleBarOverlayOptions): Promise<void> {
try {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setTitleBarOverlay', overlay))