feat: add webdav backup cron scheduler and resore filename sort (#1192)

This commit is contained in:
苹果派派 2025-09-19 19:27:17 +08:00 committed by GitHub
parent b30f49c9f4
commit eb69bd51a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 125 additions and 7 deletions

View File

@ -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()
}) })

View File

@ -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')
}

View File

@ -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)
) )

View File

@ -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')}

View File

@ -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"

View File

@ -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",

View File

@ -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": "پشتیبان‌گیری موفق",

View File

@ -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": "Резервное копирование успешно",

View File

@ -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": "备份成功",

View File

@ -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))

View File

@ -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