mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-26 20:50:30 +08:00
feat: backup & restore
This commit is contained in:
parent
f67d4150f1
commit
55860af9b3
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
81
src/renderer/src/components/settings/local-backup-config.tsx
Normal file
81
src/renderer/src/components/settings/local-backup-config.tsx
Normal 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
|
||||
@ -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",
|
||||
|
||||
@ -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": "تغییر وضعیت پنجره شناور",
|
||||
|
||||
@ -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": "Показать/скрыть плавающее окно",
|
||||
|
||||
@ -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": "打开/关闭悬浮窗",
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user