fix: add compatibility for webdav servers that do not support streaming (#7992)

* fix: add compatibility for webdav servers that do not support streaming

* fix: fix grammar error

* fix: fix linter error

* fix: remove unnecessary changes

* revert: restore tolerance for failing to remove temp file after webdav backup failed

* fix: add migration support
This commit is contained in:
happyZYM 2025-07-16 09:53:51 +08:00 committed by GitHub
parent f155b98a92
commit 27c39415c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 87 additions and 16 deletions

View File

@ -321,14 +321,22 @@ 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, undefined, webdavConfig.skipBackupFile)
const contentLength = (await fs.stat(backupedFilePath)).size
const webdavClient = new WebDav(webdavConfig)
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true,
contentLength
})
// 上传成功后删除本地备份文件
let result
if (webdavConfig.disableStream) {
const fileContent = await fs.readFile(backupedFilePath)
result = await webdavClient.putFileContents(filename, fileContent, {
overwrite: true
})
} else {
const contentLength = (await fs.stat(backupedFilePath)).size
result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true,
contentLength
})
}
await fs.remove(backupedFilePath)
return result
} catch (error) {

View File

@ -27,6 +27,7 @@ interface WebdavBackupManagerProps {
webdavUser?: string
webdavPass?: string
webdavPath?: string
webdavDisableStream?: boolean
}
restoreMethod?: (fileName: string) => Promise<void>
}

View File

