feat: support skipping files during backup(slim backup) (#6075)

* feat: support skipping files during backup

* 修复 lint

* 修复 lint

---------

Co-authored-by: daisai.1 <daisai.1@bytedance.com>
This commit is contained in:
Rudbeckia.hirta.L 2025-05-17 19:20:40 +08:00 committed by GitHub
parent fe45a68260
commit 365eb9bab5
20 changed files with 169 additions and 42 deletions

View File

@ -77,7 +77,8 @@ class BackupManager {
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
fileName: string, fileName: string,
data: string, data: string,
destinationPath: string = this.backupDir destinationPath: string = this.backupDir,
skipBackupFile: boolean = false
): Promise<string> { ): Promise<string> {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
@ -104,23 +105,30 @@ class BackupManager {
onProgress({ stage: 'writing_data', progress: 20, total: 100 }) onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录 Logger.log('[BackupManager IPC] ', skipBackupFile)
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
// 获取源目录总大小 if (!skipBackupFile) {
const totalSize = await this.getDirSize(sourcePath) // 复制 Data 目录到临时目录
let copiedSize = 0 const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
// 使用流式复制 // 获取源目录总大小
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => { const totalSize = await this.getDirSize(sourcePath)
copiedSize += size let copiedSize = 0
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 }) 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) const backupedFilePath = path.join(destinationPath, fileName)
@ -279,7 +287,7 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' 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) const webdavClient = new WebDav(webdavConfig)
try { try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), { const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {

View File

@ -37,8 +37,8 @@ const api = {
decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text) decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
}, },
backup: { backup: {
backup: (fileName: string, data: string, destinationPath?: string) => backup: (fileName: string, data: string, destinationPath?: string, skipBackupFile?: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath), ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath, skipBackupFile),
restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath), restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig), ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),

View File

