mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
feat: backup & restore
This commit is contained in:
parent
f67d4150f1
commit
55860af9b3
@ -14,6 +14,9 @@ import {
|
|||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
import { systemLogger } from '../utils/logger'
|
import { systemLogger } from '../utils/logger'
|
||||||
import { Cron } from 'croner'
|
import { Cron } from 'croner'
|
||||||
|
import { dialog } from 'electron'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
let backupCronJob: Cron | null = null
|
let backupCronJob: Cron | null = null
|
||||||
|
|
||||||
@ -196,3 +199,75 @@ export async function reinitScheduler(): Promise<void> {
|
|||||||
await initWebdavBackupScheduler()
|
await initWebdavBackupScheduler()
|
||||||
await systemLogger.info('WebDAV backup scheduler reinitialized successfully')
|
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
|
setupFirewall
|
||||||
} from '../sys/misc'
|
} from '../sys/misc'
|
||||||
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
|
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 { getInterfaces } from '../sys/interface'
|
||||||
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon, updateTrayIconImmediate } from '../resolve/tray'
|
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon, updateTrayIconImmediate } from '../resolve/tray'
|
||||||
import { registerShortcut } from '../resolve/shortcut'
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
@ -250,6 +250,8 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
|
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
|
||||||
ipcMain.handle('webdavDelete', (_e, filename) => ipcErrorWrapper(webdavDelete)(filename))
|
ipcMain.handle('webdavDelete', (_e, filename) => ipcErrorWrapper(webdavDelete)(filename))
|
||||||
ipcMain.handle('reinitWebdavBackupScheduler', ipcErrorWrapper(reinitScheduler))
|
ipcMain.handle('reinitWebdavBackupScheduler', ipcErrorWrapper(reinitScheduler))
|
||||||
|
ipcMain.handle('exportLocalBackup', () => ipcErrorWrapper(exportLocalBackup)())
|
||||||
|
ipcMain.handle('importLocalBackup', () => ipcErrorWrapper(importLocalBackup)())
|
||||||
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
||||||
ipcErrorWrapper(registerShortcut)(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.restore.noBackups": "No backups available",
|
||||||
"webdav.notification.backupSuccess.title": "Backup Successful",
|
"webdav.notification.backupSuccess.title": "Backup Successful",
|
||||||
"webdav.notification.backupSuccess.body": "Backup file has been uploaded to WebDAV",
|
"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.title": "Keyboard Shortcuts",
|
||||||
"shortcuts.toggleWindow": "Toggle Window",
|
"shortcuts.toggleWindow": "Toggle Window",
|
||||||
"shortcuts.toggleFloatingWindow": "Toggle Floating Window",
|
"shortcuts.toggleFloatingWindow": "Toggle Floating Window",
|
||||||
|
|||||||
@ -217,6 +217,17 @@
|
|||||||
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
|
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
|
||||||
"webdav.notification.backupSuccess.title": "پشتیبانگیری موفق",
|
"webdav.notification.backupSuccess.title": "پشتیبانگیری موفق",
|
||||||
"webdav.notification.backupSuccess.body": "فایل پشتیبان در WebDAV بارگذاری شد",
|
"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.title": "میانبرهای صفحه کلید",
|
||||||
"shortcuts.toggleWindow": "تغییر وضعیت پنجره",
|
"shortcuts.toggleWindow": "تغییر وضعیت پنجره",
|
||||||
"shortcuts.toggleFloatingWindow": "تغییر وضعیت پنجره شناور",
|
"shortcuts.toggleFloatingWindow": "تغییر وضعیت پنجره شناور",
|
||||||
|
|||||||
@ -217,6 +217,17 @@
|
|||||||
"webdav.restore.noBackups": "Нет доступных резервных копий",
|
"webdav.restore.noBackups": "Нет доступных резервных копий",
|
||||||
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
|
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
|
||||||
"webdav.notification.backupSuccess.body": "Файл резервной копии загружен на WebDAV",
|
"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.title": "Горячие клавиши",
|
||||||
"shortcuts.toggleWindow": "Показать/скрыть окно",
|
"shortcuts.toggleWindow": "Показать/скрыть окно",
|
||||||
"shortcuts.toggleFloatingWindow": "Показать/скрыть плавающее окно",
|
"shortcuts.toggleFloatingWindow": "Показать/скрыть плавающее окно",
|
||||||
|
|||||||
@ -239,6 +239,17 @@
|
|||||||
"webdav.restore.noBackups": "还没有备份",
|
"webdav.restore.noBackups": "还没有备份",
|
||||||
"webdav.notification.backupSuccess.title": "备份成功",
|
"webdav.notification.backupSuccess.title": "备份成功",
|
||||||
"webdav.notification.backupSuccess.body": "备份文件已上传到 WebDAV",
|
"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.title": "快捷键设置",
|
||||||
"shortcuts.toggleWindow": "打开/关闭窗口",
|
"shortcuts.toggleWindow": "打开/关闭窗口",
|
||||||
"shortcuts.toggleFloatingWindow": "打开/关闭悬浮窗",
|
"shortcuts.toggleFloatingWindow": "打开/关闭悬浮窗",
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config'
|
|||||||
import { FaTelegramPlane } from 'react-icons/fa'
|
import { FaTelegramPlane } from 'react-icons/fa'
|
||||||
import SiderConfig from '@renderer/components/settings/sider-config'
|
import SiderConfig from '@renderer/components/settings/sider-config'
|
||||||
import SubStoreConfig from '@renderer/components/settings/substore-config'
|
import SubStoreConfig from '@renderer/components/settings/substore-config'
|
||||||
|
import LocalBackupConfig from '@renderer/components/settings/local-backup-config'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
@ -65,6 +66,7 @@ const Settings: React.FC = () => {
|
|||||||
<WebdavConfig />
|
<WebdavConfig />
|
||||||
<MihomoConfig />
|
<MihomoConfig />
|
||||||
<ShortcutConfig />
|
<ShortcutConfig />
|
||||||
|
<LocalBackupConfig />
|
||||||
<Actions />
|
<Actions />
|
||||||
</BasePage>
|
</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> {
|
export async function setTitleBarOverlay(overlay: TitleBarOverlayOptions): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setTitleBarOverlay', overlay))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setTitleBarOverlay', overlay))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user