feat(update): mirror agent release downloads

This commit is contained in:
t8y2 2026-07-11 23:40:18 +08:00
parent f4ff8b11a5
commit fec91b9aef
10 changed files with 355 additions and 53 deletions

View File

@ -551,3 +551,67 @@ jobs:
aws s3 cp "$f" "$R2_BUCKET/$key" --endpoint-url "$R2_ENDPOINT"
echo "Uploaded $(basename $f)"
done
sync-release-to-cnb:
name: Sync agents releases to CNB
needs: release
if: ${{ always() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
tag: ["${{ github.ref_name }}", "agents-latest"]
steps:
- name: Sync release assets to CNB
env:
TAG_NAME: ${{ matrix.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
CNB_REPOSITORY: dbxio.com/dbx
CNB_TOKEN: ${{ secrets.CNB_TOKEN }}
run: |
set -euo pipefail
wget -q https://cnb.cool/znb/mpgrm/-/releases/download/v0.1.0/mpgrm_linux_amd64.tar.gz
tar -xf mpgrm_linux_amd64.tar.gz
chmod +x mpgrm
./mpgrm releases sync \
--repo "https://github.com/${GITHUB_REPOSITORY}.git" \
--target-repo "https://cnb.cool/${CNB_REPOSITORY}.git" \
--tags "${TAG_NAME}"
sync-release-to-atomgit:
name: Sync agents releases to AtomGit
needs: release
if: ${{ always() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
tag: ["${{ github.ref_name }}", "agents-latest"]
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Sync release assets to AtomGit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ matrix.tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
ATOMGIT_TOKEN: ${{ secrets.ATOMGIT_TOKEN }}
ATOMGIT_REPOSITORY: t8y2/dbx
run: |
set -euo pipefail
git fetch origin "refs/tags/${TAG_NAME}:refs/tags/${TAG_NAME}" --force
git remote add atomgit "https://t8y2:${ATOMGIT_TOKEN}@atomgit.com/t8y2/dbx.git"
git push atomgit "refs/tags/${TAG_NAME}:refs/tags/${TAG_NAME}" --force
mkdir -p "$RUNNER_TEMP/github-release" "$RUNNER_TEMP/release-assets"
gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" \
--json tagName,name,body,targetCommitish,isPrerelease,isDraft,assets \
> "$RUNNER_TEMP/github-release/release.json"
gh release download "$TAG_NAME" --repo "$GITHUB_REPOSITORY" \
--dir "$RUNNER_TEMP/release-assets" --clobber
node .github/scripts/sync-atomgit-release.mjs \
--github-release "$RUNNER_TEMP/github-release/release.json" \
--assets-dir "$RUNNER_TEMP/release-assets" \
${{ matrix.tag == 'agents-latest' && '--replace-assets' || '' }}

View File

@ -2994,7 +2994,7 @@ export default {
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 or AtomGit can be faster on mainland China networks.",
updateDownloadSourceDescription: "Choose where app updates, database agents, drivers, and managed JREs are downloaded from. The official source is recommended; CNB or AtomGit can be faster on mainland China networks.",
updateDownloadSourceOfficial: "Official source (recommended)",
updateDownloadSourceCnb: "CNB",
updateDownloadSourceAtomgit: "AtomGit",

View File

@ -2993,7 +2993,7 @@ export default withEnglishFallback({
updateNotificationsEnabled: "启用更新提醒",
updateNotificationsEnabledDescription: "关闭后DBX 不会自动检查应用和驱动更新,也不会显示更新红点;仍可手动检查更新。",
updateDownloadSource: "更新下载源",
updateDownloadSourceDescription: "选择应用内更新安装包的下载来源。官方源为推荐选项CNB 或 AtomGit 适合国内网络环境。",
updateDownloadSourceDescription: "选择应用更新、数据库 Agent、驱动和托管 JRE 的下载来源。官方源为推荐选项CNB 或 AtomGit 适合国内网络环境。",
updateDownloadSourceOfficial: "官方源(推荐)",
updateDownloadSourceCnb: "CNB",
updateDownloadSourceAtomgit: "AtomGit",

View File

@ -1,6 +1,7 @@
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import type * as TauriModule from "@/lib/backend/tauri";
import { appendDebugLog } from "@/lib/backend/debugLog";
import { useSettingsStore } from "@/stores/settingsStore";
// ---------------------------------------------------------------------------
// Lazy backend resolution (avoids top-level await)
@ -76,15 +77,24 @@ export const installJdbcPlugin = forward("installJdbcPlugin");
export const installJdbcPluginLocal = forward("installJdbcPluginLocal");
export const uninstallJdbcPlugin = forward("uninstallJdbcPlugin");
export const listInstalledAgentsLocal = forward("listInstalledAgentsLocal");
export const listInstalledAgents = forward("listInstalledAgents");
export async function listInstalledAgents() {
const backend = await getBackend();
return backend.listInstalledAgents(useSettingsStore().editorSettings.updateDownloadSource);
}
export const isAgentInstalled = forward("isAgentInstalled");
export const getDriverStoreUsage = forward("getDriverStoreUsage");
export const clearDriverDownloadCache = forward("clearDriverDownloadCache");
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
export const stopDriverRuntime = forward("stopDriverRuntime");
export const restartDriverRuntime = forward("restartDriverRuntime");
export const installAgent = forward("installAgent");
export const upgradeAllAgents = forward("upgradeAllAgents");
export async function installAgent(dbType: string) {
const backend = await getBackend();
return backend.installAgent(dbType, useSettingsStore().editorSettings.updateDownloadSource);
}
export async function upgradeAllAgents() {
const backend = await getBackend();
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource);
}
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
export const uninstallAgent = forward("uninstallAgent");
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
@ -92,7 +102,10 @@ export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
export const importAgentsFromZip = forward("importAgentsFromZip");
export const importAgentJar = forward("importAgentJar");
export const reinstallJre = forward("reinstallJre");
export async function reinstallJre(jreKey?: string) {
const backend = await getBackend();
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource);
}
export const uninstallJre = forward("uninstallJre");
export const listenAgentInstallProgress = forward("listenAgentInstallProgress");
export const loadSavedSqlLibrary = forward("loadSavedSqlLibrary");

View File

@ -342,7 +342,7 @@ export async function listInstalledAgentsLocal(): Promise<AgentDriverInfo[]> {
return get("/api/agents/installed-local");
}
export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
export async function listInstalledAgents(_source?: UpdateDownloadSource): Promise<AgentDriverInfo[]> {
return get("/api/agents/installed");
}
@ -370,11 +370,11 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
await post("/api/agents/runtime/restart", { runtimeId });
}
export async function installAgent(dbType: string): Promise<void> {
export async function installAgent(dbType: string, _source?: UpdateDownloadSource): Promise<void> {
await post("/api/agents/install", { dbType });
}
export async function upgradeAllAgents(): Promise<UpgradeAllAgentDriversResult> {
export async function upgradeAllAgents(_source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
return post("/api/agents/upgrade-all", {});
}
@ -427,7 +427,7 @@ export async function importAgentJar(dbType: string, pathOrFile: string | File):
if (!uploadRes.ok) throw new Error(await uploadRes.text());
}
export async function reinstallJre(jreKey?: string): Promise<void> {
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource): Promise<void> {
await post("/api/agents/reinstall-jre", { jreKey });
}

View File

@ -1188,8 +1188,8 @@ export async function listInstalledAgentsLocal(): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents_local");
}
export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents");
export async function listInstalledAgents(source?: UpdateDownloadSource): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents", { source });
}
export async function isAgentInstalled(dbType: string): Promise<boolean> {
@ -1216,12 +1216,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
return invoke("restart_driver_runtime", { runtimeId });
}
export async function installAgent(dbType: string): Promise<void> {
return invoke("install_agent", { dbType });
export async function installAgent(dbType: string, source?: UpdateDownloadSource): Promise<void> {
return invoke("install_agent", { dbType, source });
}
export async function upgradeAllAgents(): Promise<UpgradeAllAgentDriversResult> {
return invoke("upgrade_all_agents");
export async function upgradeAllAgents(source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
return invoke("upgrade_all_agents", { source });
}
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
@ -1258,8 +1258,8 @@ export async function importAgentJar(dbType: string, path: string | File): Promi
return invoke("import_agent_jar_cmd", { dbType, path });
}
export async function reinstallJre(jreKey?: string): Promise<void> {
return invoke("reinstall_jre", { jreKey });
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource): Promise<void> {
return invoke("reinstall_jre", { jreKey, source });
}
export async function uninstallJre(jreKey: string): Promise<void> {

View File

@ -7,6 +7,7 @@ use crate::agent_catalog;
use crate::agent_manager::{
AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, JavaRuntimeMode, DEFAULT_JRE_KEY,
};
use crate::DownloadSource;
/// Number of attempts to delete a JRE directory before giving up (Windows
/// experiences transient `ERROR_ACCESS_DENIED` when java.exe is still mapped
@ -131,8 +132,9 @@ fn replace_old_jre_dir(am: &AgentManager, path: &Path) -> Result<Option<PathBuf>
const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx/releases/download/agents-latest/agent-registry.json";
const REGISTRY_R2_PATH: &str = "agents/agent-registry.json";
static REGISTRY_CACHE: std::sync::LazyLock<tokio::sync::Mutex<Option<(std::time::Instant, AgentRegistry)>>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(None));
static REGISTRY_CACHE: std::sync::LazyLock<
tokio::sync::Mutex<std::collections::HashMap<DownloadSource, (std::time::Instant, AgentRegistry)>>,
> = std::sync::LazyLock::new(|| tokio::sync::Mutex::new(std::collections::HashMap::new()));
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct AgentProgressEvent {
@ -323,9 +325,13 @@ pub fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) ->
}
pub async fn fetch_registry() -> Result<AgentRegistry, String> {
fetch_registry_from(DownloadSource::Official).await
}
pub async fn fetch_registry_from(source: DownloadSource) -> Result<AgentRegistry, String> {
{
let cache = REGISTRY_CACHE.lock().await;
if let Some((ts, registry)) = cache.as_ref() {
if let Some((ts, registry)) = cache.get(&source) {
if ts.elapsed() < std::time::Duration::from_secs(300) {
return Ok(registry.clone());
}
@ -335,16 +341,40 @@ pub async fn fetch_registry() -> Result<AgentRegistry, String> {
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|err| format!("Failed to create HTTP client: {err}"))?;
let resp = crate::race_download(&client, REGISTRY_PATH, REGISTRY_R2_PATH, "dbx-agent-manager")
let resp = open_download_response(&client, source, REGISTRY_PATH, REGISTRY_R2_PATH, "dbx-agent-manager")
.await
.map_err(|err| format!("Failed to fetch agent registry: {err}"))?;
let registry: AgentRegistry = resp.json().await.map_err(|err| format!("Failed to parse registry: {err}"))?;
*REGISTRY_CACHE.lock().await = Some((std::time::Instant::now(), registry.clone()));
REGISTRY_CACHE.lock().await.insert(source, (std::time::Instant::now(), registry.clone()));
Ok(registry)
}
async fn open_download_response(
client: &reqwest::Client,
source: DownloadSource,
github_url: &str,
r2_path: &str,
user_agent: &str,
) -> Result<reqwest::Response, String> {
let mut errors = Vec::new();
for url in source.download_candidate_urls(github_url, r2_path)? {
match client
.get(&url)
.header(reqwest::header::USER_AGENT, user_agent)
.header(reqwest::header::ACCEPT_ENCODING, "identity")
.send()
.await
.and_then(|response| response.error_for_status())
{
Ok(response) => return Ok(response),
Err(error) => errors.push(format!("{url}: {error}")),
}
}
Err(errors.join("; "))
}
pub async fn invalidate_registry_cache() {
*REGISTRY_CACHE.lock().await = None;
REGISTRY_CACHE.lock().await.clear();
}
pub async fn install_agent_driver(
@ -352,14 +382,31 @@ pub async fn install_agent_driver(
db_type: &str,
progress: impl Fn(AgentProgressEvent),
) -> Result<(), String> {
install_agent_driver_with_batch(am, db_type, &progress, None, None).await
install_agent_driver_from(am, db_type, DownloadSource::Official, progress).await
}
pub async fn install_agent_driver_from(
am: &AgentManager,
db_type: &str,
source: DownloadSource,
progress: impl Fn(AgentProgressEvent),
) -> Result<(), String> {
install_agent_driver_with_batch(am, db_type, source, &progress, None, None).await
}
pub async fn upgrade_all_agent_drivers(
am: &AgentManager,
progress: impl Fn(AgentProgressEvent),
) -> Result<UpgradeAllAgentDriversResult, String> {
let registry = fetch_registry().await?;
upgrade_all_agent_drivers_from(am, DownloadSource::Official, progress).await
}
pub async fn upgrade_all_agent_drivers_from(
am: &AgentManager,
source: DownloadSource,
progress: impl Fn(AgentProgressEvent),
) -> Result<UpgradeAllAgentDriversResult, String> {
let registry = fetch_registry_from(source).await?;
let agents = build_agent_list(am, Some(&registry));
let updatable: Vec<&AgentDriverInfo> = agents.iter().filter(|agent| agent.update_available).collect();
let total = updatable.len() as u32;
@ -369,6 +416,7 @@ pub async fn upgrade_all_agent_drivers(
match install_agent_driver_from_registry(
am,
&registry,
source,
&agent.db_type,
&progress,
Some((index + 1) as u32),
@ -439,7 +487,16 @@ pub async fn reinstall_agent_jre(
jre_key: &str,
progress: impl Fn(AgentProgressEvent),
) -> Result<(), String> {
let registry = fetch_registry().await?;
reinstall_agent_jre_from(am, jre_key, DownloadSource::Official, progress).await
}
pub async fn reinstall_agent_jre_from(
am: &AgentManager,
jre_key: &str,
source: DownloadSource,
progress: impl Fn(AgentProgressEvent),
) -> Result<(), String> {
let registry = fetch_registry_from(source).await?;
let jre_info = registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?;
let platform = AgentManager::current_platform();
let platform_jre = jre_info
@ -451,6 +508,7 @@ pub async fn reinstall_agent_jre(
am,
&progress,
"jre",
source,
&platform_jre.url,
&r2_path_with_cache_buster(&github_url_to_r2_path(&platform_jre.url, "jre"), &jre_info.version),
&jre_archive,
@ -497,19 +555,23 @@ pub fn import_agents_from_zip(
async fn install_agent_driver_with_batch(
am: &AgentManager,
db_type: &str,
source: DownloadSource,
progress: &impl Fn(AgentProgressEvent),
current: Option<u32>,
total_drivers: Option<u32>,
) -> Result<(), String> {
match fetch_registry().await {
match fetch_registry_from(source).await {
Ok(registry) => {
match install_agent_driver_from_registry(am, &registry, db_type, progress, current, total_drivers).await {
match install_agent_driver_from_registry(am, &registry, source, db_type, progress, current, total_drivers)
.await
{
Ok(()) => Ok(()),
Err(registry_err) => {
if let Some(local_jar) = find_local_agent_jar(db_type) {
install_local_agent_with_registry_jre(
am,
&registry,
source,
db_type,
local_jar,
progress,
@ -538,6 +600,7 @@ async fn install_agent_driver_with_batch(
async fn ensure_jre_from_registry(
am: &AgentManager,
registry: &AgentRegistry,
source: DownloadSource,
jre_key: &str,
db_type: &str,
progress: &impl Fn(AgentProgressEvent),
@ -560,6 +623,7 @@ async fn ensure_jre_from_registry(
am,
progress,
"jre",
source,
&platform_jre.url,
&r2_path_with_cache_buster(&github_url_to_r2_path(&platform_jre.url, "jre"), &jre_info.version),
&jre_archive,
@ -584,6 +648,7 @@ async fn ensure_jre_from_registry(
async fn install_local_agent_with_registry_jre(
am: &AgentManager,
registry: &AgentRegistry,
source: DownloadSource,
db_type: &str,
local_jar: PathBuf,
progress: &impl Fn(AgentProgressEvent),
@ -592,7 +657,7 @@ async fn install_local_agent_with_registry_jre(
) -> Result<(), String> {
let jre_key = DEFAULT_JRE_KEY;
if jre_needs_install(am, registry, jre_key) {
ensure_jre_from_registry(am, registry, jre_key, db_type, progress, current, total_drivers).await?;
ensure_jre_from_registry(am, registry, source, jre_key, db_type, progress, current, total_drivers).await?;
}
install_local_agent(am, db_type, local_jar)?;
if let Some(jre_info) = registry.resolve_jre(jre_key) {
@ -608,6 +673,7 @@ async fn install_local_agent_with_registry_jre(
async fn install_agent_driver_from_registry(
am: &AgentManager,
registry: &AgentRegistry,
source: DownloadSource,
db_type: &str,
progress: &impl Fn(AgentProgressEvent),
current: Option<u32>,
@ -615,8 +681,17 @@ async fn install_agent_driver_from_registry(
) -> Result<(), String> {
let Some(driver) = agent_registry_driver(registry, db_type) else {
if let Some(local_jar) = find_local_agent_jar(db_type) {
install_local_agent_with_registry_jre(am, registry, db_type, local_jar, progress, current, total_drivers)
.await?;
install_local_agent_with_registry_jre(
am,
registry,
source,
db_type,
local_jar,
progress,
current,
total_drivers,
)
.await?;
return Ok(());
}
return Err(format!("Unknown driver type: {db_type}"));
@ -628,7 +703,7 @@ async fn install_agent_driver_from_registry(
let needs_jre = requires_java_runtime && jre_needs_install(am, registry, jre_key);
if needs_jre {
ensure_jre_from_registry(am, registry, jre_key, db_type, progress, current, total_drivers).await?;
ensure_jre_from_registry(am, registry, source, jre_key, db_type, progress, current, total_drivers).await?;
}
let (artifact, target_path, is_native_artifact) = if let Some(native) = native_artifact {
@ -650,6 +725,7 @@ async fn install_agent_driver_from_registry(
am,
progress,
"driver",
source,
&artifact.url,
&r2_path_with_cache_buster(&github_url_to_r2_path(&artifact.url, "driver"), &driver.version),
&target_path,
@ -706,6 +782,7 @@ async fn download_with_progress(
am: &AgentManager,
progress: &impl Fn(AgentProgressEvent),
step: &str,
source: DownloadSource,
url: &str,
r2_path: &str,
dest: &std::path::Path,
@ -764,6 +841,7 @@ async fn download_with_progress(
let (mut resp, resumed, source_url) = match open_agent_download_response(
&client,
source,
url,
r2_path,
"dbx-agent-manager",
@ -847,6 +925,7 @@ async fn download_with_progress(
async fn open_agent_download_response(
client: &reqwest::Client,
source: DownloadSource,
github_url: &str,
r2_path: &str,
user_agent: &str,
@ -855,7 +934,7 @@ async fn open_agent_download_response(
resume_source: Option<&str>,
) -> Result<(reqwest::Response, bool, String), String> {
let mut errors = Vec::new();
for candidate_url in crate::download_candidate_urls(github_url, r2_path) {
for candidate_url in source.download_candidate_urls(github_url, r2_path)? {
if resume_from > 0 && resume_source.is_some_and(|source| source != candidate_url) {
continue;
}
@ -1448,7 +1527,17 @@ mod agent_registry_install_tests {
write_cached_driver_download(&manager, db_type, version, native_url, &native_path, native_bytes);
let progress = |_| {};
install_agent_driver_from_registry(&manager, &registry, db_type, &progress, None, None).await.unwrap();
install_agent_driver_from_registry(
&manager,
&registry,
DownloadSource::Official,
db_type,
&progress,
None,
None,
)
.await
.unwrap();
assert_eq!(std::fs::read(&native_path).unwrap(), native_bytes);
assert!(!cache_path.exists());
@ -1474,8 +1563,17 @@ mod agent_registry_install_tests {
.unwrap();
let progress = |_| {};
let err =
install_agent_driver_from_registry(&manager, &registry, db_type, &progress, None, None).await.unwrap_err();
let err = install_agent_driver_from_registry(
&manager,
&registry,
DownloadSource::Official,
db_type,
&progress,
None,
None,
)
.await
.unwrap_err();
assert!(err.contains("invalid or corrupt"));
assert!(cache_path.exists());

View File

@ -66,6 +66,43 @@ pub mod update;
pub mod xlsx_export;
pub const R2_CDN_BASE: &str = "https://dl.dbxio.com/";
pub const GITHUB_RELEASE_DOWNLOAD_PREFIX: &str = "https://github.com/t8y2/dbx/releases/download/";
pub const CNB_RELEASE_DOWNLOAD_PREFIX: &str = "https://cnb.cool/dbxio.com/dbx/-/releases/download/";
pub const ATOMGIT_RELEASE_DOWNLOAD_PREFIX: &str = "https://atomgit.com/t8y2/dbx/releases/download/";
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum DownloadSource {
#[default]
Official,
Cnb,
Atomgit,
}
impl DownloadSource {
pub fn download_candidate_urls(self, github_url: &str, r2_path: &str) -> Result<Vec<String>, String> {
match self {
Self::Official => Ok(download_candidate_urls(github_url, r2_path)),
Self::Cnb => Ok(vec![
rewrite_github_release_url(github_url, CNB_RELEASE_DOWNLOAD_PREFIX)?,
format!("{R2_CDN_BASE}{r2_path}"),
]),
Self::Atomgit => Ok(vec![
rewrite_github_release_url(github_url, ATOMGIT_RELEASE_DOWNLOAD_PREFIX)?,
format!("{R2_CDN_BASE}{r2_path}"),
]),
}
}
}
fn rewrite_github_release_url(url: &str, target_prefix: &str) -> Result<String, String> {
if url.starts_with(target_prefix) {
return Ok(url.to_string());
}
url.strip_prefix(GITHUB_RELEASE_DOWNLOAD_PREFIX)
.map(|path| format!("{target_prefix}{path}"))
.ok_or_else(|| format!("Unsupported DBX release download URL: {url}"))
}
pub fn download_candidate_urls(github_url: &str, r2_path: &str) -> Vec<String> {
vec![format!("{R2_CDN_BASE}{r2_path}"), github_url.to_string()]
@ -109,7 +146,7 @@ pub async fn race_download(
#[cfg(test)]
mod tests {
use super::download_candidate_urls;
use super::{download_candidate_urls, DownloadSource};
#[test]
fn download_candidates_exclude_third_party_github_proxy() {
@ -126,4 +163,23 @@ mod tests {
]
);
}
#[test]
fn mirror_download_candidates_rewrite_release_urls() {
let github_url = "https://github.com/t8y2/dbx/releases/download/agents-latest/agent-registry.json";
assert_eq!(
DownloadSource::Cnb.download_candidate_urls(github_url, "agents/agent-registry.json").unwrap(),
vec![
"https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/agent-registry.json",
"https://dl.dbxio.com/agents/agent-registry.json",
]
);
assert_eq!(
DownloadSource::Atomgit.download_candidate_urls(github_url, "agents/agent-registry.json").unwrap(),
vec![
"https://atomgit.com/t8y2/dbx/releases/download/agents-latest/agent-registry.json",
"https://dl.dbxio.com/agents/agent-registry.json",
]
);
}
}

View File

@ -4,13 +4,14 @@ use tauri::{Emitter, State};
use dbx_core::agent_manager::{AgentDriverInfo, DriverStoreUsage, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY};
use dbx_core::agent_service::{
build_agent_list, clear_agent_download_cache, fetch_registry, import_agent_jar,
import_agents_from_zip as import_agents_from_zip_core, install_agent_driver, invalidate_registry_cache,
reinstall_agent_jre, uninstall_agent_driver, uninstall_agent_jre, upgrade_all_agent_drivers, AgentProgressEvent,
UpgradeAllAgentDriversResult,
build_agent_list, clear_agent_download_cache, fetch_registry_from, import_agent_jar,
import_agents_from_zip as import_agents_from_zip_core, install_agent_driver_from, invalidate_registry_cache,
reinstall_agent_jre_from, uninstall_agent_driver, uninstall_agent_jre, upgrade_all_agent_drivers_from,
AgentProgressEvent, UpgradeAllAgentDriversResult,
};
use dbx_core::connection::AppState;
use dbx_core::driver_runtime::DriverRuntimeSummary;
use dbx_core::DownloadSource;
#[derive(Debug, Clone, serde::Serialize)]
pub struct AgentUpdateBlocker {
@ -24,8 +25,11 @@ pub async fn list_installed_agents_local(state: State<'_, Arc<AppState>>) -> Res
}
#[tauri::command]
pub async fn list_installed_agents(state: State<'_, Arc<AppState>>) -> Result<Vec<AgentDriverInfo>, String> {
let registry = fetch_registry().await.ok();
pub async fn list_installed_agents(
state: State<'_, Arc<AppState>>,
source: Option<DownloadSource>,
) -> Result<Vec<AgentDriverInfo>, String> {
let registry = fetch_registry_from(source.unwrap_or_default()).await.ok();
Ok(build_agent_list(&state.agent_manager, registry.as_ref()))
}
@ -64,24 +68,31 @@ pub async fn install_agent(
app: tauri::AppHandle,
state: State<'_, Arc<AppState>>,
db_type: String,
source: Option<DownloadSource>,
) -> Result<(), String> {
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
let app_handle = app.clone();
install_agent_driver(&state.agent_manager, &db_type, move |event| emit_agent_progress(&app_handle, event)).await
install_agent_driver_from(&state.agent_manager, &db_type, source.unwrap_or_default(), move |event| {
emit_agent_progress(&app_handle, event)
})
.await
}
#[tauri::command]
pub async fn upgrade_all_agents(
app: tauri::AppHandle,
state: State<'_, Arc<AppState>>,
source: Option<DownloadSource>,
) -> Result<UpgradeAllAgentDriversResult, String> {
let registry = fetch_registry().await?;
let source = source.unwrap_or_default();
let registry = fetch_registry_from(source).await?;
let agents = build_agent_list(&state.agent_manager, Some(&registry));
let updatable: Vec<String> =
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
ensure_no_agent_update_blockers(state.inner().as_ref(), &updatable).await?;
let app_handle = app.clone();
upgrade_all_agent_drivers(&state.agent_manager, move |event| emit_agent_progress(&app_handle, event)).await
upgrade_all_agent_drivers_from(&state.agent_manager, source, move |event| emit_agent_progress(&app_handle, event))
.await
}
#[tauri::command]
@ -172,10 +183,14 @@ pub async fn reinstall_jre(
app: tauri::AppHandle,
state: State<'_, Arc<AppState>>,
jre_key: Option<String>,
source: Option<DownloadSource>,
) -> Result<(), String> {
let key = jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY);
let app_handle = app.clone();
reinstall_agent_jre(&state.agent_manager, key, move |event| emit_agent_progress(&app_handle, event)).await
reinstall_agent_jre_from(&state.agent_manager, key, source.unwrap_or_default(), move |event| {
emit_agent_progress(&app_handle, event)
})
.await
}
fn emit_agent_progress(app: &tauri::AppHandle, event: AgentProgressEvent) {

View File

@ -12,6 +12,7 @@ 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 R2_LATEST_RELEASE_DOWNLOAD_PREFIX: &str = "https://dl.dbxio.com/releases/latest/";
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 ATOMGIT_RELEASE_DOWNLOAD_PREFIX: &str = "https://atomgit.com/t8y2/dbx/releases/download/";
@ -46,12 +47,18 @@ impl UpdateDownloadSource {
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))])
Ok(vec![
format!("{CNB_RELEASE_DOWNLOAD_PREFIX}{}/latest.json", tag_version(version)),
OFFICIAL_UPDATE_ENDPOINTS[0].to_string(),
])
}
Self::Atomgit => {
let version =
latest_version.ok_or_else(|| "Latest version is required for AtomGit updates.".to_string())?;
Ok(vec![format!("{ATOMGIT_RELEASE_DOWNLOAD_PREFIX}{}/latest.json", tag_version(version))])
Ok(vec![
format!("{ATOMGIT_RELEASE_DOWNLOAD_PREFIX}{}/latest.json", tag_version(version)),
OFFICIAL_UPDATE_ENDPOINTS[0].to_string(),
])
}
}
}
@ -78,6 +85,18 @@ impl UpdateDownloadSource {
Self::Official => None,
}
}
fn r2_fallback_url(&self, url: &str) -> Result<Option<String>, String> {
if matches!(self, Self::Official) || url.starts_with(R2_LATEST_RELEASE_DOWNLOAD_PREFIX) {
return Ok(None);
}
let filename = url
.rsplit('/')
.next()
.filter(|name| !name.is_empty())
.ok_or_else(|| format!("Unsupported update download URL for {} source: {url}", self.label()))?;
Ok(Some(format!("{R2_LATEST_RELEASE_DOWNLOAD_PREFIX}{filename}")))
}
}
fn tag_version(version: &str) -> String {
@ -136,6 +155,12 @@ pub async fn download_and_install_update(
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}"))?;
}
if !update_url_is_available(update.download_url.as_str()).await {
if let Some(fallback_url) = source.r2_fallback_url(update.download_url.as_str())? {
println!("[DBX updater] {} asset unavailable; falling back to R2: {fallback_url}", source.label());
update.download_url = fallback_url.parse().map_err(|e| format!("Invalid R2 update download URL: {e}"))?;
}
}
println!("[DBX updater] downloading from {} URL: {}", source.label(), update.download_url);
let downloaded = Arc::new(AtomicU64::new(0));
@ -159,11 +184,25 @@ pub async fn download_and_install_update(
.map_err(|e| format!("Failed to download and install update: {e}"))
}
async fn update_url_is_available(url: &str) -> bool {
let client = match reqwest::Client::builder().timeout(std::time::Duration::from_secs(10)).build() {
Ok(client) => client,
Err(_) => return false,
};
// Request only the first byte because some release hosts do not implement HEAD consistently.
client
.get(url)
.header(reqwest::header::RANGE, "bytes=0-0")
.send()
.await
.is_ok_and(|response| response.status().is_success())
}
#[cfg(test)]
mod tests {
use super::{
tag_version, UpdateDownloadSource, ATOMGIT_RELEASE_DOWNLOAD_PREFIX, CNB_RELEASE_DOWNLOAD_PREFIX,
OFFICIAL_UPDATE_ENDPOINTS,
OFFICIAL_UPDATE_ENDPOINTS, R2_LATEST_RELEASE_DOWNLOAD_PREFIX,
};
#[test]
@ -181,13 +220,22 @@ mod tests {
#[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")]);
assert_eq!(
endpoints,
vec![format!("{CNB_RELEASE_DOWNLOAD_PREFIX}v0.5.39/latest.json"), OFFICIAL_UPDATE_ENDPOINTS[0].to_string()]
);
}
#[test]
fn builds_atomgit_update_endpoint_for_tag() {
let endpoints = UpdateDownloadSource::Atomgit.endpoints(Some("0.5.44")).unwrap();
assert_eq!(endpoints, vec![format!("{ATOMGIT_RELEASE_DOWNLOAD_PREFIX}v0.5.44/latest.json")]);
assert_eq!(
endpoints,
vec![
format!("{ATOMGIT_RELEASE_DOWNLOAD_PREFIX}v0.5.44/latest.json"),
OFFICIAL_UPDATE_ENDPOINTS[0].to_string(),
]
);
}
#[test]
@ -223,4 +271,12 @@ mod tests {
.unwrap();
assert_eq!(download_url, None);
}
#[test]
fn builds_r2_fallback_for_mirror_asset() {
let fallback = UpdateDownloadSource::Atomgit
.r2_fallback_url("https://atomgit.com/t8y2/dbx/releases/download/v0.5.44/DBX_0.5.44_x64.dmg")
.unwrap();
assert_eq!(fallback, Some(format!("{R2_LATEST_RELEASE_DOWNLOAD_PREFIX}DBX_0.5.44_x64.dmg")));
}
}