feat(update): add configurable update download source
This commit is contained in:
parent
d128604743
commit
76e2d33cd3
|
|
@ -34,6 +34,7 @@ import {
|
|||
type DesktopIconTheme,
|
||||
type InterfaceLayout,
|
||||
type DisconnectTabHandlingMode,
|
||||
type UpdateDownloadSource,
|
||||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
} from "@/stores/settingsStore";
|
||||
|
|
@ -226,6 +227,7 @@ const editExportBatchSize = ref(settingsStore.editorSettings.exportBatchSize);
|
|||
const editExportRowLimitEnabled = ref(settingsStore.editorSettings.exportRowLimitEnabled);
|
||||
const editExportRowLimit = ref(settingsStore.editorSettings.exportRowLimit);
|
||||
const editQueryExportKeysetOptimizationEnabled = ref(settingsStore.editorSettings.queryExportKeysetOptimizationEnabled);
|
||||
const editUpdateDownloadSource = ref<UpdateDownloadSource>(settingsStore.editorSettings.updateDownloadSource);
|
||||
const editToolbarItems = ref({ ...settingsStore.editorSettings.toolbarItems });
|
||||
const systemFonts = ref<string[]>([]);
|
||||
const systemFontsLoading = ref(false);
|
||||
|
|
@ -493,6 +495,7 @@ watch(
|
|||
editExportRowLimitEnabled.value = settingsStore.editorSettings.exportRowLimitEnabled;
|
||||
editExportRowLimit.value = settingsStore.editorSettings.exportRowLimit;
|
||||
editQueryExportKeysetOptimizationEnabled.value = settingsStore.editorSettings.queryExportKeysetOptimizationEnabled;
|
||||
editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource;
|
||||
editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems };
|
||||
editSnippets.value = settingsStore.editorSettings.snippets.map((s) => ({ ...s }));
|
||||
}
|
||||
|
|
@ -553,6 +556,7 @@ function hasChanges(): boolean {
|
|||
editExportRowLimitEnabled.value !== settingsStore.editorSettings.exportRowLimitEnabled ||
|
||||
editExportRowLimit.value !== settingsStore.editorSettings.exportRowLimit ||
|
||||
editQueryExportKeysetOptimizationEnabled.value !== settingsStore.editorSettings.queryExportKeysetOptimizationEnabled ||
|
||||
editUpdateDownloadSource.value !== settingsStore.editorSettings.updateDownloadSource ||
|
||||
JSON.stringify(editToolbarItems.value) !== JSON.stringify(settingsStore.editorSettings.toolbarItems) ||
|
||||
JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !== JSON.stringify(settingsStore.editorSettings.sidebarHiddenTablePrefixes) ||
|
||||
JSON.stringify(editSnippets.value) !== JSON.stringify(settingsStore.editorSettings.snippets)
|
||||
|
|
@ -597,6 +601,7 @@ async function persistSettings() {
|
|||
exportRowLimitEnabled: editExportRowLimitEnabled.value,
|
||||
exportRowLimit: editExportRowLimit.value,
|
||||
queryExportKeysetOptimizationEnabled: editQueryExportKeysetOptimizationEnabled.value,
|
||||
updateDownloadSource: editUpdateDownloadSource.value,
|
||||
toolbarItems: { ...editToolbarItems.value },
|
||||
snippets: editSnippets.value,
|
||||
});
|
||||
|
|
@ -675,6 +680,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
|||
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
|
||||
} else if (tab === "snippets") {
|
||||
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
|
||||
} else if (tab === "about") {
|
||||
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -719,6 +726,7 @@ function resetAllDefaults() {
|
|||
editExportRowLimitEnabled.value = DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled;
|
||||
editExportRowLimit.value = DEFAULT_EDITOR_SETTINGS.exportRowLimit;
|
||||
editQueryExportKeysetOptimizationEnabled.value = DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled;
|
||||
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
|
||||
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
|
||||
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
|
||||
}
|
||||
|
|
@ -904,6 +912,10 @@ function onLocaleChange(v: any) {
|
|||
if (typeof v === "string") void setLocale(v as Locale);
|
||||
}
|
||||
|
||||
function onUpdateDownloadSourceChange(v: any) {
|
||||
if (v === "official" || v === "cnb") editUpdateDownloadSource.value = v;
|
||||
}
|
||||
|
||||
function setSidebarObjectDisplay(value: "grouped" | "simple") {
|
||||
editSidebarObjectDisplay.value = value;
|
||||
}
|
||||
|
|
@ -3342,6 +3354,24 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<Label>{{ t("settings.updateDownloadSource") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.updateDownloadSourceDescription") }}</p>
|
||||
</div>
|
||||
<Select :model-value="editUpdateDownloadSource" @update:model-value="onUpdateDownloadSourceChange">
|
||||
<SelectTrigger class="h-9 w-full sm:w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{{ t("settings.updateDownloadSourceOfficial") }}</SelectItem>
|
||||
<SelectItem value="cnb">{{ t("settings.updateDownloadSourceCnb") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<button type="button" class="rounded-lg border p-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" @click="openExternalUrl('https://qm.qq.com/cgi-bin/qm/qr?k=&group_code=1087880322')">
|
||||
<div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
|
|
@ -3515,6 +3545,12 @@ watch(
|
|||
<Button variant="outline" @click="emit('update:open', false)">
|
||||
{{ t("common.close") }}
|
||||
</Button>
|
||||
<Button :disabled="!hasChanges() || hasApplyBlocker" @click="applySettings">
|
||||
{{ t("settings.apply") }}
|
||||
</Button>
|
||||
<Button :disabled="!hasChanges() || hasApplyBlocker" @click="applySettingsAndClose">
|
||||
{{ t("settings.applyAndClose") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { useI18n } from "vue-i18n";
|
|||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import * as api from "@/lib/api";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { UpdateDownloadSource as SettingsUpdateDownloadSource } from "@/stores/settingsStore";
|
||||
import type { UpdateDownloadProgress } from "@/lib/tauri";
|
||||
|
||||
export function shouldOpenUpdateDialog(options: { silent?: boolean }) {
|
||||
return options.silent !== true;
|
||||
|
|
@ -12,6 +15,23 @@ export function canDownloadAndInstallUpdate(info: api.UpdateInfo | null, isDeskt
|
|||
return isDesktop && info?.update_available === true && info.portable_mode !== true;
|
||||
}
|
||||
|
||||
export function normalizeUpdateDownloadSource(value: unknown): SettingsUpdateDownloadSource {
|
||||
return value === "cnb" ? "cnb" : "official";
|
||||
}
|
||||
|
||||
export function tagVersion(version: string): string {
|
||||
const trimmed = version.trim();
|
||||
return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
|
||||
}
|
||||
|
||||
export function resolveUpdateReleaseUrl(info: api.UpdateInfo | null, source: unknown, fallbackUrl: string): string {
|
||||
const normalizedSource = normalizeUpdateDownloadSource(source);
|
||||
if (normalizedSource === "cnb" && info?.latest_version) {
|
||||
return `https://cnb.cool/dbxio.com/dbx/-/releases/tag/${tagVersion(info.latest_version)}`;
|
||||
}
|
||||
return info?.release_url || fallbackUrl;
|
||||
}
|
||||
|
||||
export async function resolveUpdaterProxy(): Promise<string | undefined> {
|
||||
if (!isTauriRuntime()) return undefined;
|
||||
try {
|
||||
|
|
@ -25,6 +45,7 @@ export async function resolveUpdaterProxy(): Promise<string | undefined> {
|
|||
export function useAppUpdater() {
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const checkingUpdates = ref(false);
|
||||
const updateInfo = ref<api.UpdateInfo | null>(null);
|
||||
|
|
@ -78,7 +99,7 @@ export function useAppUpdater() {
|
|||
}
|
||||
|
||||
function openLatestRelease() {
|
||||
const url = updateInfo.value?.release_url || latestReleaseUrl;
|
||||
const url = resolveUpdateReleaseUrl(updateInfo.value, settingsStore.editorSettings.updateDownloadSource, latestReleaseUrl);
|
||||
openUrl(url);
|
||||
}
|
||||
|
||||
|
|
@ -90,27 +111,21 @@ export function useAppUpdater() {
|
|||
}
|
||||
isDownloadingUpdate.value = true;
|
||||
downloadProgress.value = 0;
|
||||
let unlisten: (() => void) | undefined;
|
||||
const latestVersion = updateInfo.value?.latest_version;
|
||||
try {
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
const proxy = await resolveUpdaterProxy();
|
||||
const update = await check(proxy ? { proxy } : undefined);
|
||||
if (!update) return;
|
||||
let totalBytes = 0;
|
||||
let downloadedBytes = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
if (event.event === "Started" && event.data.contentLength) {
|
||||
totalBytes = event.data.contentLength;
|
||||
} else if (event.event === "Progress") {
|
||||
downloadedBytes += event.data.chunkLength;
|
||||
downloadProgress.value = totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
|
||||
} else if (event.event === "Finished") {
|
||||
downloadProgress.value = 100;
|
||||
}
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
unlisten = await listen<UpdateDownloadProgress>("update-download-progress", (event) => {
|
||||
const total = event.payload.total ?? 0;
|
||||
downloadProgress.value = total > 0 ? Math.round((event.payload.downloaded / total) * 100) : 0;
|
||||
});
|
||||
await api.downloadAndInstallUpdate(normalizeUpdateDownloadSource(settingsStore.editorSettings.updateDownloadSource), latestVersion);
|
||||
downloadProgress.value = 100;
|
||||
updateReady.value = true;
|
||||
} catch (e: any) {
|
||||
toast(t("updates.downloadFailed", { error: e?.message || String(e) }), 5000);
|
||||
} finally {
|
||||
unlisten?.();
|
||||
isDownloadingUpdate.value = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2482,6 +2482,10 @@ export default {
|
|||
closeActionMinimize: "Minimize to tray",
|
||||
updateNotificationsEnabled: "Enable update reminders",
|
||||
updateNotificationsEnabledDescription: "When disabled, DBX will not automatically check app or driver updates or show update badges. Manual checks are still available.",
|
||||
updateDownloadSource: "Update download source",
|
||||
updateDownloadSourceDescription: "Choose where in-app update installers are downloaded from. The official source is recommended; CNB can be faster on mainland China networks.",
|
||||
updateDownloadSourceOfficial: "Official source (recommended)",
|
||||
updateDownloadSourceCnb: "CNB",
|
||||
debugLoggingEnabled: "Enable debug logs",
|
||||
debugLoggingEnabledDescription: "Record more detailed local user-side diagnostic logs while troubleshooting reports. Disabled by default.",
|
||||
debugLogsCopy: "Copy logs",
|
||||
|
|
|
|||
|
|
@ -2506,6 +2506,10 @@ export default {
|
|||
closeActionMinimize: "最小化到托盘",
|
||||
updateNotificationsEnabled: "启用更新提醒",
|
||||
updateNotificationsEnabledDescription: "关闭后,DBX 不会自动检查应用和驱动更新,也不会显示更新红点;仍可手动检查更新。",
|
||||
updateDownloadSource: "更新下载源",
|
||||
updateDownloadSourceDescription: "选择应用内更新安装包的下载来源。官方源为推荐选项,CNB 适合国内网络环境。",
|
||||
updateDownloadSourceOfficial: "官方源(推荐)",
|
||||
updateDownloadSourceCnb: "CNB",
|
||||
debugLoggingEnabled: "启用调试日志",
|
||||
debugLoggingEnabledDescription: "开启后在本机记录更详细的用户侧诊断日志,反馈问题时可临时开启;默认关闭。",
|
||||
debugLogsCopy: "复制日志",
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ export const checkMcpServerStatus = forward("checkMcpServerStatus");
|
|||
export const installMcpServer = forward("installMcpServer");
|
||||
export const checkForUpdates = forward("checkForUpdates");
|
||||
export const getSystemProxyUrl = forward("getSystemProxyUrl");
|
||||
export const downloadAndInstallUpdate = forward("downloadAndInstallUpdate");
|
||||
export const getAppVersion = forward("getAppVersion");
|
||||
|
||||
// Layout
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import type {
|
|||
DriverInstallProgress,
|
||||
JavaRuntimeConfig,
|
||||
UpdateInfo,
|
||||
UpdateDownloadSource,
|
||||
RedisDatabaseInfo,
|
||||
RedisValue,
|
||||
RedisScanResult,
|
||||
|
|
@ -1810,6 +1811,10 @@ export async function getSystemProxyUrl(): Promise<string | null> {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function downloadAndInstallUpdate(_source: UpdateDownloadSource, _latestVersion?: string): Promise<void> {
|
||||
throw new Error("In-app update installation is only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function getAppVersion(): Promise<string> {
|
||||
const res: { version: string } = await get("/api/version");
|
||||
return res.version;
|
||||
|
|
|
|||
|
|
@ -1103,6 +1103,13 @@ export interface UpdateInfo {
|
|||
release_notes: string;
|
||||
}
|
||||
|
||||
export type UpdateDownloadSource = "official" | "cnb";
|
||||
|
||||
export interface UpdateDownloadProgress {
|
||||
downloaded: number;
|
||||
total: number | null;
|
||||
}
|
||||
|
||||
export interface McpServerStatus {
|
||||
installed: boolean;
|
||||
npm_available: boolean;
|
||||
|
|
@ -1132,6 +1139,10 @@ export async function getSystemProxyUrl(): Promise<string | null> {
|
|||
return invoke("get_system_proxy_url");
|
||||
}
|
||||
|
||||
export async function downloadAndInstallUpdate(source: UpdateDownloadSource, latestVersion?: string): Promise<void> {
|
||||
return invoke("download_and_install_update", { source, latestVersion });
|
||||
}
|
||||
|
||||
export async function getAppVersion(): Promise<string> {
|
||||
const { getVersion } = await import("@tauri-apps/api/app");
|
||||
return getVersion();
|
||||
|
|
|
|||
|
|
@ -9,4 +9,13 @@ describe("normalizeEditorSettings", () => {
|
|||
it("preserves disabled automatic table aliases", () => {
|
||||
expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults update downloads to the official source", () => {
|
||||
expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official");
|
||||
});
|
||||
|
||||
it("preserves CNB update download source and rejects invalid values", () => {
|
||||
expect(normalizeEditorSettings({ updateDownloadSource: "cnb" }).updateDownloadSource).toBe("cnb");
|
||||
expect(normalizeEditorSettings({ updateDownloadSource: "mirror" as any }).updateDownloadSource).toBe("official");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export type DesktopIconTheme = "default" | "black";
|
|||
|
||||
export type InterfaceLayout = "separated" | "classic";
|
||||
|
||||
export type UpdateDownloadSource = "official" | "cnb";
|
||||
|
||||
export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000;
|
||||
|
||||
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
||||
|
|
@ -359,6 +361,7 @@ export interface EditorSettings {
|
|||
exportRowLimitEnabled: boolean;
|
||||
exportRowLimit: number;
|
||||
queryExportKeysetOptimizationEnabled: boolean;
|
||||
updateDownloadSource: UpdateDownloadSource;
|
||||
toolbarItems: ToolbarItems;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +465,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
exportRowLimitEnabled: false,
|
||||
exportRowLimit: 100000,
|
||||
queryExportKeysetOptimizationEnabled: true,
|
||||
updateDownloadSource: "official",
|
||||
toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS },
|
||||
};
|
||||
|
||||
|
|
@ -494,6 +498,10 @@ function normalizeDataGridRenderMode(value: unknown): DataGridRenderMode {
|
|||
return DATA_GRID_RENDER_MODES.includes(value as DataGridRenderMode) ? (value as DataGridRenderMode) : DEFAULT_EDITOR_SETTINGS.dataGridRenderMode;
|
||||
}
|
||||
|
||||
function normalizeUpdateDownloadSource(value: unknown): UpdateDownloadSource {
|
||||
return value === "cnb" ? "cnb" : DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
|
||||
}
|
||||
|
||||
function normalizeDisconnectTabHandlingMode(value: unknown, legacyCloseTabsOnDisconnect?: unknown): DisconnectTabHandlingMode {
|
||||
if (DISCONNECT_TAB_HANDLING_MODES.includes(value as DisconnectTabHandlingMode)) {
|
||||
return value as DisconnectTabHandlingMode;
|
||||
|
|
@ -632,6 +640,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
exportRowLimitEnabled: typeof settings.exportRowLimitEnabled === "boolean" ? settings.exportRowLimitEnabled : DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled,
|
||||
exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit,
|
||||
queryExportKeysetOptimizationEnabled: typeof settings.queryExportKeysetOptimizationEnabled === "boolean" ? settings.queryExportKeysetOptimizationEnabled : DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled,
|
||||
updateDownloadSource: normalizeUpdateDownloadSource(settings.updateDownloadSource),
|
||||
toolbarItems: normalizeToolbarItems(settings.toolbarItems),
|
||||
};
|
||||
}
|
||||
|
|
@ -803,6 +812,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
if (partial.exportRowLimitEnabled !== undefined) editorSettings.value.exportRowLimitEnabled = partial.exportRowLimitEnabled;
|
||||
if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit)));
|
||||
if (partial.queryExportKeysetOptimizationEnabled !== undefined) editorSettings.value.queryExportKeysetOptimizationEnabled = partial.queryExportKeysetOptimizationEnabled;
|
||||
if (partial.updateDownloadSource !== undefined) editorSettings.value.updateDownloadSource = normalizeUpdateDownloadSource(partial.updateDownloadSource);
|
||||
if (partial.toolbarItems !== undefined) editorSettings.value.toolbarItems = normalizeToolbarItems(partial.toolbarItems);
|
||||
saveEditorSettings(editorSettings.value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
|
||||
import { canDownloadAndInstallUpdate } from "../../apps/desktop/src/composables/useAppUpdater.ts";
|
||||
import { canDownloadAndInstallUpdate, normalizeUpdateDownloadSource, resolveUpdateReleaseUrl, tagVersion } from "../../apps/desktop/src/composables/useAppUpdater.ts";
|
||||
import type { UpdateInfo } from "../../apps/desktop/src/lib/api.ts";
|
||||
|
||||
function updateInfo(overrides: Partial<UpdateInfo> = {}): UpdateInfo {
|
||||
|
|
@ -30,3 +30,21 @@ test("blocks in-app update installation outside desktop runtime or without an up
|
|||
assert.equal(canDownloadAndInstallUpdate(updateInfo({ update_available: false }), true), false);
|
||||
assert.equal(canDownloadAndInstallUpdate(null, true), false);
|
||||
});
|
||||
|
||||
test("normalizes update download source", () => {
|
||||
assert.equal(normalizeUpdateDownloadSource("official"), "official");
|
||||
assert.equal(normalizeUpdateDownloadSource("cnb"), "cnb");
|
||||
assert.equal(normalizeUpdateDownloadSource("unknown"), "official");
|
||||
});
|
||||
|
||||
test("normalizes release tag versions", () => {
|
||||
assert.equal(tagVersion("0.5.39"), "v0.5.39");
|
||||
assert.equal(tagVersion("v0.5.39"), "v0.5.39");
|
||||
});
|
||||
|
||||
test("resolves release page URL from update download source", () => {
|
||||
const fallbackUrl = "https://github.com/t8y2/dbx/releases/latest";
|
||||
assert.equal(resolveUpdateReleaseUrl(updateInfo({ latest_version: "0.5.39" }), "cnb", fallbackUrl), "https://cnb.cool/dbxio.com/dbx/-/releases/tag/v0.5.39");
|
||||
assert.equal(resolveUpdateReleaseUrl(updateInfo({ release_url: "https://github.com/t8y2/dbx/releases/tag/v0.5.39" }), "official", fallbackUrl), "https://github.com/t8y2/dbx/releases/tag/v0.5.39");
|
||||
assert.equal(resolveUpdateReleaseUrl(null, "cnb", fallbackUrl), fallbackUrl);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,78 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub use dbx_core::update::UpdateInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
const OFFICIAL_UPDATE_ENDPOINTS: [&str; 2] = [
|
||||
"https://dl.dbxio.com/releases/latest/latest.json",
|
||||
"https://github.com/t8y2/dbx/releases/latest/download/latest.json",
|
||||
];
|
||||
const CNB_RELEASE_DOWNLOAD_PREFIX: &str = "https://cnb.cool/dbxio.com/dbx/-/releases/download/";
|
||||
const GITHUB_RELEASE_DOWNLOAD_PREFIX: &str = "https://github.com/t8y2/dbx/releases/download/";
|
||||
const UPDATE_DOWNLOAD_PROGRESS_EVENT: &str = "update-download-progress";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UpdateDownloadSource {
|
||||
Official,
|
||||
Cnb,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct UpdateDownloadProgress {
|
||||
pub downloaded: u64,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
impl UpdateDownloadSource {
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Official => "official",
|
||||
Self::Cnb => "cnb",
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoints(&self, latest_version: Option<&str>) -> Result<Vec<String>, String> {
|
||||
match self {
|
||||
Self::Official => Ok(OFFICIAL_UPDATE_ENDPOINTS.iter().map(|endpoint| endpoint.to_string()).collect()),
|
||||
Self::Cnb => {
|
||||
let version =
|
||||
latest_version.ok_or_else(|| "Latest version is required for CNB updates.".to_string())?;
|
||||
Ok(vec![format!("{CNB_RELEASE_DOWNLOAD_PREFIX}{}/latest.json", tag_version(version))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_download_url(&self, url: &str) -> Result<Option<String>, String> {
|
||||
if !matches!(self, Self::Cnb) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if url.starts_with(CNB_RELEASE_DOWNLOAD_PREFIX) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rewritten = url
|
||||
.strip_prefix(GITHUB_RELEASE_DOWNLOAD_PREFIX)
|
||||
.map(|path| format!("{CNB_RELEASE_DOWNLOAD_PREFIX}{path}"))
|
||||
.ok_or_else(|| format!("Unsupported update download URL for CNB source: {url}"))?;
|
||||
Ok(Some(rewritten))
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_version(version: &str) -> String {
|
||||
let version = version.trim();
|
||||
if version.starts_with('v') {
|
||||
version.to_string()
|
||||
} else {
|
||||
format!("v{version}")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
||||
|
|
@ -13,3 +87,98 @@ pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
|||
pub async fn get_system_proxy_url() -> Option<String> {
|
||||
tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.ok().flatten()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_and_install_update(
|
||||
app: AppHandle,
|
||||
source: UpdateDownloadSource,
|
||||
latest_version: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
if crate::data_dir::is_portable_mode() {
|
||||
return Err("Portable builds cannot use the in-app installer.".to_string());
|
||||
}
|
||||
|
||||
let endpoint_urls = source.endpoints(latest_version.as_deref())?;
|
||||
println!("[DBX updater] checking from {} endpoints: {}", source.label(), endpoint_urls.join(", "));
|
||||
let mut endpoints = Vec::with_capacity(endpoint_urls.len());
|
||||
for endpoint_url in endpoint_urls {
|
||||
endpoints.push(endpoint_url.parse().map_err(|e| format!("Invalid update endpoint: {e}"))?);
|
||||
}
|
||||
let mut builder =
|
||||
app.updater_builder().endpoints(endpoints).map_err(|e| format!("Failed to configure updater endpoint: {e}"))?;
|
||||
|
||||
if let Some(proxy_url) = dbx_core::update::system_proxy_url() {
|
||||
let proxy = proxy_url.parse().map_err(|e| format!("Invalid system proxy URL: {e}"))?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
let updater = builder.build().map_err(|e| format!("Failed to create updater: {e}"))?;
|
||||
let update = updater.check().await.map_err(|e| format!("Failed to check updates: {e}"))?;
|
||||
let Some(mut update) = update else {
|
||||
return Err("No update available.".to_string());
|
||||
};
|
||||
if let Some(download_url) = source.rewrite_download_url(update.download_url.as_str())? {
|
||||
update.download_url = download_url.parse().map_err(|e| format!("Invalid CNB update download URL: {e}"))?;
|
||||
}
|
||||
println!("[DBX updater] downloading from {} URL: {}", source.label(), update.download_url);
|
||||
|
||||
let downloaded = Arc::new(AtomicU64::new(0));
|
||||
let finished_downloaded = Arc::clone(&downloaded);
|
||||
update
|
||||
.download_and_install(
|
||||
|chunk_len, total| {
|
||||
let downloaded =
|
||||
downloaded.fetch_add(chunk_len as u64, Ordering::Relaxed).saturating_add(chunk_len as u64);
|
||||
let _ = app.emit(UPDATE_DOWNLOAD_PROGRESS_EVENT, UpdateDownloadProgress { downloaded, total });
|
||||
},
|
||||
|| {
|
||||
let downloaded = finished_downloaded.load(Ordering::Relaxed);
|
||||
let _ = app.emit(
|
||||
UPDATE_DOWNLOAD_PROGRESS_EVENT,
|
||||
UpdateDownloadProgress { downloaded, total: Some(downloaded) },
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download and install update: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{tag_version, UpdateDownloadSource, CNB_RELEASE_DOWNLOAD_PREFIX, OFFICIAL_UPDATE_ENDPOINTS};
|
||||
|
||||
#[test]
|
||||
fn normalizes_update_tag_versions() {
|
||||
assert_eq!(tag_version("0.5.39"), "v0.5.39");
|
||||
assert_eq!(tag_version("v0.5.39"), "v0.5.39");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_official_update_endpoints() {
|
||||
let endpoints = UpdateDownloadSource::Official.endpoints(None).unwrap();
|
||||
assert_eq!(endpoints, OFFICIAL_UPDATE_ENDPOINTS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_cnb_update_endpoint_for_tag() {
|
||||
let endpoints = UpdateDownloadSource::Cnb.endpoints(Some("0.5.39")).unwrap();
|
||||
assert_eq!(endpoints, vec![format!("{CNB_RELEASE_DOWNLOAD_PREFIX}v0.5.39/latest.json")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrites_github_asset_url_to_cnb() {
|
||||
let download_url = UpdateDownloadSource::Cnb
|
||||
.rewrite_download_url("https://github.com/t8y2/dbx/releases/download/v0.5.39/DBX_0.5.39_aarch64.dmg")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(download_url, "https://cnb.cool/dbxio.com/dbx/-/releases/download/v0.5.39/DBX_0.5.39_aarch64.dmg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_existing_cnb_asset_url() {
|
||||
let download_url = UpdateDownloadSource::Cnb
|
||||
.rewrite_download_url("https://cnb.cool/dbxio.com/dbx/-/releases/download/v0.5.39/DBX_0.5.39_aarch64.dmg")
|
||||
.unwrap();
|
||||
assert_eq!(download_url, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -878,6 +878,7 @@ pub fn run() {
|
|||
commands::mcp::install_mcp_server,
|
||||
commands::update::check_for_updates,
|
||||
commands::update::get_system_proxy_url,
|
||||
commands::update::download_and_install_update,
|
||||
commands::transfer::start_transfer,
|
||||
commands::transfer::cancel_transfer,
|
||||
commands::database_export::export_database_sql,
|
||||
|
|
|
|||
Loading…
Reference in New Issue