@ -1676,7 +1676,11 @@
"syncError": "Backup Error",
"syncStatus": "Backup Status",
"title": "WebDAV",
"user": "WebDAV User"
"user": "WebDAV User",
"disableStream": {
"title": "Disable Stream Upload",
"help": "When enabled, loads the file into memory before uploading. This can solve incompatibility issues with some WebDAV servers that do not support chunked uploads, but it will increase memory usage."
}
},
"yuque": {
"check": {

View File

@ -1676,7 +1676,11 @@
"syncError": "バックアップエラー",
"syncStatus": "バックアップ状態",
"title": "WebDAV",
"user": "WebDAVユーザー"
"user": "WebDAVユーザー",
"disableStream": {
"title": "ストリーミングアップロードを無効にする",
"help": "有効にすると、アップロード前にファイルがメモリに読み込まれます。これにより、チャンクアップロードをサポートしていない一部のWebDAVサーバーとの互換性の問題を解決できますが、メモリ使用量が増加します。"
}
},
"yuque": {
"check": {

View File

@ -1676,7 +1676,11 @@
"syncError": "Ошибка резервного копирования",
"syncStatus": "Статус резервного копирования",
"title": "WebDAV",
"user": "Пользователь WebDAV"
"user": "Пользователь WebDAV",
"disableStream": {
"title": "Отключить потоковую загрузку",
"help": "При включении файл загружается в память перед отправкой. Это может решить проблемы совместимости с некоторыми серверами WebDAV, не поддерживающими фрагментированную (chunked) загрузку, но увеличит потребление памяти."
}
},
"yuque": {
"check": {

View File

@ -1676,7 +1676,11 @@
"syncError": "备份错误",
"syncStatus": "备份状态",
"title": "WebDAV",
"user": "WebDAV 用户名"
"user": "WebDAV 用户名",
"disableStream": {
"title": "禁用流式上传",
"help": "开启后将文件加载到内存中再上传可解决部分WebDAV服务不兼容chunked上传的问题但会增加内存占用。"
}
},
"yuque": {
"check": {

View File

@ -1676,7 +1676,11 @@
"syncError": "備份錯誤",
"syncStatus": "備份狀態",
"title": "WebDAV",
"user": "WebDAV 使用者名稱"
"user": "WebDAV 使用者名稱",
"disableStream": {
"title": "禁用串流上傳",
"help": "開啟後,將檔案載入到記憶體中再上傳,可解決部分 WebDAV 服務不相容 chunked 上傳的問題,但會增加記憶體佔用。"
}
},
"yuque": {
"check": {

View File

@ -9,6 +9,7 @@ import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
setWebdavAutoSync,
setWebdavDisableStream as _setWebdavDisableStream,
setWebdavHost as _setWebdavHost,
setWebdavMaxBackups as _setWebdavMaxBackups,
setWebdavPass as _setWebdavPass,
@ -32,7 +33,8 @@ const WebDavSettings: FC = () => {
webdavPath: webDAVPath,
webdavSyncInterval: webDAVSyncInterval,
webdavMaxBackups: webDAVMaxBackups,
webdavSkipBackupFile: webdDAVSkipBackupFile
webdavSkipBackupFile: webdDAVSkipBackupFile,
webdavDisableStream: webDAVDisableStream
} = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
@ -40,6 +42,7 @@ const WebDavSettings: FC = () => {
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [webdavSkipBackupFile, setWebdavSkipBackupFile] = useState<boolean>(webdDAVSkipBackupFile)
const [webdavDisableStream, setWebdavDisableStream] = useState<boolean>(webDAVDisableStream)
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
@ -76,6 +79,11 @@ const WebDavSettings: FC = () => {
dispatch(_setWebdavSkipBackupFile(value))
}
const onDisableStreamChange = (value: boolean) => {
setWebdavDisableStream(value)
dispatch(_setWebdavDisableStream(value))
}
const renderSyncStatus = () => {
if (!webdavHost) return null
@ -220,6 +228,14 @@ const WebDavSettings: FC = () => {
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.disableStream.title')}</SettingRowTitle>
<Switch checked={webdavDisableStream} onChange={onDisableStreamChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.webdav.disableStream.help')}</SettingHelpText>
</SettingRow>
{webdavSync && syncInterval > 0 && (
<>
<SettingDivider />
@ -246,7 +262,8 @@ const WebDavSettings: FC = () => {
webdavHost,
webdavUser,
webdavPass,
webdavPath
webdavPath,
webdavDisableStream
}}
/>
</>

View File

@ -154,8 +154,15 @@ export async function backupToWebdav({
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups, webdavSkipBackupFile } =
store.getState().settings
const {
webdavHost,
webdavUser,
webdavPass,
webdavPath,
webdavMaxBackups,
webdavSkipBackupFile,
webdavDisableStream
} = store.getState().settings
let deviceType = 'unknown'
let hostname = 'unknown'
try {
@ -177,7 +184,8 @@ export async function backupToWebdav({
webdavPass,
webdavPath,
fileName: finalFileName,
skipBackupFile: webdavSkipBackupFile
skipBackupFile: webdavSkipBackupFile,
disableStream: webdavDisableStream
})
if (success) {
store.dispatch(

View File

@ -1799,6 +1799,16 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'122': (state: RootState) => {
try {
if (state.settings && typeof state.settings.webdavDisableStream === 'undefined') {
state.settings.webdavDisableStream = false
}
return state
} catch (error) {
return state
}
}
}

View File

@ -112,6 +112,7 @@ export interface SettingsState {
webdavSyncInterval: number
webdavMaxBackups: number
webdavSkipBackupFile: boolean
webdavDisableStream: boolean
translateModelPrompt: string
autoTranslateWithSpace: boolean
showTranslateConfirm: boolean
@ -273,6 +274,7 @@ export const initialState: SettingsState = {
webdavSyncInterval: 0,
webdavMaxBackups: 0,
webdavSkipBackupFile: false,
webdavDisableStream: false,
translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false,
showTranslateConfirm: true,
@ -501,6 +503,9 @@ const settingsSlice = createSlice({
setWebdavSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.webdavSkipBackupFile = action.payload
},
setWebdavDisableStream: (state, action: PayloadAction<boolean>) => {
state.webdavDisableStream = action.payload
},
setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => {
if (action.payload.enabled !== undefined) {
state.codeExecution.enabled = action.payload.enabled
@ -801,6 +806,7 @@ export const {
setWebdavSyncInterval,
setWebdavMaxBackups,
setWebdavSkipBackupFile,
setWebdavDisableStream,
setCodeExecution,
setCodeEditor,
setCodePreview,

View File

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