feat(sync): support webdav sync on web

This commit is contained in:
t8y2
2026-06-19 13:22:19 +08:00
parent dad548e41b
commit 3cbead1c20
12 changed files with 249 additions and 20 deletions
@@ -70,12 +70,14 @@ import AppLogo from "@/components/icons/AppLogo.vue";
import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue";
import type { AppThemeAppearance } from "@/lib/appTheme";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { currentLocale, setLocale, type Locale } from "@/i18n";
import { LOCALE_OPTIONS } from "@/lib/localeOptions";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const connectionStore = useConnectionStore();
const savedSqlStore = useSavedSqlStore();
const { isDark, themeMode, setThemeMode } = useTheme();
let cachedSystemFonts: string[] | null = null;
@@ -882,9 +884,12 @@ const webdavHasSavedPassword = ref(false);
const webdavRemotePath = ref(localStorage.getItem("dbx-webdav-remote-path") || "DBX/sync/snapshot.json");
const webdavSyncSecrets = ref(false);
const webdavSecretsPassphrase = ref("");
const webdavAutoUploadEnabled = ref(localStorage.getItem("dbx-webdav-auto-upload-enabled") === "true");
const webdavAutoUploadIntervalMinutes = ref(Number(localStorage.getItem("dbx-webdav-auto-upload-interval-minutes") || "30"));
const webdavBusy = ref<"" | "test" | "upload" | "download">("");
const webdavMessage = ref("");
const webdavError = ref(false);
let webdavAutoUploadTimer: ReturnType<typeof window.setInterval> | undefined;
const webdavReady = computed(() => !!webdavEndpoint.value.trim() && !webdavBusy.value && (!webdavSyncSecrets.value || !!webdavSecretsPassphrase.value.trim()));
@@ -906,6 +911,8 @@ function rememberWebDavFields() {
localStorage.setItem("dbx-webdav-endpoint", webdavEndpoint.value.trim());
localStorage.setItem("dbx-webdav-username", webdavUsername.value.trim());
localStorage.setItem("dbx-webdav-remote-path", webdavRemotePath.value.trim() || "DBX/sync/snapshot.json");
localStorage.setItem("dbx-webdav-auto-upload-enabled", String(webdavAutoUploadEnabled.value));
localStorage.setItem("dbx-webdav-auto-upload-interval-minutes", String(normalizedWebDavAutoUploadInterval()));
}
function setWebDavResult(message: string, error = false) {
@@ -970,6 +977,43 @@ async function uploadWebDavSnapshot() {
});
}
function normalizedWebDavAutoUploadInterval() {
const value = Number(webdavAutoUploadIntervalMinutes.value);
if (!Number.isFinite(value)) return 30;
return Math.max(1, Math.min(1440, Math.round(value)));
}
function scheduleWebDavAutoUpload() {
if (webdavAutoUploadTimer) {
window.clearInterval(webdavAutoUploadTimer);
webdavAutoUploadTimer = undefined;
}
if (!webdavAutoUploadEnabled.value) return;
const intervalMinutes = normalizedWebDavAutoUploadInterval();
webdavAutoUploadIntervalMinutes.value = intervalMinutes;
webdavAutoUploadTimer = window.setInterval(() => {
void runWebDavAutoUpload();
}, intervalMinutes * 60_000);
}
async function runWebDavAutoUpload() {
if (!webdavEndpoint.value.trim() || webdavBusy.value) return;
webdavBusy.value = "upload";
webdavMessage.value = "";
webdavError.value = false;
try {
rememberWebDavFields();
await applyWebDavPasswordPreference();
const summary = await webdavSyncUpload(currentWebDavConfig(), settingsStore.editorSettings, webdavSyncSecrets.value ? webdavSecretsPassphrase.value : undefined);
setWebDavResult(t("settings.syncAutoUploadSuccess", { bytes: summary.bytes, path: summary.remotePath }));
} catch (e: any) {
setWebDavResult(e?.message || String(e), true);
} finally {
webdavBusy.value = "";
}
}
async function downloadWebDavSnapshot() {
if (!window.confirm(t("settings.syncDownloadConfirm"))) return;
await runWebDavAction("download", async () => {
@@ -979,6 +1023,7 @@ async function downloadWebDavSnapshot() {
}
await settingsStore.updateDesktopSettings(result.desktopSettings);
await connectionStore.initFromDisk();
await savedSqlStore.initFromStorage();
const message = t("settings.syncDownloadSuccess", {
bytes: result.summary.bytes,
path: result.summary.remotePath,
@@ -1031,6 +1076,10 @@ watch([webdavEndpoint, webdavUsername], () => {
watch(webdavRememberPassword, (val) => {
localStorage.setItem("dbx-webdav-remember-password", String(val));
});
watch([webdavAutoUploadEnabled, webdavAutoUploadIntervalMinutes], () => {
rememberWebDavFields();
scheduleWebDavAutoUpload();
});
watch(activeSettingsTab, (tab) => {
if (tab === "mcp" && !mcpStatus.value && !mcpStatusLoading.value) void refreshMcpStatus();
@@ -1043,12 +1092,17 @@ watch(activeSettingsTab, (tab) => {
onMounted(() => {
void refreshWebDavPasswordStatus();
scheduleWebDavAutoUpload();
checkLayoutDescTruncation();
checkIconThemeDescTruncation();
initTruncationObservers();
});
onUnmounted(() => {
if (webdavAutoUploadTimer) {
window.clearInterval(webdavAutoUploadTimer);
webdavAutoUploadTimer = undefined;
}
cleanupTruncationObservers();
});
@@ -2367,6 +2421,18 @@ watch(
<Input id="webdav-remote-path" v-model="webdavRemotePath" autocomplete="off" />
<p class="text-xs text-muted-foreground">{{ t("settings.syncRemotePathDescription") }}</p>
</div>
<div class="space-y-2 md:col-span-2 rounded-md border bg-muted/20 px-3 py-3">
<label class="flex items-center gap-2 text-xs">
<input v-model="webdavAutoUploadEnabled" type="checkbox" class="h-4 w-4 shrink-0 accent-primary" />
<span class="font-medium">{{ t("settings.syncAutoUpload") }}</span>
</label>
<div class="flex items-center gap-2">
<Label for="webdav-auto-upload-interval" class="text-xs text-muted-foreground">{{ t("settings.syncAutoUploadInterval") }}</Label>
<Input id="webdav-auto-upload-interval" v-model.number="webdavAutoUploadIntervalMinutes" type="number" min="1" max="1440" step="1" class="h-7 w-24 text-xs" :disabled="!webdavAutoUploadEnabled" />
<span class="text-xs text-muted-foreground">{{ t("settings.syncAutoUploadMinutes") }}</span>
</div>
<p class="text-xs text-muted-foreground">{{ t("settings.syncAutoUploadDescription") }}</p>
</div>
</div>
<div class="rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
+6 -1
View File
@@ -2273,7 +2273,7 @@ export default {
snippetsAddTitle: "Add Snippet",
snippetsEditTitle: "Edit Snippet",
syncWebDavTitle: "WebDAV Sync",
syncWebDavDescription: "Upload or restore a DBX snapshot from a WebDAV-compatible storage service.",
syncWebDavDescription: "Upload or restore a DBX snapshot, including saved SQL library entries, from a WebDAV-compatible storage service.",
syncEndpoint: "WebDAV URL",
syncUsername: "Username",
syncPassword: "Password",
@@ -2294,6 +2294,11 @@ export default {
syncDownload: "Download",
syncTestSuccess: "WebDAV target is reachable.",
syncUploadSuccess: "Uploaded {bytes} bytes to {path}.",
syncAutoUpload: "Automatically upload snapshots",
syncAutoUploadInterval: "Every",
syncAutoUploadMinutes: "minutes",
syncAutoUploadDescription: "Runs while DBX is open. Downloads still require manual confirmation to avoid replacing local saved SQL unexpectedly.",
syncAutoUploadSuccess: "Auto-uploaded {bytes} bytes to {path}.",
syncDownloadSuccess: "Downloaded and applied {bytes} bytes from {path}.",
syncSecretsApplied: "Encrypted secrets were restored.",
syncSecretsSkipped: "Encrypted secrets were present but were not restored.",
+6 -1
View File
@@ -1949,7 +1949,7 @@ export default {
snippetsAddTitle: "Agregar fragmento",
snippetsEditTitle: "Editar fragmento",
syncWebDavTitle: "Sincronización WebDAV",
syncWebDavDescription: "Sube o restaura una instantánea de DBX desde almacenamiento compatible con WebDAV.",
syncWebDavDescription: "Sube o restaura una instantánea de DBX, incluida la biblioteca SQL guardada, desde almacenamiento compatible con WebDAV.",
syncEndpoint: "URL WebDAV",
syncUsername: "Usuario",
syncPassword: "Contraseña",
@@ -1970,6 +1970,11 @@ export default {
syncDownload: "Descargar",
syncTestSuccess: "El destino WebDAV está disponible.",
syncUploadSuccess: "Se subieron {bytes} bytes a {path}.",
syncAutoUpload: "Subir instantáneas automáticamente",
syncAutoUploadInterval: "Cada",
syncAutoUploadMinutes: "minutos",
syncAutoUploadDescription: "Se ejecuta mientras DBX está abierto. Las descargas aún requieren confirmación manual para evitar reemplazar SQL guardado localmente.",
syncAutoUploadSuccess: "Se subieron automáticamente {bytes} bytes a {path}.",
syncDownloadSuccess: "Se descargaron y aplicaron {bytes} bytes desde {path}.",
syncSecretsApplied: "Los secretos cifrados fueron restaurados.",
syncSecretsSkipped: "Había secretos cifrados, pero no fueron restaurados.",
+6 -1
View File
@@ -2022,7 +2022,7 @@ export default {
snippetsAddTitle: "Aggiungi Snippet",
snippetsEditTitle: "Modifica Snippet",
syncWebDavTitle: "Sincronizzazione WebDAV",
syncWebDavDescription: "Carica o ripristina uno snapshot di DBX da un servizio di archiviazione compatibile con WebDAV.",
syncWebDavDescription: "Carica o ripristina uno snapshot di DBX, inclusa la libreria SQL salvata, da un servizio di archiviazione compatibile con WebDAV.",
syncEndpoint: "URL WebDAV",
syncUsername: "Nome utente",
syncPassword: "Password",
@@ -2043,6 +2043,11 @@ export default {
syncDownload: "Scarica",
syncTestSuccess: "La destinazione WebDAV è raggiungibile.",
syncUploadSuccess: "Caricati {bytes} byte su {path}.",
syncAutoUpload: "Carica automaticamente gli snapshot",
syncAutoUploadInterval: "Ogni",
syncAutoUploadMinutes: "minuti",
syncAutoUploadDescription: "Viene eseguito mentre DBX è aperto. I download richiedono ancora conferma manuale per evitare di sostituire SQL salvati localmente.",
syncAutoUploadSuccess: "Caricati automaticamente {bytes} byte su {path}.",
syncDownloadSuccess: "Scaricati e applicati {bytes} byte da {path}.",
syncSecretsApplied: "I segreti crittografati sono stati ripristinati.",
syncSecretsSkipped: "I segreti crittografati erano presenti ma non sono stati ripristinati.",
+6 -1
View File
@@ -2241,7 +2241,7 @@ export default {
snippetsAddTitle: "スニペットを追加",
snippetsEditTitle: "スニペットを編集",
syncWebDavTitle: "WebDAV同期",
syncWebDavDescription: "WebDAV互換ストレージサービスからDBXスナップショットをアップロードまたは復元します。",
syncWebDavDescription: "保存済みSQLライブラリを含むDBXスナップショットをWebDAV互換ストレージへアップロードまたは復元します。",
syncEndpoint: "WebDAV URL",
syncUsername: "ユーザー名",
syncPassword: "パスワード",
@@ -2262,6 +2262,11 @@ export default {
syncDownload: "ダウンロード",
syncTestSuccess: "WebDAVターゲットに到達可能です。",
syncUploadSuccess: "{bytes}バイトを{path}にアップロードしました。",
syncAutoUpload: "スナップショットを自動アップロード",
syncAutoUploadInterval: "間隔",
syncAutoUploadMinutes: "分",
syncAutoUploadDescription: "DBXを開いている間だけ実行されます。ローカルの保存済みSQLを誤って置き換えないよう、ダウンロードは手動確認が必要です。",
syncAutoUploadSuccess: "{bytes}バイトを{path}に自動アップロードしました。",
syncDownloadSuccess: "{path}から{bytes}バイトをダウンロードして適用しました。",
syncSecretsApplied: "暗号化されたシークレットが復元されました。",
syncSecretsSkipped: "暗号化されたシークレットが存在しましたが復元されませんでした。",
+6 -1
View File
@@ -2033,7 +2033,7 @@ export default {
snippetsAddTitle: "Adicionar snippet",
snippetsEditTitle: "Editar snippet",
syncWebDavTitle: "Sincronização WebDAV",
syncWebDavDescription: "Envie ou restaure um snapshot do DBX a partir de um serviço de armazenamento compatível com 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.",
syncEndpoint: "URL do WebDAV",
syncUsername: "Nome de usuário",
syncPassword: "Senha",
@@ -2054,6 +2054,11 @@ export default {
syncDownload: "Baixar",
syncTestSuccess: "O destino WebDAV está acessível.",
syncUploadSuccess: "Enviados {bytes} bytes para {path}.",
syncAutoUpload: "Enviar snapshots automaticamente",
syncAutoUploadInterval: "A cada",
syncAutoUploadMinutes: "minutos",
syncAutoUploadDescription: "Executa enquanto o DBX está aberto. Downloads ainda exigem confirmação manual para evitar substituir SQL salvos localmente.",
syncAutoUploadSuccess: "Enviados automaticamente {bytes} bytes para {path}.",
syncDownloadSuccess: "Baixados e aplicados {bytes} bytes de {path}.",
syncSecretsApplied: "Os segredos criptografados foram restaurados.",
syncSecretsSkipped: "Havia segredos criptografados, mas não foram restaurados.",
+6 -1
View File
@@ -2297,7 +2297,7 @@ export default {
snippetsAddTitle: "添加片段",
snippetsEditTitle: "编辑片段",
syncWebDavTitle: "WebDAV 同步",
syncWebDavDescription: "将 DBX 快照上传到兼容 WebDAV 的存储服务,或从远端恢复。",
syncWebDavDescription: "将包含 SQL 库的 DBX 快照上传到兼容 WebDAV 的存储服务,或从远端恢复。",
syncEndpoint: "WebDAV 地址",
syncUsername: "用户名",
syncPassword: "密码",
@@ -2318,6 +2318,11 @@ export default {
syncDownload: "下载",
syncTestSuccess: "WebDAV 目标可访问。",
syncUploadSuccess: "已上传 {bytes} 字节到 {path}。",
syncAutoUpload: "自动上传快照",
syncAutoUploadInterval: "每",
syncAutoUploadMinutes: "分钟",
syncAutoUploadDescription: "仅在 DBX 打开时运行。下载仍需手动确认,避免意外覆盖本地保存的 SQL。",
syncAutoUploadSuccess: "已自动上传 {bytes} 字节到 {path}。",
syncDownloadSuccess: "已从 {path} 下载并应用 {bytes} 字节。",
syncSecretsApplied: "已恢复加密敏感信息。",
syncSecretsSkipped: "远端包含加密敏感信息,但本次未恢复。",
+6 -1
View File
@@ -2069,7 +2069,7 @@ export default {
snippetsAddTitle: "新增片段",
snippetsEditTitle: "編輯片段",
syncWebDavTitle: "WebDAV 同步",
syncWebDavDescription: "上傳 DBX 快照到相容 WebDAV 的儲存服務,或從該服務還原快照。",
syncWebDavDescription: "上傳包含 SQL 庫的 DBX 快照到相容 WebDAV 的儲存服務,或從該服務還原快照。",
syncEndpoint: "WebDAV 地址",
syncUsername: "使用者名稱",
syncPassword: "密碼",
@@ -2090,6 +2090,11 @@ export default {
syncDownload: "下載",
syncTestSuccess: "WebDAV 目標可存取。",
syncUploadSuccess: "已上傳 {bytes} 位元組到 {path}。",
syncAutoUpload: "自動上傳快照",
syncAutoUploadInterval: "每",
syncAutoUploadMinutes: "分鐘",
syncAutoUploadDescription: "僅在 DBX 開啟時執行。下載仍需手動確認,避免意外覆蓋本機已儲存的 SQL。",
syncAutoUploadSuccess: "已自動上傳 {bytes} 位元組到 {path}。",
syncDownloadSuccess: "已從 {path} 下載並套用 {bytes} 位元組。",
syncSecretsApplied: "已還原加密敏感資訊。",
syncSecretsSkipped: "偵測到加密敏感資訊,但本次未還原。",
+12 -12
View File
@@ -962,28 +962,28 @@ export interface WebDavPasswordStatus {
hasSavedPassword: boolean;
}
export async function webdavSyncTest(_config: WebDavConfig): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
export async function webdavSyncTest(config: WebDavConfig): Promise<void> {
return post("/api/cloud-sync/webdav/test", { config });
}
export async function webdavPasswordStatus(_config: WebDavConfig): Promise<WebDavPasswordStatus> {
return { hasSavedPassword: false };
export async function webdavPasswordStatus(config: WebDavConfig): Promise<WebDavPasswordStatus> {
return post("/api/cloud-sync/webdav/password-status", { config });
}
export async function saveWebdavSavedPassword(_config: WebDavConfig, _password: string): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
export async function saveWebdavSavedPassword(config: WebDavConfig, password: string): Promise<void> {
return post("/api/cloud-sync/webdav/save-password", { config, password });
}
export async function forgetWebdavSavedPassword(_config: WebDavConfig): Promise<void> {
throw new Error("WebDAV sync is only available in the desktop app.");
export async function forgetWebdavSavedPassword(config: WebDavConfig): Promise<void> {
return post("/api/cloud-sync/webdav/forget-password", { config });
}
export async function webdavSyncUpload(_config: WebDavConfig, _editorSettings?: unknown, _secretsPassphrase?: string): Promise<WebDavSyncSummary> {
throw new Error("WebDAV sync is only available in the desktop app.");
export async function webdavSyncUpload(config: WebDavConfig, editorSettings?: unknown, secretsPassphrase?: string): Promise<WebDavSyncSummary> {
return post("/api/cloud-sync/webdav/upload", { config, editorSettings, secretsPassphrase });
}
export async function webdavSyncDownload(_config: WebDavConfig, _secretsPassphrase?: string): Promise<WebDavDownloadResult> {
throw new Error("WebDAV sync is only available in the desktop app.");
export async function webdavSyncDownload(config: WebDavConfig, secretsPassphrase?: string): Promise<WebDavDownloadResult> {
return post("/api/cloud-sync/webdav/download", { config, secretsPassphrase });
}
export async function loadPinnedTreeNodeIds(): Promise<string[]> {
+8 -1
View File
@@ -398,7 +398,14 @@ async fn main() {
"/app-settings/pinned-tree-node-ids",
get(routes::app_settings::load_pinned_tree_node_ids).post(routes::app_settings::save_pinned_tree_node_ids),
)
.route("/app-settings/config/decrypt", post(routes::app_settings::decrypt_config));
.route("/app-settings/config/decrypt", post(routes::app_settings::decrypt_config))
// Cloud sync
.route("/cloud-sync/webdav/test", post(routes::cloud_sync::webdav_sync_test))
.route("/cloud-sync/webdav/password-status", post(routes::cloud_sync::webdav_password_status))
.route("/cloud-sync/webdav/save-password", post(routes::cloud_sync::save_webdav_saved_password))
.route("/cloud-sync/webdav/forget-password", post(routes::cloud_sync::forget_webdav_saved_password))
.route("/cloud-sync/webdav/upload", post(routes::cloud_sync::webdav_sync_upload))
.route("/cloud-sync/webdav/download", post(routes::cloud_sync::webdav_sync_download));
let api = add_mq_routes(api)
.layer(middleware::from_fn_with_state(web_state.clone(), auth::auth_middleware))
+120
View File
@@ -0,0 +1,120 @@
use std::sync::Arc;
use axum::extract::State;
use axum::Json;
use dbx_core::cloud_sync::{
apply_sync_snapshot, build_sync_snapshot, forget_webdav_password, resolve_webdav_password, save_webdav_password,
webdav_saved_password_status, ApplySnapshotOptions, ApplySnapshotSummary, WebDavClient, WebDavConfig,
WebDavPasswordStatus, WebDavSyncSummary,
};
use dbx_core::storage::DesktopSettings;
use serde::{Deserialize, Serialize};
use crate::error::AppError;
use crate::state::WebState;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebDavDownloadResult {
pub summary: WebDavSyncSummary,
pub editor_settings: Option<serde_json::Value>,
pub desktop_settings: DesktopSettings,
pub apply_summary: ApplySnapshotSummary,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebDavConfigRequest {
pub config: WebDavConfig,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveWebDavPasswordRequest {
pub config: WebDavConfig,
pub password: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebDavUploadRequest {
pub config: WebDavConfig,
pub editor_settings: Option<serde_json::Value>,
pub secrets_passphrase: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebDavDownloadRequest {
pub config: WebDavConfig,
pub secrets_passphrase: Option<String>,
}
pub async fn webdav_sync_test(
State(state): State<Arc<WebState>>,
Json(mut req): Json<WebDavConfigRequest>,
) -> Result<Json<()>, AppError> {
resolve_webdav_password(&state.app.storage, &mut req.config).await.map_err(AppError)?;
WebDavClient::new(req.config).test().await.map_err(AppError)?;
Ok(Json(()))
}
pub async fn webdav_password_status(
State(state): State<Arc<WebState>>,
Json(req): Json<WebDavConfigRequest>,
) -> Result<Json<WebDavPasswordStatus>, AppError> {
webdav_saved_password_status(&state.app.storage, &req.config).await.map(Json).map_err(AppError)
}
pub async fn save_webdav_saved_password(
State(state): State<Arc<WebState>>,
Json(req): Json<SaveWebDavPasswordRequest>,
) -> Result<Json<()>, AppError> {
save_webdav_password(&state.app.storage, &req.config, &req.password).await.map_err(AppError)?;
Ok(Json(()))
}
pub async fn forget_webdav_saved_password(
State(state): State<Arc<WebState>>,
Json(req): Json<WebDavConfigRequest>,
) -> Result<Json<()>, AppError> {
forget_webdav_password(&state.app.storage, &req.config).await.map_err(AppError)?;
Ok(Json(()))
}
pub async fn webdav_sync_upload(
State(state): State<Arc<WebState>>,
Json(mut req): Json<WebDavUploadRequest>,
) -> Result<Json<WebDavSyncSummary>, AppError> {
resolve_webdav_password(&state.app.storage, &mut req.config).await.map_err(AppError)?;
let snapshot = build_sync_snapshot(
&state.app.storage,
env!("CARGO_PKG_VERSION"),
req.editor_settings,
req.secrets_passphrase.as_deref(),
)
.await
.map_err(AppError)?;
WebDavClient::new(req.config).put_snapshot(&snapshot).await.map(Json).map_err(AppError)
}
pub async fn webdav_sync_download(
State(state): State<Arc<WebState>>,
Json(mut req): Json<WebDavDownloadRequest>,
) -> Result<Json<WebDavDownloadResult>, AppError> {
resolve_webdav_password(&state.app.storage, &mut req.config).await.map_err(AppError)?;
let (snapshot, summary) = WebDavClient::new(req.config).get_snapshot().await.map_err(AppError)?;
let apply_summary = apply_sync_snapshot(
&state.app.storage,
&snapshot,
ApplySnapshotOptions { secrets_passphrase: req.secrets_passphrase.as_deref() },
)
.await
.map_err(AppError)?;
Ok(Json(WebDavDownloadResult {
summary,
editor_settings: snapshot.editor_settings,
desktop_settings: snapshot.desktop_settings,
apply_summary,
}))
}
+1
View File
@@ -1,6 +1,7 @@
pub mod agents;
pub mod ai;
pub mod app_settings;
pub mod cloud_sync;
pub mod connection;
pub mod data_compare;
pub mod database_export;