From 365eb9bab5f705a7e83785d9c8fe0900795fdb6c Mon Sep 17 00:00:00 2001 From: "Rudbeckia.hirta.L" <83773602@qq.com> Date: Sat, 17 May 2025 19:20:40 +0800 Subject: [PATCH] feat: support skipping files during backup(slim backup) (#6075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support skipping files during backup * 修复 lint * 修复 lint --------- Co-authored-by: daisai.1 --- src/main/services/BackupManager.ts | 40 +++++++++++-------- src/preload/index.ts | 4 +- .../src/components/Popups/BackupPopup.tsx | 6 ++- src/renderer/src/i18n/locales/en-us.json | 2 + src/renderer/src/i18n/locales/ja-jp.json | 2 + src/renderer/src/i18n/locales/ru-ru.json | 3 ++ src/renderer/src/i18n/locales/zh-cn.json | 2 + src/renderer/src/i18n/locales/zh-tw.json | 2 + src/renderer/src/i18n/translate/el-gr.json | 4 +- src/renderer/src/i18n/translate/es-es.json | 3 +- src/renderer/src/i18n/translate/fr-fr.json | 4 +- src/renderer/src/i18n/translate/pt-pt.json | 5 ++- .../settings/DataSettings/DataSettings.tsx | 32 ++++++++++++++- .../DataSettings/NutstoreSettings.tsx | 31 +++++++++++--- .../settings/DataSettings/WebDavSettings.tsx | 22 ++++++++-- src/renderer/src/services/BackupService.ts | 10 +++-- src/renderer/src/services/NutstoreService.ts | 8 +++- src/renderer/src/store/nutstore.ts | 17 ++++++-- src/renderer/src/store/settings.ts | 13 ++++++ src/renderer/src/types/index.ts | 1 + 20 files changed, 169 insertions(+), 42 deletions(-) diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index ea8521aa1..4d951f369 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -77,7 +77,8 @@ class BackupManager { _: Electron.IpcMainInvokeEvent, fileName: string, data: string, - destinationPath: string = this.backupDir + destinationPath: string = this.backupDir, + skipBackupFile: boolean = false ): Promise { const mainWindow = windowService.getMainWindow() @@ -104,23 +105,30 @@ class BackupManager { onProgress({ stage: 'writing_data', progress: 20, total: 100 }) - // 复制 Data 目录到临时目录 - const sourcePath = path.join(app.getPath('userData'), 'Data') - const tempDataDir = path.join(this.tempDir, 'Data') + Logger.log('[BackupManager IPC] ', skipBackupFile) - // 获取源目录总大小 - const totalSize = await this.getDirSize(sourcePath) - let copiedSize = 0 + if (!skipBackupFile) { + // 复制 Data 目录到临时目录 + const sourcePath = path.join(app.getPath('userData'), 'Data') + const tempDataDir = path.join(this.tempDir, 'Data') - // 使用流式复制 - await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => { - copiedSize += size - const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50)) - onProgress({ stage: 'copying_files', progress, total: 100 }) - }) + // 获取源目录总大小 + const totalSize = await this.getDirSize(sourcePath) + let copiedSize = 0 - await this.setWritableRecursive(tempDataDir) - onProgress({ stage: 'preparing_compression', progress: 50, total: 100 }) + // 使用流式复制 + await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => { + copiedSize += size + const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50)) + onProgress({ stage: 'copying_files', progress, total: 100 }) + }) + + await this.setWritableRecursive(tempDataDir) + onProgress({ stage: 'preparing_compression', progress: 50, total: 100 }) + } else { + Logger.log('[BackupManager] Skip the backup of the file') + await fs.promises.mkdir(path.join(this.tempDir, 'Data')) // 不创建空 Data 目录会导致 restore 失败 + } // 创建输出文件流 const backupedFilePath = path.join(destinationPath, fileName) @@ -279,7 +287,7 @@ class BackupManager { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' - const backupedFilePath = await this.backup(_, filename, data) + const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile) const webdavClient = new WebDav(webdavConfig) try { const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), { diff --git a/src/preload/index.ts b/src/preload/index.ts index 828557865..3679dd080 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -37,8 +37,8 @@ const api = { decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text) }, backup: { - backup: (fileName: string, data: string, destinationPath?: string) => - ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath), + backup: (fileName: string, data: string, destinationPath?: string, skipBackupFile?: boolean) => + ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath, skipBackupFile), restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath), backupToWebdav: (data: string, webdavConfig: WebDavConfig) => ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig), diff --git a/src/renderer/src/components/Popups/BackupPopup.tsx b/src/renderer/src/components/Popups/BackupPopup.tsx index 41eb268a1..dd19e0010 100644 --- a/src/renderer/src/components/Popups/BackupPopup.tsx +++ b/src/renderer/src/components/Popups/BackupPopup.tsx @@ -1,6 +1,8 @@ import { backup } from '@renderer/services/BackupService' +import store from '@renderer/store' import { IpcChannel } from '@shared/IpcChannel' import { Modal, Progress } from 'antd' +import Logger from 'electron-log' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,6 +22,7 @@ const PopupContainer: React.FC = ({ resolve }) => { const [open, setOpen] = useState(true) const [progressData, setProgressData] = useState() const { t } = useTranslation() + const skipBackupFile = store.getState().settings.skipBackupFile useEffect(() => { const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => { @@ -32,7 +35,8 @@ const PopupContainer: React.FC = ({ resolve }) => { }, []) const onOk = async () => { - await backup() + Logger.log('[BackupManager] ', skipBackupFile) + await backup(skipBackupFile) setOpen(false) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 286673974..5c1e47f05 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -956,6 +956,8 @@ "app_knowledge.remove_all_confirm": "Deleting knowledge base files will reduce the storage space occupied, but will not delete the knowledge base vector data, after deletion, the source file will no longer be able to be opened. Continue?", "app_knowledge.remove_all_success": "Files removed successfully", "app_logs": "App Logs", + "backup.skip_file_data_title": "Slim Backup", + "backup.skip_file_data_help": "Skip backing up data files such as pictures and knowledge bases during backup, and only back up chat records and settings. Reduce space occupancy and speed up the backup speed.", "clear_cache": { "button": "Clear Cache", "confirm": "Clearing the cache will delete application cache data, including minapp data. This action is irreversible, continue?", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 9be762b22..ca3f98c4a 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -953,6 +953,8 @@ "app_knowledge.remove_all": "ナレッジベースファイルを削除", "app_knowledge.remove_all_confirm": "ナレッジベースファイルを削除すると、ナレッジベース自体は削除されません。これにより、ストレージ容量を節約できます。続行しますか?", "app_knowledge.remove_all_success": "ファイル削除成功", + "backup.skip_file_data_title": "精簡バックアップ", + "backup.skip_file_data_help": "バックアップ時に、画像や知識ベースなどのデータファイルをバックアップ対象から除外し、チャット履歴と設定のみをバックアップします。スペースの占有を減らし、バックアップ速度を向上させます。", "app_logs": "アプリログ", "clear_cache": { "button": "キャッシュをクリア", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f749fb2d0..003ac5a44 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -904,6 +904,7 @@ "restore": { "confirm": "Вы уверены, что хотите восстановить данные?", "confirm.button": "Выбрать файл резервной копии", + "content": "Операция восстановления перезапишет все текущие данные приложения данными из резервной копии. Это может занять некоторое время.", "progress": { "completed": "Восстановление завершено", @@ -954,6 +955,8 @@ "app_knowledge.remove_all_confirm": "Удаление файлов базы знаний не удалит саму базу знаний, что позволит уменьшить занимаемый объем памяти, продолжить?", "app_knowledge.remove_all_success": "Файлы удалены успешно", "app_logs": "Логи приложения", + "backup.skip_file_data_title": "Упрощенная резервная копия", + "backup.skip_file_data_help": "Пропустить при резервном копировании такие данные, как изображения, базы знаний и другие файлы данных, и сделать резервную копию только переписки и настроек. Это уменьшает использование места на диске и ускоряет процесс резервного копирования.", "clear_cache": { "button": "Очистка кэша", "confirm": "Очистка кэша удалит данные приложения. Это действие необратимо, продолжить?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e787498e8..9bbdb5aa4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -956,6 +956,8 @@ "app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?", "app_knowledge.remove_all_success": "文件删除成功", "app_logs": "应用日志", + "backup.skip_file_data_title": "精简备份", + "backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度。", "clear_cache": { "button": "清除缓存", "confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8b1eff594..085b50323 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -956,6 +956,8 @@ "app_knowledge.remove_all_confirm": "刪除知識庫文件可以減少儲存空間佔用,但不會刪除知識庫向量化資料,刪除之後將無法開啟原始檔,是否刪除?", "app_knowledge.remove_all_success": "檔案刪除成功", "app_logs": "應用程式日誌", + "backup.skip_file_data_title": "精簡備份", + "backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度。", "clear_cache": { "button": "清除快取", "confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 8f2cb2b29..394d27185 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -930,6 +930,8 @@ "app_knowledge.remove_all_confirm": "Η διαγραφή των αρχείων της βάσης γνώσεων μπορεί να μειώσει τη χρήση χώρου αποθήκευσης, αλλά δεν θα διαγράψει τα διανυσματωτικά δεδομένα της βάσης γνώσεων. Μετά τη διαγραφή, δεν θα μπορείτε να ανοίξετε τα αρχεία πηγή. Θέλετε να διαγράψετε;", "app_knowledge.remove_all_success": "Τα αρχεία διαγράφηκαν με επιτυχία", "app_logs": "Φάκελοι εφαρμογής", + "backup.skip_file_data_title": "Συμπυκνωμένο αντίγραφο ασφαλείας", + "backup.skip_file_data_help": "Κατά τη δημιουργία αντιγράφων ασφαλείας, παραλείψτε τις εικόνες, τις βάσεις γνώσεων και άλλα αρχεία δεδομένων. Δημιουργήστε αντίγραφα μόνο για το ιστορικό συνομιλιών και τις ρυθμίσεις. Αυτό θα μειώσει τη χρήση χώρου και θα επιταχύνει την ταχύτητα δημιουργίας αντιγράφων.", "clear_cache": { "button": "Καθαρισμός Μνήμης", "confirm": "Η διαγραφή της μνήμης θα διαγράψει τα στοιχεία καθαρισμού της εφαρμογής, συμπεριλαμβανομένων των στοιχείων πρόσθετων εφαρμογών. Αυτή η ενέργεια δεν είναι αναστρέψιμη. Θέλετε να συνεχίσετε;", @@ -1655,4 +1657,4 @@ "visualization": "προβολή" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 8dc1ad294..f71f859ce 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -107,6 +107,7 @@ "backup": { "confirm": "¿Está seguro de que desea realizar una copia de seguridad de los datos?", "confirm.button": "Seleccionar ubicación de copia de seguridad", + "confirm.file_checkbox": "El tamaño del archivo es {{size}}, ¿desea elegir el archivo de copia de seguridad?", "content": "Realizar una copia de seguridad de todos los datos, incluyendo registros de chat, configuraciones, bases de conocimiento y todos los demás datos. Tenga en cuenta que el proceso de copia de seguridad puede llevar algún tiempo, gracias por su paciencia.", "progress": { "completed": "Copia de seguridad completada", @@ -1655,4 +1656,4 @@ "visualization": "Visualización" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index d80f63c02..725f89717 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -930,6 +930,8 @@ "app_knowledge.remove_all_confirm": "La suppression des fichiers de la base de connaissances libérera de l'espace de stockage, mais ne supprimera pas les données vectorisées de la base de connaissances. Après la suppression, vous ne pourrez plus ouvrir les fichiers sources. Souhaitez-vous continuer ?", "app_knowledge.remove_all_success": "Fichiers supprimés avec succès", "app_logs": "Journaux de l'application", + "backup.skip_file_data_title": "Sauvegarde réduite", + "backup.skip_file_data_help": "Passer outre les fichiers de données tels que les images et les bases de connaissances lors de la sauvegarde, et ne sauvegarder que les conversations et les paramètres. Cela réduit l'occupation d'espace et accélère la vitesse de sauvegarde.", "clear_cache": { "button": "Effacer le cache", "confirm": "L'effacement du cache supprimera les données du cache de l'application, y compris les données des mini-programmes. Cette action ne peut pas être annulée, voulez-vous continuer ?", @@ -1655,4 +1657,4 @@ "visualization": "Visualisation" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 52ae44702..d4b536240 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -107,6 +107,7 @@ "backup": { "confirm": "Tem certeza de que deseja fazer backup dos dados?", "confirm.button": "Escolher local de backup", + "confirm.file_checkbox": "Pule a cópia de segurança de arquivos de dados como imagens e banco de conhecimento e copie apenas as conversas e as configurações.", "content": "Fazer backup de todos os dados, incluindo registros de chat, configurações, base de conhecimento e todos os outros dados. Por favor, note que o processo de backup pode levar algum tempo. Agradecemos sua paciência.", "progress": { "completed": "Backup concluído", @@ -931,6 +932,8 @@ "app_knowledge.remove_all_confirm": "A exclusão dos arquivos da base de conhecimento reduzirá o uso do espaço de armazenamento, mas não excluirá os dados vetoriais da base de conhecimento. Após a exclusão, os arquivos originais não poderão ser abertos. Deseja excluir?", "app_knowledge.remove_all_success": "Arquivo excluído com sucesso", "app_logs": "Logs do aplicativo", + "backup.skip_file_data_title": "Backup simplificado", + "backup.skip_file_data_help": "Pule arquivos de dados como imagens e bancos de conhecimento durante o backup e realize apenas o backup das conversas e configurações. Diminua o consumo de espaço e aumente a velocidade do backup.", "clear_cache": { "button": "Limpar cache", "confirm": "Limpar cache removerá os dados armazenados em cache do aplicativo, incluindo dados de aplicativos minúsculos. Esta ação não pode ser desfeita, deseja continuar?", @@ -1656,4 +1659,4 @@ "visualization": "Visualização" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index ec31fd6ae..90e25de51 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -14,15 +14,25 @@ import RestorePopup from '@renderer/components/Popups/RestorePopup' import { useTheme } from '@renderer/context/ThemeProvider' import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles' import { reset } from '@renderer/services/BackupService' +import store, { useAppDispatch } from '@renderer/store' +import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/settings' import { AppInfo } from '@renderer/types' import { formatFileSize } from '@renderer/utils' -import { Button, Typography } from 'antd' +import { Button, Switch, Typography } from 'antd' import { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import { + SettingContainer, + SettingDivider, + SettingGroup, + SettingHelpText, + SettingRow, + SettingRowTitle, + SettingTitle +} from '..' import AgentsSubscribeUrlSettings from './AgentsSubscribeUrlSettings' import ExportMenuOptions from './ExportMenuSettings' import JoplinSettings from './JoplinSettings' @@ -42,6 +52,11 @@ const DataSettings: FC = () => { const { theme } = useTheme() const [menu, setMenu] = useState('data') + const _skipBackupFile = store.getState().settings.skipBackupFile + const [skipBackupFile, setSkipBackupFile] = useState(_skipBackupFile) + + const dispatch = useAppDispatch() + //joplin icon needs to be updated into iconfont const JoplinIcon = () => ( @@ -164,6 +179,11 @@ const DataSettings: FC = () => { }) } + const onSkipBackupFilesChange = (value: boolean) => { + setSkipBackupFile(value) + dispatch(_setSkipBackupFile(value)) + } + return ( @@ -208,6 +228,14 @@ const DataSettings: FC = () => { + + + {t('settings.data.backup.skip_file_data_title')} + + + + {t('settings.data.backup.skip_file_data_help')} + {t('settings.data.data.title')} diff --git a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx index 8ef343230..6b9784cea 100644 --- a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx @@ -17,25 +17,31 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import { setNutstoreAutoSync, setNutstorePath, + setNutstoreSkipBackupFile, setNutstoreSyncInterval, setNutstoreToken } from '@renderer/store/nutstore' import { modalConfirm } from '@renderer/utils' import { NUTSTORE_HOST } from '@shared/config/nutstore' -import { Button, Input, Select, Tooltip, Typography } from 'antd' +import { Button, Input, Select, Switch, Tooltip, Typography } from 'antd' import dayjs from 'dayjs' import { FC, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { type FileStat } from 'webdav' -import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' const NutstoreSettings: FC = () => { const { theme } = useTheme() const { t } = useTranslation() - const { nutstoreToken, nutstorePath, nutstoreSyncInterval, nutstoreAutoSync, nutstoreSyncState } = useAppSelector( - (state) => state.nutstore - ) + const { + nutstoreToken, + nutstorePath, + nutstoreSyncInterval, + nutstoreAutoSync, + nutstoreSyncState, + nutstoreSkipBackupFile + } = useAppSelector((state) => state.nutstore) const dispatch = useAppDispatch() @@ -48,6 +54,8 @@ const NutstoreSettings: FC = () => { const [syncInterval, setSyncInterval] = useState(nutstoreSyncInterval) + const [nutSkipBackupFile, setNutSkipBackupFile] = useState(nutstoreSkipBackupFile) + const nutstoreSSOHandler = useNutstoreSSO() const [backupManagerVisible, setBackupManagerVisible] = useState(false) @@ -128,6 +136,11 @@ const NutstoreSettings: FC = () => { } } + const onSkipBackupFilesChange = (value: boolean) => { + setNutSkipBackupFile(value) + dispatch(setNutstoreSkipBackupFile(value)) + } + const handleClickPathChange = async () => { if (!nutstoreToken) { return @@ -287,6 +300,14 @@ const NutstoreSettings: FC = () => { )} + + + {t('settings.data.backup.skip_file_data_title')} + + + + {t('settings.data.backup.skip_file_data_help')} + )} <> diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index 29221f9c0..104fa5cbd 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -12,15 +12,16 @@ import { setWebdavMaxBackups as _setWebdavMaxBackups, setWebdavPass as _setWebdavPass, setWebdavPath as _setWebdavPath, + setWebdavSkipBackupFile as _setWebdavSkipBackupFile, setWebdavSyncInterval as _setWebdavSyncInterval, setWebdavUser as _setWebdavUser } from '@renderer/store/settings' -import { Button, Input, Select, Tooltip } from 'antd' +import { Button, Input, Select, Switch, Tooltip } from 'antd' import dayjs from 'dayjs' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' -import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' const WebDavSettings: FC = () => { const { @@ -29,13 +30,15 @@ const WebDavSettings: FC = () => { webdavPass: webDAVPass, webdavPath: webDAVPath, webdavSyncInterval: webDAVSyncInterval, - webdavMaxBackups: webDAVMaxBackups + webdavMaxBackups: webDAVMaxBackups, + webdavSkipBackupFile: webdDAVSkipBackupFile } = useSettings() const [webdavHost, setWebdavHost] = useState(webDAVHost) const [webdavUser, setWebdavUser] = useState(webDAVUser) const [webdavPass, setWebdavPass] = useState(webDAVPass) const [webdavPath, setWebdavPath] = useState(webDAVPath) + const [webdavSkipBackupFile, setWebdavSkipBackupFile] = useState(webdDAVSkipBackupFile) const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [syncInterval, setSyncInterval] = useState(webDAVSyncInterval) @@ -67,6 +70,11 @@ const WebDavSettings: FC = () => { dispatch(_setWebdavMaxBackups(value)) } + const onSkipBackupFilesChange = (value: boolean) => { + setWebdavSkipBackupFile(value) + dispatch(_setWebdavSkipBackupFile(value)) + } + const renderSyncStatus = () => { if (!webdavHost) return null @@ -194,6 +202,14 @@ const WebDavSettings: FC = () => { 50 + + + {t('settings.data.backup.skip_file_data_title')} + + + + {t('settings.data.backup.skip_file_data_help')} + {webdavSync && syncInterval > 0 && ( <> diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index a40b94e0f..e0720e36d 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -6,12 +6,12 @@ import store from '@renderer/store' import { setWebDAVSyncState } from '@renderer/store/backup' import dayjs from 'dayjs' -export async function backup() { +export async function backup(skipBackupFile: boolean) { const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip` const fileContnet = await getBackupData() const selectFolder = await window.api.file.selectFolder() if (selectFolder) { - await window.api.backup.backup(filename, fileContnet, selectFolder) + await window.api.backup.backup(filename, fileContnet, selectFolder, skipBackupFile) window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) } } @@ -83,7 +83,8 @@ export async function backupToWebdav({ store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null })) - const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups } = store.getState().settings + const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups, webdavSkipBackupFile } = + store.getState().settings let deviceType = 'unknown' let hostname = 'unknown' try { @@ -104,7 +105,8 @@ export async function backupToWebdav({ webdavUser, webdavPass, webdavPath, - fileName: finalFileName + fileName: finalFileName, + skipBackupFile: webdavSkipBackupFile }) if (success) { store.dispatch( diff --git a/src/renderer/src/services/NutstoreService.ts b/src/renderer/src/services/NutstoreService.ts index bec3f477e..e576a37ff 100644 --- a/src/renderer/src/services/NutstoreService.ts +++ b/src/renderer/src/services/NutstoreService.ts @@ -98,9 +98,13 @@ export async function backupToNutstore({ store.dispatch(setNutstoreSyncState({ syncing: true, lastSyncError: null })) const backupData = await getBackupData() - + const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile try { - const isSuccess = await window.api.backup.backupToWebdav(backupData, { ...config, fileName: finalFileName }) + const isSuccess = await window.api.backup.backupToWebdav(backupData, { + ...config, + fileName: finalFileName, + skipBackupFile: skipBackupFile + }) if (isSuccess) { store.dispatch( diff --git a/src/renderer/src/store/nutstore.ts b/src/renderer/src/store/nutstore.ts index 97542a30b..354a93bd3 100644 --- a/src/renderer/src/store/nutstore.ts +++ b/src/renderer/src/store/nutstore.ts @@ -10,6 +10,7 @@ export interface NutstoreState { nutstoreAutoSync: boolean nutstoreSyncInterval: number nutstoreSyncState: NutstoreSyncState + nutstoreSkipBackupFile: boolean } const initialState: NutstoreState = { @@ -21,7 +22,8 @@ const initialState: NutstoreState = { lastSyncTime: null, syncing: false, lastSyncError: null - } + }, + nutstoreSkipBackupFile: false } const nutstoreSlice = createSlice({ @@ -42,11 +44,20 @@ const nutstoreSlice = createSlice({ }, setNutstoreSyncState: (state, action: PayloadAction>) => { state.nutstoreSyncState = { ...state.nutstoreSyncState, ...action.payload } + }, + setNutstoreSkipBackupFile: (state, action: PayloadAction) => { + state.nutstoreSkipBackupFile = action.payload } } }) -export const { setNutstoreToken, setNutstorePath, setNutstoreAutoSync, setNutstoreSyncInterval, setNutstoreSyncState } = - nutstoreSlice.actions +export const { + setNutstoreToken, + setNutstorePath, + setNutstoreAutoSync, + setNutstoreSyncInterval, + setNutstoreSyncState, + setNutstoreSkipBackupFile +} = nutstoreSlice.actions export default nutstoreSlice.reducer diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 81a06c756..f68eac0a4 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -77,6 +77,8 @@ export interface SettingsState { gridColumns: number gridPopoverTrigger: 'hover' | 'click' messageNavigation: 'none' | 'buttons' | 'anchor' + // 数据目录设置 + skipBackupFile: boolean // webdav 配置 host, user, pass, path webdavHost: string webdavUser: string @@ -85,6 +87,7 @@ export interface SettingsState { webdavAutoSync: boolean webdavSyncInterval: number webdavMaxBackups: number + webdavSkipBackupFile: boolean translateModelPrompt: string autoTranslateWithSpace: boolean showTranslateConfirm: boolean @@ -202,6 +205,7 @@ export const initialState: SettingsState = { gridColumns: 2, gridPopoverTrigger: 'click', messageNavigation: 'none', + skipBackupFile: false, webdavHost: '', webdavUser: '', webdavPass: '', @@ -209,6 +213,7 @@ export const initialState: SettingsState = { webdavAutoSync: false, webdavSyncInterval: 0, webdavMaxBackups: 0, + webdavSkipBackupFile: false, translateModelPrompt: TRANSLATE_PROMPT, autoTranslateWithSpace: false, showTranslateConfirm: true, @@ -356,6 +361,9 @@ const settingsSlice = createSlice({ setClickAssistantToShowTopic: (state, action: PayloadAction) => { state.clickAssistantToShowTopic = action.payload }, + setSkipBackupFile: (state, action: PayloadAction) => { + state.skipBackupFile = action.payload + }, setWebdavHost: (state, action: PayloadAction) => { state.webdavHost = action.payload }, @@ -377,6 +385,9 @@ const settingsSlice = createSlice({ setWebdavMaxBackups: (state, action: PayloadAction) => { state.webdavMaxBackups = action.payload }, + setWebdavSkipBackupFile: (state, action: PayloadAction) => { + state.webdavSkipBackupFile = action.payload + }, setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => { if (action.payload.enabled !== undefined) { state.codeExecution.enabled = action.payload.enabled @@ -611,6 +622,7 @@ export const { setAutoCheckUpdate, setRenderInputMessageAsMarkdown, setClickAssistantToShowTopic, + setSkipBackupFile, setWebdavHost, setWebdavUser, setWebdavPass, @@ -618,6 +630,7 @@ export const { setWebdavAutoSync, setWebdavSyncInterval, setWebdavMaxBackups, + setWebdavSkipBackupFile, setCodeExecution, setCodeEditor, setCodePreview, diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 2a0d77260..dabfd6049 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -313,6 +313,7 @@ export type WebDavConfig = { webdavPass: string webdavPath: string fileName?: string + skipBackupFile?: boolean } export type AppInfo = {