diff --git a/src/main/index.ts b/src/main/index.ts index 407dfe0..f2ca294 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -21,6 +21,7 @@ import { showFloatingWindow } from './resolve/floatingWindow' import { initI18n } from '../shared/i18n' import i18next from 'i18next' import { logger } from './utils/logger' +import { initWebdavBackupScheduler } from './resolve/backup' // 错误处理 function showSafeErrorBox(titleKey: string, message: string): void { @@ -246,7 +247,8 @@ app.whenReady().then(async () => { try { const [startPromise] = await startCore() startPromise.then(async () => { - await initProfileUpdater() + await initProfileUpdater() + await initWebdavBackupScheduler() // 初始化WebDAV定时备份任务 // 上次是否为了开启 TUN 而重启 await checkAdminRestartForTun() }) diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index fb1c02f..39b672d 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -13,6 +13,9 @@ import { themesDir } from '../utils/dirs' import { systemLogger } from '../utils/logger' +import { Cron } from 'croner' + +let backupCronJob: Cron | null = null export async function webdavBackup(): Promise { const { createClient } = await import('webdav/dist/node/index.js') @@ -137,3 +140,59 @@ export async function webdavDelete(filename: string): Promise { }) await client.deleteFile(`${webdavDir}/${filename}`) } + +/** + * 初始化WebDAV定时备份任务 + */ +export async function initWebdavBackupScheduler(): Promise { + 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 { + if (backupCronJob) { + backupCronJob.stop() + backupCronJob = null + await systemLogger.info('WebDAV backup scheduler stopped') + } +} + +/** + * 重新初始化WebDAV定时备份任务 + * 先停止现有任务,然后重新启动 + */ +export async function reinitScheduler(): Promise { + await systemLogger.info('Reinitializing WebDAV backup scheduler...') + await stopWebdavBackupScheduler() + await initWebdavBackupScheduler() + await systemLogger.info('WebDAV backup scheduler reinitialized successfully') +} diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index ef701d6..900d80e 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -106,6 +106,7 @@ import { startMonitor } from '../resolve/trafficMonitor' import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow' import i18next from 'i18next' import { addProfileUpdater, removeProfileUpdater } from '../core/profileUpdater' +import { reinitScheduler } from '../resolve/backup' function ipcErrorWrapper( // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise // 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('listWebdavBackups', ipcErrorWrapper(listWebdavBackups)) ipcMain.handle('webdavDelete', (_e, filename) => ipcErrorWrapper(webdavDelete)(filename)) + ipcMain.handle('reinitWebdavBackupScheduler', ipcErrorWrapper(reinitScheduler)) ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) => ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action) ) diff --git a/src/renderer/src/components/settings/webdav-config.tsx b/src/renderer/src/components/settings/webdav-config.tsx index a336f9c..6575827 100644 --- a/src/renderer/src/components/settings/webdav-config.tsx +++ b/src/renderer/src/components/settings/webdav-config.tsx @@ -2,11 +2,12 @@ import React, { useState } from 'react' import SettingCard from '../base/base-setting-card' import SettingItem from '../base/base-setting-item' 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 debounce from '@renderer/utils/debounce' import { useAppConfig } from '@renderer/hooks/use-app-config' import { useTranslation } from 'react-i18next' +import { isValidCron } from 'cron-validator' const WebdavConfig: React.FC = () => { const { t } = useTranslation() @@ -16,7 +17,8 @@ const WebdavConfig: React.FC = () => { webdavUsername, webdavPassword, webdavDir = 'clash-party', - webdavMaxBackups = 0 + webdavMaxBackups = 0, + webdavBackupCron } = appConfig || {} const [backuping, setBackuping] = useState(false) const [restoring, setRestoring] = useState(false) @@ -28,11 +30,12 @@ const WebdavConfig: React.FC = () => { webdavUsername, webdavPassword, webdavDir, - webdavMaxBackups + webdavMaxBackups, + webdavBackupCron }) const setWebdavDebounce = debounce( - ({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups }) => { - patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups }) + ({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron }) => { + patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron }) }, 500 ) @@ -135,6 +138,42 @@ const WebdavConfig: React.FC = () => { 20 + +
+ {webdavBackupCron !== webdav.webdavBackupCron && ( + + )} + { + setWebdav({ ...webdav, webdavBackupCron: v }) + }} + /> + +
+