@ -1,6 +1,8 @@
import { backup } from '@renderer/services/BackupService' import { backup } from '@renderer/services/BackupService'
import store from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Modal, Progress } from 'antd' import { Modal, Progress } from 'antd'
import Logger from 'electron-log'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -20,6 +22,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>() const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation() const { t } = useTranslation()
const skipBackupFile = store.getState().settings.skipBackupFile
useEffect(() => { useEffect(() => {
const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => { const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => {
@ -32,7 +35,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
}, []) }, [])
const onOk = async () => { const onOk = async () => {
await backup() Logger.log('[BackupManager] ', skipBackupFile)
await backup(skipBackupFile)
setOpen(false) setOpen(false)
} }

View File

@ -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_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_knowledge.remove_all_success": "Files removed successfully",
"app_logs": "App Logs", "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": { "clear_cache": {
"button": "Clear Cache", "button": "Clear Cache",
"confirm": "Clearing the cache will delete application cache data, including minapp data. This action is irreversible, continue?", "confirm": "Clearing the cache will delete application cache data, including minapp data. This action is irreversible, continue?",

View File

@ -953,6 +953,8 @@
"app_knowledge.remove_all": "ナレッジベースファイルを削除", "app_knowledge.remove_all": "ナレッジベースファイルを削除",
"app_knowledge.remove_all_confirm": "ナレッジベースファイルを削除すると、ナレッジベース自体は削除されません。これにより、ストレージ容量を節約できます。続行しますか?", "app_knowledge.remove_all_confirm": "ナレッジベースファイルを削除すると、ナレッジベース自体は削除されません。これにより、ストレージ容量を節約できます。続行しますか?",
"app_knowledge.remove_all_success": "ファイル削除成功", "app_knowledge.remove_all_success": "ファイル削除成功",
"backup.skip_file_data_title": "精簡バックアップ",
"backup.skip_file_data_help": "バックアップ時に、画像や知識ベースなどのデータファイルをバックアップ対象から除外し、チャット履歴と設定のみをバックアップします。スペースの占有を減らし、バックアップ速度を向上させます。",
"app_logs": "アプリログ", "app_logs": "アプリログ",
"clear_cache": { "clear_cache": {
"button": "キャッシュをクリア", "button": "キャッシュをクリア",

View File

@ -904,6 +904,7 @@
"restore": { "restore": {
"confirm": "Вы уверены, что хотите восстановить данные?", "confirm": "Вы уверены, что хотите восстановить данные?",
"confirm.button": "Выбрать файл резервной копии", "confirm.button": "Выбрать файл резервной копии",
"content": "Операция восстановления перезапишет все текущие данные приложения данными из резервной копии. Это может занять некоторое время.", "content": "Операция восстановления перезапишет все текущие данные приложения данными из резервной копии. Это может занять некоторое время.",
"progress": { "progress": {
"completed": "Восстановление завершено", "completed": "Восстановление завершено",
@ -954,6 +955,8 @@
"app_knowledge.remove_all_confirm": "Удаление файлов базы знаний не удалит саму базу знаний, что позволит уменьшить занимаемый объем памяти, продолжить?", "app_knowledge.remove_all_confirm": "Удаление файлов базы знаний не удалит саму базу знаний, что позволит уменьшить занимаемый объем памяти, продолжить?",
"app_knowledge.remove_all_success": "Файлы удалены успешно", "app_knowledge.remove_all_success": "Файлы удалены успешно",
"app_logs": "Логи приложения", "app_logs": "Логи приложения",
"backup.skip_file_data_title": "Упрощенная резервная копия",
"backup.skip_file_data_help": "Пропустить при резервном копировании такие данные, как изображения, базы знаний и другие файлы данных, и сделать резервную копию только переписки и настроек. Это уменьшает использование места на диске и ускоряет процесс резервного копирования.",
"clear_cache": { "clear_cache": {
"button": "Очистка кэша", "button": "Очистка кэша",
"confirm": "Очистка кэша удалит данные приложения. Это действие необратимо, продолжить?", "confirm": "Очистка кэша удалит данные приложения. Это действие необратимо, продолжить?",

View File

@ -956,6 +956,8 @@
"app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?", "app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?",
"app_knowledge.remove_all_success": "文件删除成功", "app_knowledge.remove_all_success": "文件删除成功",
"app_logs": "应用日志", "app_logs": "应用日志",
"backup.skip_file_data_title": "精简备份",
"backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度。",
"clear_cache": { "clear_cache": {
"button": "清除缓存", "button": "清除缓存",
"confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?", "confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?",

View File

@ -956,6 +956,8 @@
"app_knowledge.remove_all_confirm": "刪除知識庫文件可以減少儲存空間佔用,但不會刪除知識庫向量化資料,刪除之後將無法開啟原始檔,是否刪除?", "app_knowledge.remove_all_confirm": "刪除知識庫文件可以減少儲存空間佔用,但不會刪除知識庫向量化資料,刪除之後將無法開啟原始檔,是否刪除?",
"app_knowledge.remove_all_success": "檔案刪除成功", "app_knowledge.remove_all_success": "檔案刪除成功",
"app_logs": "應用程式日誌", "app_logs": "應用程式日誌",
"backup.skip_file_data_title": "精簡備份",
"backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度。",
"clear_cache": { "clear_cache": {
"button": "清除快取", "button": "清除快取",
"confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?", "confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?",

View File

@ -930,6 +930,8 @@
"app_knowledge.remove_all_confirm": "Η διαγραφή των αρχείων της βάσης γνώσεων μπορεί να μειώσει τη χρήση χώρου αποθήκευσης, αλλά δεν θα διαγράψει τα διανυσματωτικά δεδομένα της βάσης γνώσεων. Μετά τη διαγραφή, δεν θα μπορείτε να ανοίξετε τα αρχεία πηγή. Θέλετε να διαγράψετε;", "app_knowledge.remove_all_confirm": "Η διαγραφή των αρχείων της βάσης γνώσεων μπορεί να μειώσει τη χρήση χώρου αποθήκευσης, αλλά δεν θα διαγράψει τα διανυσματωτικά δεδομένα της βάσης γνώσεων. Μετά τη διαγραφή, δεν θα μπορείτε να ανοίξετε τα αρχεία πηγή. Θέλετε να διαγράψετε;",
"app_knowledge.remove_all_success": "Τα αρχεία διαγράφηκαν με επιτυχία", "app_knowledge.remove_all_success": "Τα αρχεία διαγράφηκαν με επιτυχία",
"app_logs": "Φάκελοι εφαρμογής", "app_logs": "Φάκελοι εφαρμογής",
"backup.skip_file_data_title": "Συμπυκνωμένο αντίγραφο ασφαλείας",
"backup.skip_file_data_help": "Κατά τη δημιουργία αντιγράφων ασφαλείας, παραλείψτε τις εικόνες, τις βάσεις γνώσεων και άλλα αρχεία δεδομένων. Δημιουργήστε αντίγραφα μόνο για το ιστορικό συνομιλιών και τις ρυθμίσεις. Αυτό θα μειώσει τη χρήση χώρου και θα επιταχύνει την ταχύτητα δημιουργίας αντιγράφων.",
"clear_cache": { "clear_cache": {
"button": "Καθαρισμός Μνήμης", "button": "Καθαρισμός Μνήμης",
"confirm": "Η διαγραφή της μνήμης θα διαγράψει τα στοιχεία καθαρισμού της εφαρμογής, συμπεριλαμβανομένων των στοιχείων πρόσθετων εφαρμογών. Αυτή η ενέργεια δεν είναι αναστρέψιμη. Θέλετε να συνεχίσετε;", "confirm": "Η διαγραφή της μνήμης θα διαγράψει τα στοιχεία καθαρισμού της εφαρμογής, συμπεριλαμβανομένων των στοιχείων πρόσθετων εφαρμογών. Αυτή η ενέργεια δεν είναι αναστρέψιμη. Θέλετε να συνεχίσετε;",
@ -1655,4 +1657,4 @@
"visualization": "προβολή" "visualization": "προβολή"
} }
} }
} }

View File

@ -107,6 +107,7 @@
"backup": { "backup": {
"confirm": "¿Está seguro de que desea realizar una copia de seguridad de los datos?", "confirm": "¿Está seguro de que desea realizar una copia de seguridad de los datos?",
"confirm.button": "Seleccionar ubicación de copia de seguridad", "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.", "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": { "progress": {
"completed": "Copia de seguridad completada", "completed": "Copia de seguridad completada",
@ -1655,4 +1656,4 @@
"visualization": "Visualización" "visualization": "Visualización"
} }
} }
} }

View File

@ -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_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_knowledge.remove_all_success": "Fichiers supprimés avec succès",
"app_logs": "Journaux de l'application", "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": { "clear_cache": {
"button": "Effacer le 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 ?", "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" "visualization": "Visualisation"
} }
} }
} }

View File

@ -107,6 +107,7 @@
"backup": { "backup": {
"confirm": "Tem certeza de que deseja fazer backup dos dados?", "confirm": "Tem certeza de que deseja fazer backup dos dados?",
"confirm.button": "Escolher local de backup", "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.", "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": { "progress": {
"completed": "Backup concluído", "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_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_knowledge.remove_all_success": "Arquivo excluído com sucesso",
"app_logs": "Logs do aplicativo", "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": { "clear_cache": {
"button": "Limpar 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?", "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" "visualization": "Visualização"
} }
} }
} }

View File

@ -14,15 +14,25 @@ import RestorePopup from '@renderer/components/Popups/RestorePopup'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles' import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles'
import { reset } from '@renderer/services/BackupService' 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 { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' 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 { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' 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 AgentsSubscribeUrlSettings from './AgentsSubscribeUrlSettings'
import ExportMenuOptions from './ExportMenuSettings' import ExportMenuOptions from './ExportMenuSettings'
import JoplinSettings from './JoplinSettings' import JoplinSettings from './JoplinSettings'
@ -42,6 +52,11 @@ const DataSettings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const [menu, setMenu] = useState<string>('data') const [menu, setMenu] = useState<string>('data')
const _skipBackupFile = store.getState().settings.skipBackupFile
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(_skipBackupFile)
const dispatch = useAppDispatch()
//joplin icon needs to be updated into iconfont //joplin icon needs to be updated into iconfont
const JoplinIcon = () => ( const JoplinIcon = () => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-icon)" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-icon)" xmlns="http://www.w3.org/2000/svg">
@ -164,6 +179,11 @@ const DataSettings: FC = () => {
}) })
} }
const onSkipBackupFilesChange = (value: boolean) => {
setSkipBackupFile(value)
dispatch(_setSkipBackupFile(value))
}
return ( return (
<Container> <Container>
<MenuList> <MenuList>
@ -208,6 +228,14 @@ const DataSettings: FC = () => {
</Button> </Button>
</HStack> </HStack>
</SettingRow> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
</SettingGroup> </SettingGroup>
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.data.title')}</SettingTitle> <SettingTitle>{t('settings.data.data.title')}</SettingTitle>

View File

@ -17,25 +17,31 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
setNutstoreAutoSync, setNutstoreAutoSync,
setNutstorePath, setNutstorePath,
setNutstoreSkipBackupFile,
setNutstoreSyncInterval, setNutstoreSyncInterval,
setNutstoreToken setNutstoreToken
} from '@renderer/store/nutstore' } from '@renderer/store/nutstore'
import { modalConfirm } from '@renderer/utils' import { modalConfirm } from '@renderer/utils'
import { NUTSTORE_HOST } from '@shared/config/nutstore' 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 dayjs from 'dayjs'
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { type FileStat } from 'webdav' import { type FileStat } from 'webdav'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const NutstoreSettings: FC = () => { const NutstoreSettings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const { nutstoreToken, nutstorePath, nutstoreSyncInterval, nutstoreAutoSync, nutstoreSyncState } = useAppSelector( const {
(state) => state.nutstore nutstoreToken,
) nutstorePath,
nutstoreSyncInterval,
nutstoreAutoSync,
nutstoreSyncState,
nutstoreSkipBackupFile
} = useAppSelector((state) => state.nutstore)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -48,6 +54,8 @@ const NutstoreSettings: FC = () => {
const [syncInterval, setSyncInterval] = useState<number>(nutstoreSyncInterval) const [syncInterval, setSyncInterval] = useState<number>(nutstoreSyncInterval)
const [nutSkipBackupFile, setNutSkipBackupFile] = useState<boolean>(nutstoreSkipBackupFile)
const nutstoreSSOHandler = useNutstoreSSO() const nutstoreSSOHandler = useNutstoreSSO()
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
@ -128,6 +136,11 @@ const NutstoreSettings: FC = () => {
} }
} }
const onSkipBackupFilesChange = (value: boolean) => {
setNutSkipBackupFile(value)
dispatch(setNutstoreSkipBackupFile(value))
}
const handleClickPathChange = async () => { const handleClickPathChange = async () => {
if (!nutstoreToken) { if (!nutstoreToken) {
return return
@ -287,6 +300,14 @@ const NutstoreSettings: FC = () => {
</SettingRow> </SettingRow>
</> </>
)} )}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={nutSkipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
</> </>
)} )}
<> <>

