mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
feat: add webdav backup cron scheduler and resore filename sort (#1192)
This commit is contained in:
parent
b30f49c9f4
commit
eb69bd51a6
@ -21,6 +21,7 @@ import { showFloatingWindow } from './resolve/floatingWindow'
|
|||||||
import { initI18n } from '../shared/i18n'
|
import { initI18n } from '../shared/i18n'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { logger } from './utils/logger'
|
import { logger } from './utils/logger'
|
||||||
|
import { initWebdavBackupScheduler } from './resolve/backup'
|
||||||
|
|
||||||
// 错误处理
|
// 错误处理
|
||||||
function showSafeErrorBox(titleKey: string, message: string): void {
|
function showSafeErrorBox(titleKey: string, message: string): void {
|
||||||
@ -246,7 +247,8 @@ app.whenReady().then(async () => {
|
|||||||
try {
|
try {
|
||||||
const [startPromise] = await startCore()
|
const [startPromise] = await startCore()
|
||||||
startPromise.then(async () => {
|
startPromise.then(async () => {
|
||||||
await initProfileUpdater()
|
await initProfileUpdater()
|
||||||
|
await initWebdavBackupScheduler() // 初始化WebDAV定时备份任务
|
||||||
// 上次是否为了开启 TUN 而重启
|
// 上次是否为了开启 TUN 而重启
|
||||||
await checkAdminRestartForTun()
|
await checkAdminRestartForTun()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import {
|
|||||||
themesDir
|
themesDir
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
import { systemLogger } from '../utils/logger'
|
import { systemLogger } from '../utils/logger'
|
||||||
|
import { Cron } from 'croner'
|
||||||
|
|
||||||
|
let backupCronJob: Cron | null = null
|
||||||
|
|
||||||
export async function webdavBackup(): Promise<boolean> {
|
export async function webdavBackup(): Promise<boolean> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
@ -137,3 +140,59 @@ export async function webdavDelete(filename: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
await client.deleteFile(`${webdavDir}/${filename}`)
|
await client.deleteFile(`${webdavDir}/${filename}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WebDAV定时备份任务
|
||||||
|
*/
|
||||||
|
export async function initWebdavBackupScheduler(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 先停止现有的定时任务
|
||||||
|
if (backupCronJob) {
|
||||||
|
backupCronJob.stop()
|
||||||
|
backupCronJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { webdavBackupCron } = await getAppConfig()
|
||||||
|
|
||||||
|
// 如果配置了Cron表达式,则启动定时任务
|
||||||
|
if (webdavBackupCron) {
|
||||||
|
backupCronJob = new Cron(webdavBackupCron, async () => {
|
||||||
|
try {
|
||||||
|
await webdavBackup()
|
||||||
|
await systemLogger.info('WebDAV backup completed successfully via cron job')
|
||||||
|
} catch (error) {
|
||||||
|
await systemLogger.error('Failed to execute WebDAV backup via cron job', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await systemLogger.info(`WebDAV backup scheduler initialized with cron: ${webdavBackupCron}`)
|
||||||
|
await systemLogger.info(`WebDAV backup scheduler nextRun: ${backupCronJob.nextRun()}`)
|
||||||
|
} else {
|
||||||
|
await systemLogger.info('WebDAV backup scheduler disabled (no cron expression configured)')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await systemLogger.error('Failed to initialize WebDAV backup scheduler', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止WebDAV定时备份任务
|
||||||
|
*/
|
||||||
|
export async function stopWebdavBackupScheduler(): Promise<void> {
|
||||||
|
if (backupCronJob) {
|
||||||
|
backupCronJob.stop()
|
||||||
|
backupCronJob = null
|
||||||
|
await systemLogger.info('WebDAV backup scheduler stopped')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新初始化WebDAV定时备份任务
|
||||||
|
* 先停止现有任务,然后重新启动
|
||||||
|
*/
|
||||||
|
export async function reinitScheduler(): Promise<void> {
|
||||||
|
await systemLogger.info('Reinitializing WebDAV backup scheduler...')
|
||||||
|
await stopWebdavBackupScheduler()
|
||||||
|
await initWebdavBackupScheduler()
|
||||||
|
await systemLogger.info('WebDAV backup scheduler reinitialized successfully')
|
||||||
|
}
|
||||||
|
|||||||
@ -106,6 +106,7 @@ import { startMonitor } from '../resolve/trafficMonitor'
|
|||||||
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
|
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { addProfileUpdater, removeProfileUpdater } from '../core/profileUpdater'
|
import { addProfileUpdater, removeProfileUpdater } from '../core/profileUpdater'
|
||||||
|
import { reinitScheduler } from '../resolve/backup'
|
||||||
|
|
||||||
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -244,6 +245,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('webdavRestore', (_e, filename) => ipcErrorWrapper(webdavRestore)(filename))
|
ipcMain.handle('webdavRestore', (_e, filename) => ipcErrorWrapper(webdavRestore)(filename))
|
||||||
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('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
||||||
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import React, { useState } from 'react'
|
|||||||
import SettingCard from '../base/base-setting-card'
|
import SettingCard from '../base/base-setting-card'
|
||||||
import SettingItem from '../base/base-setting-item'
|
import SettingItem from '../base/base-setting-item'
|
||||||
import { Button, Input, Select, SelectItem } from '@heroui/react'
|
import { Button, Input, Select, SelectItem } from '@heroui/react'
|
||||||
import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc'
|
import { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } from '@renderer/utils/ipc'
|
||||||
import WebdavRestoreModal from './webdav-restore-modal'
|
import WebdavRestoreModal from './webdav-restore-modal'
|
||||||
import debounce from '@renderer/utils/debounce'
|
import debounce from '@renderer/utils/debounce'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isValidCron } from 'cron-validator'
|
||||||
|
|
||||||
const WebdavConfig: React.FC = () => {
|
const WebdavConfig: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -16,7 +17,8 @@ const WebdavConfig: React.FC = () => {
|
|||||||
webdavUsername,
|
webdavUsername,
|
||||||
webdavPassword,
|
webdavPassword,
|
||||||
webdavDir = 'clash-party',
|
webdavDir = 'clash-party',
|
||||||
webdavMaxBackups = 0
|
webdavMaxBackups = 0,
|
||||||
|
webdavBackupCron
|
||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
const [backuping, setBackuping] = useState(false)
|
const [backuping, setBackuping] = useState(false)
|
||||||
const [restoring, setRestoring] = useState(false)
|
const [restoring, setRestoring] = useState(false)
|
||||||
@ -28,11 +30,12 @@ const WebdavConfig: React.FC = () => {
|
|||||||
webdavUsername,
|
webdavUsername,
|
||||||
webdavPassword,
|
webdavPassword,
|
||||||
webdavDir,
|
webdavDir,
|
||||||
webdavMaxBackups
|
webdavMaxBackups,
|
||||||
|
webdavBackupCron
|
||||||
})
|
})
|
||||||
const setWebdavDebounce = debounce(
|
const setWebdavDebounce = debounce(
|
||||||
({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups }) => {
|
({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron }) => {
|
||||||
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups })
|
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron })
|
||||||
},
|
},
|
||||||
500
|
500
|
||||||
)
|
)
|
||||||
@ -135,6 +138,42 @@ const WebdavConfig: React.FC = () => {
|
|||||||
<SelectItem key="20">20</SelectItem>
|
<SelectItem key="20">20</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title={t('webdav.backup.cron.title')} divider>
|
||||||
|
<div className="flex w-[60%] gap-2">
|
||||||
|
{webdavBackupCron !== webdav.webdavBackupCron && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
onPress={async () => {
|
||||||
|
if (!webdav.webdavBackupCron || isValidCron(webdav.webdavBackupCron)) {
|
||||||
|
try {
|
||||||
|
await patchAppConfig({ webdavBackupCron: webdav.webdavBackupCron })
|
||||||
|
// 立即重新初始化调度器
|
||||||
|
await reinitWebdavBackupScheduler()
|
||||||
|
new Notification(t('webdav.notification.cronUpdated'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update cron schedule:', error)
|
||||||
|
new Notification(t('webdav.notification.cronUpdateFailed'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(t('common.error.invalidCron'))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
value={webdav.webdavBackupCron}
|
||||||
|
placeholder={t('webdav.backup.cron.placeholder')}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setWebdav({ ...webdav, webdavBackupCron: v })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
<div className="flex justify0between">
|
<div className="flex justify0between">
|
||||||
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
||||||
{t('webdav.backup')}
|
{t('webdav.backup')}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
{filenames.length === 0 ? (
|
{filenames.length === 0 ? (
|
||||||
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
||||||
) : (
|
) : (
|
||||||
filenames.map((filename) => (
|
filenames.sort().reverse().map((filename) => (
|
||||||
<div className="flex" key={filename}>
|
<div className="flex" key={filename}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -214,6 +214,8 @@
|
|||||||
"webdav.maxBackups": "Max Backups",
|
"webdav.maxBackups": "Max Backups",
|
||||||
"webdav.noLimit": "No Limit",
|
"webdav.noLimit": "No Limit",
|
||||||
"webdav.backup": "Backup",
|
"webdav.backup": "Backup",
|
||||||
|
"webdav.backup.cron.title": "Schedule Config Backup",
|
||||||
|
"webdav.backup.cron.placeholder": "Cron expression",
|
||||||
"webdav.restore.title": "Restore Backup",
|
"webdav.restore.title": "Restore Backup",
|
||||||
"webdav.restore.noBackups": "No backups available",
|
"webdav.restore.noBackups": "No backups available",
|
||||||
"webdav.notification.backupSuccess.title": "Backup Successful",
|
"webdav.notification.backupSuccess.title": "Backup Successful",
|
||||||
|
|||||||
@ -210,6 +210,8 @@
|
|||||||
"webdav.maxBackups": "حداکثر نسخه پشتیبان",
|
"webdav.maxBackups": "حداکثر نسخه پشتیبان",
|
||||||
"webdav.noLimit": "بدون محدودیت",
|
"webdav.noLimit": "بدون محدودیت",
|
||||||
"webdav.backup": "پشتیبانگیری",
|
"webdav.backup": "پشتیبانگیری",
|
||||||
|
"webdav.backup.cron.title": "زمانبندی پشتیبانگیری پیکربندی",
|
||||||
|
"webdav.backup.cron.placeholder": "عبارت Cron",
|
||||||
"webdav.restore.title": "بازیابی پشتیبان",
|
"webdav.restore.title": "بازیابی پشتیبان",
|
||||||
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
|
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
|
||||||
"webdav.notification.backupSuccess.title": "پشتیبانگیری موفق",
|
"webdav.notification.backupSuccess.title": "پشتیبانگیری موفق",
|
||||||
|
|||||||
@ -210,6 +210,8 @@
|
|||||||
"webdav.maxBackups": "Максимум резервных копий",
|
"webdav.maxBackups": "Максимум резервных копий",
|
||||||
"webdav.noLimit": "Без ограничений",
|
"webdav.noLimit": "Без ограничений",
|
||||||
"webdav.backup": "Резервное копирование",
|
"webdav.backup": "Резервное копирование",
|
||||||
|
"webdav.backup.cron.title": "Расписание резервного копирования",
|
||||||
|
"webdav.backup.cron.placeholder": "Cron выражение",
|
||||||
"webdav.restore.title": "Восстановление резервной копии",
|
"webdav.restore.title": "Восстановление резервной копии",
|
||||||
"webdav.restore.noBackups": "Нет доступных резервных копий",
|
"webdav.restore.noBackups": "Нет доступных резервных копий",
|
||||||
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
|
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
|
||||||
|
|||||||
@ -214,6 +214,8 @@
|
|||||||
"webdav.maxBackups": "最大备份数",
|
"webdav.maxBackups": "最大备份数",
|
||||||
"webdav.noLimit": "不限制",
|
"webdav.noLimit": "不限制",
|
||||||
"webdav.backup": "备份",
|
"webdav.backup": "备份",
|
||||||
|
"webdav.backup.cron.title": "定时备份配置",
|
||||||
|
"webdav.backup.cron.placeholder": "Cron 表达式",
|
||||||
"webdav.restore.title": "恢复备份",
|
"webdav.restore.title": "恢复备份",
|
||||||
"webdav.restore.noBackups": "还没有备份",
|
"webdav.restore.noBackups": "还没有备份",
|
||||||
"webdav.notification.backupSuccess.title": "备份成功",
|
"webdav.notification.backupSuccess.title": "备份成功",
|
||||||
|
|||||||
@ -338,6 +338,13 @@ export async function webdavDelete(filename: string): Promise<void> {
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('webdavDelete', filename))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('webdavDelete', filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebDAV 备份调度器相关 IPC 调用
|
||||||
|
export async function reinitWebdavBackupScheduler(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('reinitWebdavBackupScheduler')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|||||||
1
src/shared/types.d.ts
vendored
1
src/shared/types.d.ts
vendored
@ -296,6 +296,7 @@ interface IAppConfig {
|
|||||||
webdavUsername?: string
|
webdavUsername?: string
|
||||||
webdavPassword?: string
|
webdavPassword?: string
|
||||||
webdavMaxBackups?: number
|
webdavMaxBackups?: number
|
||||||
|
webdavBackupCron?: string
|
||||||
useNameserverPolicy: boolean
|
useNameserverPolicy: boolean
|
||||||
nameserverPolicy: { [key: string]: string | string[] }
|
nameserverPolicy: { [key: string]: string | string[] }
|
||||||
showWindowShortcut?: string
|
showWindowShortcut?: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user