From 4d7d4ab74eaba654d4b65232c09b69806029a669 Mon Sep 17 00:00:00 2001 From: zhangyc Date: Mon, 24 Aug 2026 13:29:23 +0800 Subject: [PATCH] feat(web): support WebDAV config sync on web --- .../editor/EditorSettingsDialog.vue | 9 ++- apps/desktop/src/i18n/locales/en.ts | 5 +- apps/desktop/src/i18n/locales/es.ts | 5 +- apps/desktop/src/i18n/locales/it.ts | 5 +- apps/desktop/src/i18n/locales/ja.ts | 5 +- apps/desktop/src/i18n/locales/ko.ts | 5 +- apps/desktop/src/i18n/locales/pt-BR.ts | 5 +- apps/desktop/src/i18n/locales/zh-CN.ts | 5 +- apps/desktop/src/i18n/locales/zh-TW.ts | 5 +- .../backend/__tests__/cloudSyncWebApi.spec.ts | 76 +++++++++++++++++++ .../settings/__tests__/settingsSearch.spec.ts | 16 ++++ .../src/lib/settings/settingsSearch.ts | 16 ++-- docs/content/docs/cloud-sync.cn.mdx | 8 +- docs/content/docs/cloud-sync.mdx | 8 +- 14 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 apps/desktop/src/lib/backend/__tests__/cloudSyncWebApi.spec.ts diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 109d673ef3..0b7329aa4f 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -1575,7 +1575,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[ { value: "tunnels", label: t("settings.tunnelsTab") }, { value: "shortcuts", label: t("settings.shortcutsTab") }, { value: "snippets", label: t("settings.snippetsTab") }, - ...(isWeb ? [] : [{ value: "sync" as const, label: t("settings.syncTab") }]), + { value: "sync", label: t("settings.syncTab") }, { value: "ai", label: t("settings.aiTab") }, { value: "mcp" as const, label: t("settings.mcpTab") }, ...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []), @@ -5748,7 +5748,7 @@ onUnmounted(() => {
- + WebDAV GitHub / Gitee @@ -5762,6 +5762,9 @@ onUnmounted(() => {

{{ t("settings.syncWebDavDescription") }}

+

+ {{ t("settings.syncWebDavWebDescription") }} +

@@ -5974,7 +5977,7 @@ onUnmounted(() => {

- {{ t("settings.syncSecretsSharedDescription") }} + {{ t(isWeb ? "settings.syncSecretsDescription" : "settings.syncSecretsSharedDescription") }}

diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 8b31f52d6a..3f8fa81d46 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6296,6 +6296,7 @@ export default { snippetsEditTitle: "Edit Snippet", syncWebDavTitle: "WebDAV Sync", syncWebDavDescription: "Upload or restore a DBX snapshot, including saved SQL library entries, from a WebDAV-compatible storage service.", + syncWebDavWebDescription: "In Web mode, the DBX server accesses WebDAV without browser CORS restrictions. Saved passwords remain in the current DBX instance.", syncEndpoint: "WebDAV URL", syncUsername: "Username", syncPassword: "Password", @@ -6303,7 +6304,7 @@ export default { syncClearSavedPassword: "Clear saved password", syncRememberWebDavPassword: "Remember WebDAV app password", syncSavedPassword: "(saved)", - syncRememberWebDavPasswordDescription: "The password is encrypted and stored on this device only. It is not synced to WebDAV and does not replace the sync password.", + syncRememberWebDavPasswordDescription: "The password is encrypted and stored in the current DBX instance only. It is not synced to WebDAV and does not replace the sync password.", syncRemotePath: "Remote snapshot path", syncRemotePathDescription: "DBX will create missing parent folders when uploading.", syncSecretNotice: "By default, DBX syncs connection details and settings only. Database passwords, SSH passwords, proxy passwords, HTTP tunnel tokens, connection strings, and AI API keys stay local.", @@ -6311,7 +6312,7 @@ export default { syncSecretsDescription: "When enabled, DBX encrypts database passwords, SSH passwords, proxy passwords, HTTP tunnel tokens, and AI API keys before uploading them to remote storage.", syncSecretsSharedDescription: "For WebDAV, this password encrypts included credentials. GitHub Gist and Gitee snippets use independent encryption and credential controls.", syncSecretsPassphrase: "Sync password", - syncSecretsPassphraseDescription: "This password is only used to encrypt and restore sensitive data. DBX does not save it; enter it again on each device.", + syncSecretsPassphraseDescription: "This password is only used to encrypt and restore sensitive data. It is encrypted in the current DBX instance; enter it again in other instances.", syncTest: "Test", syncUpload: "Upload", syncDownload: "Download", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 7943554296..bb5526f9bd 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -6008,6 +6008,7 @@ export default withEnglishFallback({ snippetsEditTitle: "Editar fragmento", syncWebDavTitle: "Sincronización WebDAV", syncWebDavDescription: "Sube o restaura una instantánea de DBX, incluida la biblioteca SQL guardada, desde almacenamiento compatible con WebDAV.", + syncWebDavWebDescription: "En modo Web, el servidor DBX accede a WebDAV sin las restricciones CORS del navegador. Las contraseñas guardadas permanecen en la instancia DBX actual.", syncEndpoint: "URL WebDAV", syncUsername: "Usuario", syncPassword: "Contraseña", @@ -6015,14 +6016,14 @@ export default withEnglishFallback({ syncClearSavedPassword: "Borrar contraseña guardada", syncRememberWebDavPassword: "Recordar contraseña de app WebDAV", syncSavedPassword: "(guardada)", - syncRememberWebDavPasswordDescription: "La contraseña se cifra y se guarda solo en este dispositivo. No se sincroniza a WebDAV ni reemplaza la contraseña de sync.", + syncRememberWebDavPasswordDescription: "La contraseña se cifra y se guarda solo en la instancia DBX actual. No se sincroniza a WebDAV ni reemplaza la contraseña de sync.", syncRemotePath: "Ruta remota", syncRemotePathDescription: "DBX creará las carpetas padre faltantes al subir.", syncSecretNotice: "De forma predeterminada, DBX solo sincroniza detalles de conexión y ajustes. Las contraseñas de base de datos, SSH, proxy, cadenas de conexión y API keys de IA quedan locales.", syncSecrets: "Sincronizar secretos cifrados", syncSecretsDescription: "Al activarlo, DBX cifra contraseñas de base de datos, contraseñas SSH y API keys de IA antes de subirlas a WebDAV.", syncSecretsPassphrase: "Contraseña de sync", - syncSecretsPassphraseDescription: "Esta contraseña solo se usa para cifrar y restaurar datos sensibles. DBX no la guarda; tendrás que ingresarla en cada dispositivo.", + syncSecretsPassphraseDescription: "Esta contraseña solo se usa para cifrar y restaurar datos sensibles. Se cifra en la instancia DBX actual; vuelve a ingresarla en otras instancias.", syncTest: "Probar", syncUpload: "Subir", syncDownload: "Descargar", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index e0571db076..ed485640f9 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -6008,6 +6008,7 @@ export default withEnglishFallback({ snippetsEditTitle: "Modifica Snippet", syncWebDavTitle: "Sincronizzazione WebDAV", syncWebDavDescription: "Carica o ripristina uno snapshot di DBX, inclusa la libreria SQL salvata, da un servizio di archiviazione compatibile con WebDAV.", + syncWebDavWebDescription: "In modalità Web, il server DBX accede a WebDAV senza le limitazioni CORS del browser. Le password salvate restano nell'istanza DBX corrente.", syncEndpoint: "URL WebDAV", syncUsername: "Nome utente", syncPassword: "Password", @@ -6015,14 +6016,14 @@ export default withEnglishFallback({ syncClearSavedPassword: "Cancella password salvata", syncRememberWebDavPassword: "Ricorda la password dell'app WebDAV", syncSavedPassword: "(salvata)", - syncRememberWebDavPasswordDescription: "La password viene crittografata e memorizzata solo su questo dispositivo. Non viene sincronizzata su WebDAV e non sostituisce la password di sincronizzazione.", + syncRememberWebDavPasswordDescription: "La password viene crittografata e memorizzata solo nell'istanza DBX corrente. Non viene sincronizzata su WebDAV e non sostituisce la password di sincronizzazione.", syncRemotePath: "Percorso snapshot remoto", syncRemotePathDescription: "DBX creerà le cartelle superiori mancanti durante il caricamento.", syncSecretNotice: "Per impostazione predefinita, DBX sincronizza solo i dettagli e le impostazioni di connessione. Le password dei database, le password SSH, le password proxy, le stringhe di connessione e le chiavi API AI rimangono locali.", syncSecrets: "Sincronizza i segreti crittografati", syncSecretsDescription: "Se abilitato, DBX crittografa le password del database, le password SSH e le chiavi API AI prima di caricarle su WebDAV.", syncSecretsPassphrase: "Password di sincronizzazione", - syncSecretsPassphraseDescription: "Questa password viene utilizzata solo per crittografare e ripristinare i dati sensibili. DBX non la salva; inseriscila nuovamente su ciascun dispositivo.", + syncSecretsPassphraseDescription: "Questa password viene utilizzata solo per crittografare e ripristinare i dati sensibili. È crittografata nell'istanza DBX corrente; inseriscila di nuovo nelle altre istanze.", syncTest: "Verifica", syncUpload: "Carica", syncDownload: "Scarica", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index f5398dd4d8..22651830b1 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -6034,6 +6034,7 @@ export default withEnglishFallback({ snippetsEditTitle: "スニペットを編集", syncWebDavTitle: "WebDAV同期", syncWebDavDescription: "保存済みSQLライブラリを含むDBXスナップショットをWebDAV互換ストレージへアップロードまたは復元します。", + syncWebDavWebDescription: "WebモードではDBXサーバーがWebDAVへ接続するため、ブラウザーのCORS制限を受けません。保存したパスワードは現在のDBXインスタンス内に保持されます。", syncEndpoint: "WebDAV URL", syncUsername: "ユーザー名", syncPassword: "パスワード", @@ -6041,14 +6042,14 @@ export default withEnglishFallback({ syncClearSavedPassword: "保存されたパスワードをクリア", syncRememberWebDavPassword: "WebDAVアプリパスワードを記憶", syncSavedPassword: "(保存済み)", - syncRememberWebDavPasswordDescription: "パスワードは暗号化され、このデバイスにのみ保存されます。WebDAVには同期されず、同期パスワードを置き換えるものでもありません。", + syncRememberWebDavPasswordDescription: "パスワードは暗号化され、現在のDBXインスタンスにのみ保存されます。WebDAVには同期されず、同期パスワードを置き換えるものでもありません。", syncRemotePath: "リモートスナップショットパス", syncRemotePathDescription: "アップロード時に不足している親フォルダをDBXが作成します。", syncSecretNotice: "デフォルトでは、DBXは接続詳細と設定のみを同期します。データベースパスワード、SSHパスワード、プロキシパスワード、接続文字列、AI APIキーはローカルに保持されます。", syncSecrets: "暗号化されたシークレットを同期", syncSecretsDescription: "有効時、DBXはデータベースパスワード、SSHパスワード、AI APIキーを暗号化してからWebDAVにアップロードします。", syncSecretsPassphrase: "同期パスワード", - syncSecretsPassphraseDescription: "このパスワードは機密データの暗号化と復元にのみ使用されます。DBXは保存せず、各デバイスで再度入力する必要があります。", + syncSecretsPassphraseDescription: "このパスワードは機密データの暗号化と復元にのみ使用され、現在のDBXインスタンスに暗号化して保存されます。他のインスタンスでは再入力してください。", syncTest: "テスト", syncUpload: "アップロード", syncDownload: "ダウンロード", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index c0a1c304e4..769d509c39 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5687,6 +5687,7 @@ export default withEnglishFallback({ snippetsEditTitle: "스니펫 편집", syncWebDavTitle: "WebDAV 동기화", syncWebDavDescription: "WebDAV 호환 저장 서비스에서 DBX 스냅샷을 업로드하거나 복원합니다(저장된 SQL 라이브러리 항목 포함).", + syncWebDavWebDescription: "Web 모드에서는 DBX 서버가 WebDAV에 연결하므로 브라우저 CORS 제한을 받지 않습니다. 저장된 비밀번호는 현재 DBX 인스턴스에 유지됩니다.", syncEndpoint: "WebDAV URL", syncUsername: "사용자 이름", syncPassword: "비밀번호", @@ -5694,7 +5695,7 @@ export default withEnglishFallback({ syncClearSavedPassword: "저장된 비밀번호 지우기", syncRememberWebDavPassword: "WebDAV 앱 비밀번호 기억", syncSavedPassword: "(저장됨)", - syncRememberWebDavPasswordDescription: "비밀번호는 이 기기에만 암호화되어 저장됩니다. WebDAV에 동기화되지 않으며 동기화 비밀번호를 대체하지 않습니다.", + syncRememberWebDavPasswordDescription: "비밀번호는 현재 DBX 인스턴스에만 암호화되어 저장됩니다. WebDAV에 동기화되지 않으며 동기화 비밀번호를 대체하지 않습니다.", syncRemotePath: "원격 스냅샷 경로", syncRemotePathDescription: "DBX가 업로드 시 누락된 상위 폴더를 만듭니다.", syncSecretNotice: "기본적으로 DBX는 연결 세부 정보와 설정만 동기화합니다. 데이터베이스 비밀번호, SSH 비밀번호, 프록시 비밀번호, HTTP 터널 토큰, 연결 문자열 및 AI API 키는 로컬에 유지됩니다.", @@ -5702,7 +5703,7 @@ export default withEnglishFallback({ syncSecretsDescription: "활성화하면 DBX가 데이터베이스 비밀번호, SSH 비밀번호, 프록시 비밀번호, HTTP 터널 토큰 및 AI API 키를 암호화하여 원격 저장소에 업로드합니다.", syncSecretsSharedDescription: "WebDAV에서는 이 비밀번호로 포함할 자격 증명을 암호화합니다. GitHub Gist 및 Gitee 스니펫은 별도의 암호화 및 자격 증명 설정을 사용합니다.", syncSecretsPassphrase: "동기화 비밀번호", - syncSecretsPassphraseDescription: "이 비밀번호는 민감한 데이터를 암호화하고 복원하는 데만 사용됩니다. DBX가 저장하지 않으므로 각 기기에서 다시 입력하세요.", + syncSecretsPassphraseDescription: "이 비밀번호는 민감한 데이터를 암호화하고 복원하는 데만 사용되며 현재 DBX 인스턴스에 암호화되어 저장됩니다. 다른 인스턴스에서는 다시 입력하세요.", syncTest: "테스트", syncUpload: "업로드", syncDownload: "다운로드", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index bd458d8881..6d2a3787bb 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -6010,6 +6010,7 @@ export default withEnglishFallback({ snippetsEditTitle: "Editar snippet", syncWebDavTitle: "Sincronização WebDAV", syncWebDavDescription: "Envie ou restaure um snapshot do DBX, incluindo a biblioteca SQL salva, a partir de um serviço de armazenamento compatível com WebDAV.", + syncWebDavWebDescription: "No modo Web, o servidor DBX acessa o WebDAV sem as restrições de CORS do navegador. As senhas salvas permanecem na instância DBX atual.", syncEndpoint: "URL do WebDAV", syncUsername: "Nome de usuário", syncPassword: "Senha", @@ -6017,14 +6018,14 @@ export default withEnglishFallback({ syncClearSavedPassword: "Limpar senha salva", syncRememberWebDavPassword: "Lembrar senha do app WebDAV", syncSavedPassword: "(salva)", - syncRememberWebDavPasswordDescription: "A senha é criptografada e armazenada apenas neste dispositivo. Ela não é sincronizada para o WebDAV e não substitui a senha de sincronização.", + syncRememberWebDavPasswordDescription: "A senha é criptografada e armazenada apenas na instância DBX atual. Ela não é sincronizada para o WebDAV e não substitui a senha de sincronização.", syncRemotePath: "Caminho remoto do snapshot", syncRemotePathDescription: "O DBX criará as pastas pai ausentes ao enviar.", syncSecretNotice: "Por padrão, o DBX sincroniza apenas detalhes de conexão e configurações. Senhas de banco de dados, senhas SSH, senhas de proxy, strings de conexão e chaves de API de IA permanecem locais.", syncSecrets: "Sincronizar segredos criptografados", syncSecretsDescription: "Quando ativado, o DBX criptografa senhas de banco de dados, senhas SSH e chaves de API de IA antes de enviá-las para o WebDAV.", syncSecretsPassphrase: "Senha de sincronização", - syncSecretsPassphraseDescription: "Esta senha é usada apenas para criptografar e restaurar dados sensíveis. O DBX não a salva; digite-a novamente em cada dispositivo.", + syncSecretsPassphraseDescription: "Esta senha é usada apenas para criptografar e restaurar dados sensíveis. Ela é criptografada na instância DBX atual; digite-a novamente em outras instâncias.", syncTest: "Testar", syncUpload: "Enviar", syncDownload: "Baixar", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 50b5109cc9..fb80af0b86 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6279,6 +6279,7 @@ export default withEnglishFallback({ snippetsEditTitle: "编辑片段", syncWebDavTitle: "WebDAV 同步", syncWebDavDescription: "将包含 SQL 库的 DBX 快照上传到兼容 WebDAV 的存储服务,或从远端恢复。", + syncWebDavWebDescription: "Web 模式下由 DBX 服务端访问 WebDAV,不受浏览器 CORS 限制;保存的密码会留在当前 DBX 实例中。", syncEndpoint: "WebDAV 地址", syncUsername: "用户名", syncPassword: "密码", @@ -6286,7 +6287,7 @@ export default withEnglishFallback({ syncClearSavedPassword: "清除已保存的密码", syncRememberWebDavPassword: "记住 WebDAV 应用密码", syncSavedPassword: "(已保存)", - syncRememberWebDavPasswordDescription: "密码会加密保存在本机,不会同步到 WebDAV,也不会替代同步密码。", + syncRememberWebDavPasswordDescription: "密码会加密保存在当前 DBX 实例中,不会同步到 WebDAV,也不会替代同步密码。", syncRemotePath: "远端快照路径", syncRemotePathDescription: "上传时 DBX 会自动创建缺失的父目录。", syncSecretNotice: "默认只同步连接信息和设置,不会同步数据库密码、SSH 密码、代理密码、HTTP 隧道 Token、连接串或 AI API Key。", @@ -6294,7 +6295,7 @@ export default withEnglishFallback({ syncSecretsDescription: "开启后,DBX 会先加密数据库密码、SSH 密码、代理密码、HTTP 隧道 Token 和 AI API Key,再上传到远端存储。", syncSecretsSharedDescription: "WebDAV 使用此密码加密包含的敏感信息。GitHub Gist 和 Gitee 代码片段使用独立的加密和敏感信息选项。", syncSecretsPassphrase: "同步密码", - syncSecretsPassphraseDescription: "这个密码只用于加密和恢复敏感信息,DBX 不会保存;换设备恢复时需要再次输入。", + syncSecretsPassphraseDescription: "这个密码只用于加密和恢复敏感信息,并会加密保存在当前 DBX 实例中;在其他实例恢复时需要再次输入。", syncTest: "测试", syncUpload: "上传", syncDownload: "下载", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 343e93fc09..0fc0813d5f 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5326,6 +5326,7 @@ export default withEnglishFallback({ snippetsEditTitle: "編輯片段", syncWebDavTitle: "WebDAV 同步", syncWebDavDescription: "上傳包含 SQL 庫的 DBX 快照到相容 WebDAV 的儲存服務,或從該服務還原快照。", + syncWebDavWebDescription: "Web 模式由 DBX 伺服器端存取 WebDAV,不受瀏覽器 CORS 限制;儲存的密碼會保留在目前 DBX 執行個體中。", syncEndpoint: "WebDAV 地址", syncUsername: "使用者名稱", syncPassword: "密碼", @@ -5333,14 +5334,14 @@ export default withEnglishFallback({ syncClearSavedPassword: "清除已儲存的密碼", syncRememberWebDavPassword: "記住 WebDAV 應用密碼", syncSavedPassword: "(已儲存)", - syncRememberWebDavPasswordDescription: "密碼會加密儲存在本機,不會同步到 WebDAV,也不會取代同步密碼。", + syncRememberWebDavPasswordDescription: "密碼會加密儲存在目前 DBX 執行個體中,不會同步到 WebDAV,也不會取代同步密碼。", syncRemotePath: "遠端快照路徑", syncRemotePathDescription: "上傳時 DBX 會自動建立缺失的父目錄。", syncSecretNotice: "預設只同步連線詳細資料和設定。資料庫密碼、SSH 密碼、代理伺服器密碼、連線字串和 AI API Key 會保留在本機。", syncSecrets: "同步加密後的敏感資訊", syncSecretsDescription: "開啟後,DBX 會先加密資料庫密碼、SSH 密碼和 AI API Key,再上傳到 WebDAV。", syncSecretsPassphrase: "同步密碼", - syncSecretsPassphraseDescription: "此密碼只用於加密和還原敏感資料。DBX 不會儲存;每台裝置都需要再次輸入。", + syncSecretsPassphraseDescription: "此密碼只用於加密和還原敏感資料,並會加密儲存在目前 DBX 執行個體中;在其他執行個體還原時需要再次輸入。", syncTest: "測試", syncUpload: "上傳", syncDownload: "下載", diff --git a/apps/desktop/src/lib/backend/__tests__/cloudSyncWebApi.spec.ts b/apps/desktop/src/lib/backend/__tests__/cloudSyncWebApi.spec.ts new file mode 100644 index 0000000000..c686b56c23 --- /dev/null +++ b/apps/desktop/src/lib/backend/__tests__/cloudSyncWebApi.spec.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("WebDAV sync HTTP API", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + function stubFetch(response: unknown = undefined) { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(response), + }); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; + } + + function lastCall(fetchMock: ReturnType): { url: string; body: Record } { + const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit]; + return { url, body: JSON.parse(String(init.body)) as Record }; + } + + const config = { + endpoint: "https://dav.example.com/remote.php/dav/files/alice/", + username: "alice", + password: "app-password", + remotePath: "DBX/sync/snapshot.json", + }; + + it("routes WebDAV connectivity and saved-password operations through the Web backend", async () => { + const fetchMock = stubFetch({ hasSavedPassword: true }); + const { forgetWebdavSavedPassword, saveWebdavSavedPassword, webdavPasswordStatus, webdavSyncTest } = await import("@/lib/backend/http"); + + await webdavSyncTest(config); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/test", body: { config } }); + + await expect(webdavPasswordStatus(config)).resolves.toEqual({ hasSavedPassword: true }); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/password-status", body: { config } }); + + await saveWebdavSavedPassword(config, "saved-password"); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/save-password", body: { config, password: "saved-password" } }); + + await forgetWebdavSavedPassword(config); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/forget-password", body: { config } }); + }); + + it("routes WebDAV secret preferences and snapshot transfers through the Web backend", async () => { + const fetchMock = stubFetch({ bytes: 42, remotePath: config.remotePath }); + const { forgetWebdavSyncSecretsPassphrase, saveWebdavSyncSecretsPreference, webdavSyncDownload, webdavSyncSecretsStatus, webdavSyncUpload } = await import("@/lib/backend/http"); + + await webdavSyncSecretsStatus(); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/sync-secrets-status", body: {} }); + + await saveWebdavSyncSecretsPreference(true, "sync-passphrase"); + expect(lastCall(fetchMock)).toEqual({ + url: "/api/cloud-sync/webdav/save-sync-secrets-preference", + body: { enabled: true, passphrase: "sync-passphrase" }, + }); + + await forgetWebdavSyncSecretsPassphrase(); + expect(lastCall(fetchMock)).toEqual({ url: "/api/cloud-sync/webdav/forget-sync-secrets-passphrase", body: {} }); + + const editorSettings = { theme: "dark" }; + await webdavSyncUpload(config, editorSettings, "sync-passphrase"); + expect(lastCall(fetchMock)).toEqual({ + url: "/api/cloud-sync/webdav/upload", + body: { config, editorSettings, secretsPassphrase: "sync-passphrase" }, + }); + + await webdavSyncDownload(config, "sync-passphrase"); + expect(lastCall(fetchMock)).toEqual({ + url: "/api/cloud-sync/webdav/download", + body: { config, secretsPassphrase: "sync-passphrase" }, + }); + }); +}); diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 4e46384cd9..7839ad5f5c 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -55,6 +55,22 @@ describe("settings search", () => { expect(searchSettings(webEntries, "desktop", "en")).toEqual([]); }); + it("exposes WebDAV sync in Web settings without exposing snippet sync", () => { + const webEntries = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { isWeb: true, visibleCategories: new Set(["sync"]) }, translate, categoryLabels); + + expect(webEntries.map((entry) => entry.id)).toEqual(["sync-webdav", "sync-webdav-endpoint", "sync-webdav-username", "sync-webdav-password", "sync-webdav-remote-path", "sync-webdav-auto-upload", "sync-secrets", "sync-secrets-passphrase"]); + expect(settingsDialogSource).toContain('{ value: "sync", label: t("settings.syncTab") }'); + expect(settingsDialogSource).not.toContain('...(isWeb ? [] : [{ value: "sync"'); + expect(settingsDialogSource).toContain(' { + for (const locale of ["zh-CN", "zh-TW", "en", "es", "it", "ja", "ko", "pt-BR"]) { + const source = readFileSync(new URL(`../../../i18n/locales/${locale}.ts`, import.meta.url), "utf8"); + expect(source, locale).toContain("syncWebDavWebDescription:"); + } + }); + it("matches Chinese text as a Unicode substring", () => { expect(searchSettings([{ id: "font", category: "editor", title: "界面字体", description: "选择应用字体", categoryLabel: "编辑器", targetId: "editor" }], "字体", "zh-CN").map((entry) => entry.id)).toEqual(["font"]); }); diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index f88414a27f..a8b6dcd2ce 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -211,18 +211,18 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "tunnels", category: "tunnels", titleKey: "settings.tunnelsTab", targetId: "tunnels" }, { id: "shortcuts", category: "shortcuts", titleKey: "settings.shortcutsTab", targetId: "shortcuts" }, { id: "snippets", category: "snippets", titleKey: "settings.snippetsTab", descriptionKey: "settings.snippetsDescription", targetId: "snippets" }, - { id: "sync-webdav", category: "sync", titleKey: "settings.syncWebDavTitle", descriptionKey: "settings.syncWebDavDescription", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, - { id: "sync-webdav-endpoint", category: "sync", titleKey: "settings.syncEndpoint", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, - { id: "sync-webdav-username", category: "sync", titleKey: "settings.syncUsername", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, - { id: "sync-webdav-password", category: "sync", titleKey: "settings.syncPassword", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, - { id: "sync-webdav-remote-path", category: "sync", titleKey: "settings.syncRemotePath", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, - { id: "sync-webdav-auto-upload", category: "sync", titleKey: "settings.syncAutoUploadInterval", targetId: "sync-webdav", route: { syncMethodTab: "webdav" }, visible: desktopOnly }, + { id: "sync-webdav", category: "sync", titleKey: "settings.syncWebDavTitle", descriptionKey: "settings.syncWebDavDescription", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, + { id: "sync-webdav-endpoint", category: "sync", titleKey: "settings.syncEndpoint", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, + { id: "sync-webdav-username", category: "sync", titleKey: "settings.syncUsername", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, + { id: "sync-webdav-password", category: "sync", titleKey: "settings.syncPassword", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, + { id: "sync-webdav-remote-path", category: "sync", titleKey: "settings.syncRemotePath", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, + { id: "sync-webdav-auto-upload", category: "sync", titleKey: "settings.syncAutoUploadInterval", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } }, { id: "sync-snippet", category: "sync", titleKey: "settings.syncSnippetTitle", descriptionKey: "settings.syncSnippetDescription", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly }, { id: "sync-snippet-provider", category: "sync", titleKey: "settings.syncSnippetProvider", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly }, { id: "sync-snippet-id", category: "sync", titleKey: "settings.syncSnippetId", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly }, { id: "sync-snippet-token", category: "sync", titleKey: "settings.syncSnippetToken", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly }, - { id: "sync-secrets", category: "sync", titleKey: "settings.syncSecrets", targetId: "sync", visible: desktopOnly }, - { id: "sync-secrets-passphrase", category: "sync", titleKey: "settings.syncSecretsPassphrase", targetId: "sync", visible: desktopOnly }, + { id: "sync-secrets", category: "sync", titleKey: "settings.syncSecrets", targetId: "sync" }, + { id: "sync-secrets-passphrase", category: "sync", titleKey: "settings.syncSecretsPassphrase", targetId: "sync" }, { id: "ai-config", category: "ai", titleKey: "ai.configList", targetId: "ai" }, { id: "ai-prompts", category: "ai", titleKey: "ai.promptTemplates", descriptionKey: "ai.promptTemplatesDescription", targetId: "ai" }, { id: "ai-default-mode", category: "ai", titleKey: "ai.defaultAiMode", descriptionKey: "ai.defaultAiModeDescription", targetId: "ai" }, diff --git a/docs/content/docs/cloud-sync.cn.mdx b/docs/content/docs/cloud-sync.cn.mdx index 105c4cf0ad..3d40aa1db7 100644 --- a/docs/content/docs/cloud-sync.cn.mdx +++ b/docs/content/docs/cloud-sync.cn.mdx @@ -4,7 +4,7 @@ description: 使用 WebDAV、GitHub Gist 或 Gitee 代码片段同步 DBX 连接 --- -DBX 可以把连接配置、界面设置和保存的 SQL 组成一个快照,上传到 WebDAV 或私有代码片段,并在另一台设备恢复。 +DBX 可以把连接配置、界面设置和保存的 SQL 组成一个快照。桌面端可通过 WebDAV 或私有代码片段同步,Web/Docker 可通过 WebDAV 同步,并在另一个 DBX 实例恢复。 云同步备份的是 DBX 配置,不会读取或备份连接中的业务数据库。表结构和数据请使用 [数据库导出](/cn/docs/database-export);Desktop 定时 MySQL/PostgreSQL 备份请使用 [数据库备份](/cn/docs/database-backup)。 @@ -12,7 +12,7 @@ DBX 可以把连接配置、界面设置和保存的 SQL 组成一个快照, 在 **设置 → 同步** 中选择同步方式: - **WebDAV**:适合已有 NAS、Nextcloud、坚果云或其他 WebDAV 服务的用户,支持定时自动上传。 -- **GitHub / Gitee 代码片段**:不需要自己维护服务器,通过私有 Gist 或私有代码片段保存快照。目前需要手动上传和下载。 +- **GitHub / Gitee 代码片段**:不需要自己维护服务器,通过私有 Gist 或私有代码片段保存快照。目前仅桌面端显示,且需要手动上传和下载。 ## 同步哪些内容 @@ -109,7 +109,9 @@ GitHub Gist 和 Gitee 代码片段始终使用**代码片段加密密码**加密 填写 WebDAV 服务地址、用户名、应用密码和远端快照路径。默认路径为 `DBX/sync/snapshot.json`。 -WebDAV 支持定时自动上传。自动上传只在 DBX 运行期间执行;下载始终需要手动确认,避免远端快照意外覆盖本地数据。 +Web/Docker 模式下,WebDAV 请求由 DBX 服务端发起,因此不受浏览器 CORS 限制;WebDAV 应用密码会加密保存在当前 DBX 实例中,不会写入同步快照。 + +WebDAV 支持定时自动上传。自动上传只在对应 DBX 客户端保持打开时执行;Web/Docker 使用时需保持至少一个 DBX 页面打开。下载始终需要手动确认,避免远端快照意外覆盖当前实例的数据。 ## 常见问题 diff --git a/docs/content/docs/cloud-sync.mdx b/docs/content/docs/cloud-sync.mdx index 678def8607..b81abf3171 100644 --- a/docs/content/docs/cloud-sync.mdx +++ b/docs/content/docs/cloud-sync.mdx @@ -4,7 +4,7 @@ description: Sync DBX connections, settings, and saved SQL with WebDAV, GitHub G --- -DBX can package connections, application settings, and saved SQL into a snapshot, upload it to WebDAV or a private code snippet, and restore it on another device. +DBX can package connections, application settings, and saved SQL into a snapshot. Desktop can sync through WebDAV or private snippets, while Web/Docker can sync through WebDAV and restore the snapshot in another DBX instance. Cloud Sync backs up DBX configuration. It does not read or back up the business databases behind your connections. Use [Database Export](/en/docs/database-export) for schema and data, or [Database Backup](/en/docs/database-backup) for scheduled Desktop MySQL/PostgreSQL backups. @@ -12,7 +12,7 @@ DBX can package connections, application settings, and saved SQL into a snapshot Choose a provider under **Settings → Sync**: - **WebDAV**: Best when you already use a NAS, Nextcloud, or another WebDAV service. Supports scheduled uploads. -- **GitHub / Gitee snippets**: Stores the snapshot in a private Gist or code snippet without requiring your own server. Uploads and downloads are currently manual. +- **GitHub / Gitee snippets**: Stores the snapshot in a private Gist or code snippet without requiring your own server. It is currently exposed in Desktop only, and uploads and downloads are manual. ## What Is Synchronized @@ -109,7 +109,9 @@ Neither password can be recovered. An encrypted GitHub/Gitee snippet cannot be r Enter the WebDAV endpoint, username, application password, and remote snapshot path. The default path is `DBX/sync/snapshot.json`. -WebDAV supports scheduled uploads while DBX is running. Downloads always require confirmation to prevent accidental replacement of local data. +In Web/Docker mode, the DBX server sends WebDAV requests, so browser CORS restrictions do not apply. WebDAV application passwords are encrypted in the current DBX instance and are never included in sync snapshots. + +WebDAV supports scheduled uploads while the corresponding DBX client remains open. Keep at least one DBX page open when using Web/Docker. Downloads always require confirmation to prevent accidental replacement of data in the current instance. ## Troubleshooting