View File

@ -12,15 +12,16 @@ import {
setWebdavMaxBackups as _setWebdavMaxBackups, setWebdavMaxBackups as _setWebdavMaxBackups,
setWebdavPass as _setWebdavPass, setWebdavPass as _setWebdavPass,
setWebdavPath as _setWebdavPath, setWebdavPath as _setWebdavPath,
setWebdavSkipBackupFile as _setWebdavSkipBackupFile,
setWebdavSyncInterval as _setWebdavSyncInterval, setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser setWebdavUser as _setWebdavUser
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { Button, Input, Select, Tooltip } from 'antd' import { Button, Input, Select, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const WebDavSettings: FC = () => { const WebDavSettings: FC = () => {
const { const {
@ -29,13 +30,15 @@ const WebDavSettings: FC = () => {
webdavPass: webDAVPass, webdavPass: webDAVPass,
webdavPath: webDAVPath, webdavPath: webDAVPath,
webdavSyncInterval: webDAVSyncInterval, webdavSyncInterval: webDAVSyncInterval,
webdavMaxBackups: webDAVMaxBackups webdavMaxBackups: webDAVMaxBackups,
webdavSkipBackupFile: webdDAVSkipBackupFile
} = useSettings() } = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost) const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser) const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass) const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath) const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [webdavSkipBackupFile, setWebdavSkipBackupFile] = useState<boolean>(webdDAVSkipBackupFile)
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval) const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
@ -67,6 +70,11 @@ const WebDavSettings: FC = () => {
dispatch(_setWebdavMaxBackups(value)) dispatch(_setWebdavMaxBackups(value))
} }
const onSkipBackupFilesChange = (value: boolean) => {
setWebdavSkipBackupFile(value)
dispatch(_setWebdavSkipBackupFile(value))
}
const renderSyncStatus = () => { const renderSyncStatus = () => {
if (!webdavHost) return null if (!webdavHost) return null
@ -194,6 +202,14 @@ const WebDavSettings: FC = () => {
<Select.Option value={50}>50</Select.Option> <Select.Option value={50}>50</Select.Option>
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={webdavSkipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
{webdavSync && syncInterval > 0 && ( {webdavSync && syncInterval > 0 && (
<> <>
<SettingDivider /> <SettingDivider />

View File

@ -6,12 +6,12 @@ import store from '@renderer/store'
import { setWebDAVSyncState } from '@renderer/store/backup' import { setWebDAVSyncState } from '@renderer/store/backup'
import dayjs from 'dayjs' import dayjs from 'dayjs'
export async function backup() { export async function backup(skipBackupFile: boolean) {
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip` const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
const fileContnet = await getBackupData() const fileContnet = await getBackupData()
const selectFolder = await window.api.file.selectFolder() const selectFolder = await window.api.file.selectFolder()
if (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' }) 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 })) 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 deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
try { try {
@ -104,7 +105,8 @@ export async function backupToWebdav({
webdavUser, webdavUser,
webdavPass, webdavPass,
webdavPath, webdavPath,
fileName: finalFileName fileName: finalFileName,
skipBackupFile: webdavSkipBackupFile
}) })
if (success) { if (success) {
store.dispatch( store.dispatch(

View File

@ -98,9 +98,13 @@ export async function backupToNutstore({
store.dispatch(setNutstoreSyncState({ syncing: true, lastSyncError: null })) store.dispatch(setNutstoreSyncState({ syncing: true, lastSyncError: null }))
const backupData = await getBackupData() const backupData = await getBackupData()
const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile
try { 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) { if (isSuccess) {
store.dispatch( store.dispatch(

View File

@ -10,6 +10,7 @@ export interface NutstoreState {
nutstoreAutoSync: boolean nutstoreAutoSync: boolean
nutstoreSyncInterval: number nutstoreSyncInterval: number
nutstoreSyncState: NutstoreSyncState nutstoreSyncState: NutstoreSyncState
nutstoreSkipBackupFile: boolean
} }
const initialState: NutstoreState = { const initialState: NutstoreState = {
@ -21,7 +22,8 @@ const initialState: NutstoreState = {
lastSyncTime: null, lastSyncTime: null,
syncing: false, syncing: false,
lastSyncError: null lastSyncError: null
} },
nutstoreSkipBackupFile: false
} }
const nutstoreSlice = createSlice({ const nutstoreSlice = createSlice({
@ -42,11 +44,20 @@ const nutstoreSlice = createSlice({
}, },
setNutstoreSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => { setNutstoreSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
state.nutstoreSyncState = { ...state.nutstoreSyncState, ...action.payload } state.nutstoreSyncState = { ...state.nutstoreSyncState, ...action.payload }
},
setNutstoreSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.nutstoreSkipBackupFile = action.payload
} }
} }
}) })
export const { setNutstoreToken, setNutstorePath, setNutstoreAutoSync, setNutstoreSyncInterval, setNutstoreSyncState } = export const {
nutstoreSlice.actions setNutstoreToken,
setNutstorePath,
setNutstoreAutoSync,
setNutstoreSyncInterval,
setNutstoreSyncState,
setNutstoreSkipBackupFile
} = nutstoreSlice.actions
export default nutstoreSlice.reducer export default nutstoreSlice.reducer

View File

@ -77,6 +77,8 @@ export interface SettingsState {
gridColumns: number gridColumns: number
gridPopoverTrigger: 'hover' | 'click' gridPopoverTrigger: 'hover' | 'click'
messageNavigation: 'none' | 'buttons' | 'anchor' messageNavigation: 'none' | 'buttons' | 'anchor'
// 数据目录设置
skipBackupFile: boolean
// webdav 配置 host, user, pass, path // webdav 配置 host, user, pass, path
webdavHost: string webdavHost: string
webdavUser: string webdavUser: string
@ -85,6 +87,7 @@ export interface SettingsState {
webdavAutoSync: boolean webdavAutoSync: boolean
webdavSyncInterval: number webdavSyncInterval: number
webdavMaxBackups: number webdavMaxBackups: number
webdavSkipBackupFile: boolean
translateModelPrompt: string translateModelPrompt: string
autoTranslateWithSpace: boolean autoTranslateWithSpace: boolean
showTranslateConfirm: boolean showTranslateConfirm: boolean
@ -202,6 +205,7 @@ export const initialState: SettingsState = {
gridColumns: 2, gridColumns: 2,
gridPopoverTrigger: 'click', gridPopoverTrigger: 'click',
messageNavigation: 'none', messageNavigation: 'none',
skipBackupFile: false,
webdavHost: '', webdavHost: '',
webdavUser: '', webdavUser: '',
webdavPass: '', webdavPass: '',
@ -209,6 +213,7 @@ export const initialState: SettingsState = {
webdavAutoSync: false, webdavAutoSync: false,
webdavSyncInterval: 0, webdavSyncInterval: 0,
webdavMaxBackups: 0, webdavMaxBackups: 0,
webdavSkipBackupFile: false,
translateModelPrompt: TRANSLATE_PROMPT, translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false, autoTranslateWithSpace: false,
showTranslateConfirm: true, showTranslateConfirm: true,
@ -356,6 +361,9 @@ const settingsSlice = createSlice({
setClickAssistantToShowTopic: (state, action: PayloadAction<boolean>) => { setClickAssistantToShowTopic: (state, action: PayloadAction<boolean>) => {
state.clickAssistantToShowTopic = action.payload state.clickAssistantToShowTopic = action.payload
}, },
setSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.skipBackupFile = action.payload
},
setWebdavHost: (state, action: PayloadAction<string>) => { setWebdavHost: (state, action: PayloadAction<string>) => {
state.webdavHost = action.payload state.webdavHost = action.payload
}, },
@ -377,6 +385,9 @@ const settingsSlice = createSlice({
setWebdavMaxBackups: (state, action: PayloadAction<number>) => { setWebdavMaxBackups: (state, action: PayloadAction<number>) => {
state.webdavMaxBackups = action.payload state.webdavMaxBackups = action.payload
}, },
setWebdavSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.webdavSkipBackupFile = action.payload
},
setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => { setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => {
if (action.payload.enabled !== undefined) { if (action.payload.enabled !== undefined) {
state.codeExecution.enabled = action.payload.enabled state.codeExecution.enabled = action.payload.enabled
@ -611,6 +622,7 @@ export const {
setAutoCheckUpdate, setAutoCheckUpdate,
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
setClickAssistantToShowTopic, setClickAssistantToShowTopic,
setSkipBackupFile,
setWebdavHost, setWebdavHost,
setWebdavUser, setWebdavUser,
setWebdavPass, setWebdavPass,
@ -618,6 +630,7 @@ export const {
setWebdavAutoSync, setWebdavAutoSync,
setWebdavSyncInterval, setWebdavSyncInterval,
setWebdavMaxBackups, setWebdavMaxBackups,
setWebdavSkipBackupFile,
setCodeExecution, setCodeExecution,
setCodeEditor, setCodeEditor,
setCodePreview, setCodePreview,

View File

@ -313,6 +313,7 @@ export type WebDavConfig = {
webdavPass: string webdavPass: string
webdavPath: string webdavPath: string
fileName?: string fileName?: string
skipBackupFile?: boolean
} }
export type AppInfo = { export type AppInfo = {