diff --git a/docs/app/[lang]/databases/page.tsx b/docs/app/[lang]/databases/page.tsx index 89d2d7b03..ca03b0440 100644 --- a/docs/app/[lang]/databases/page.tsx +++ b/docs/app/[lang]/databases/page.tsx @@ -20,6 +20,8 @@ const databaseSupport = [ { name: "Qdrant", icon: "/icons/database/qdrant.svg", tone: "#dc244c" }, { name: "Milvus", icon: "/icons/database/milvus.png", tone: "#00a1ea" }, { name: "Weaviate", icon: "/icons/database/weaviate.png", tone: "#00b894" }, + { name: "ChromaDB", shortLabel: "CH", tone: "#ff7a59" }, + { name: "Cloudflare D1", shortLabel: "D1", tone: "#f6821f" }, { name: "MariaDB", icon: "/icons/database/mariadb.svg", tone: "#003545" }, { name: "Doris", icon: "/icons/database/doris.svg", tone: "#5b7cfa" }, { name: "StarRocks", icon: "/icons/database/starrocks.svg", tone: "#6750ff" }, @@ -47,6 +49,7 @@ const databaseSupport = [ { name: "Trino", icon: "/icons/database/trino.svg", tone: "#dd00a1" }, { name: "PrestoSQL", icon: "/icons/database/presto.svg", tone: "#5890ff" }, { name: "Hive", icon: "/icons/database/hive.svg", tone: "#fdcb00" }, + { name: "Apache Spark", shortLabel: "SP", tone: "#e25a1c" }, { name: "DB2", icon: "/icons/database/db2.svg", tone: "#054ada" }, { name: "SAP HANA", icon: "/icons/database/saphana.webp", tone: "#008fd3" }, { name: "Teradata", icon: "/icons/database/teradata.webp", tone: "#f37440" }, @@ -57,6 +60,8 @@ const databaseSupport = [ { name: "Neo4j", icon: "/icons/database/neo4j.svg", tone: "#018bff" }, { name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" }, { name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" }, + { name: "Dremio", shortLabel: "DR", tone: "#30bdbe" }, + { name: "OSCAR", shortLabel: "OS", tone: "#1b8dff" }, { name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" }, { name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" }, { name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" }, @@ -72,6 +77,7 @@ const databaseSupport = [ { name: "Etcd", icon: "/icons/database/etcd.svg", tone: "#419eda" }, { name: "ZooKeeper", icon: "/icons/database/zookeeper.svg", tone: "#3b82f6" }, { name: "Pulsar", icon: "/icons/database/pulsar.svg", tone: "#188fff" }, + { name: "Apache Kafka", shortLabel: "KF", tone: "#231f20" }, { name: "Nacos", icon: "/icons/database/nacos.png", tone: "#2f80ed" }, { name: "IRIS", icon: "/icons/database/iris.png", tone: "#0085ca" }, { name: "JDBC", icon: "/icons/database/jdbc.svg", tone: "#6ea8ff" }, @@ -150,7 +156,7 @@ export default async function DatabasesPage({ params }: { params: Promise<{ lang ) : db.icon ? ( ) : ( - {db.name.slice(0, 2).toUpperCase()} + {db.shortLabel ?? db.name.slice(0, 2).toUpperCase()} )} {db.name} diff --git a/docs/app/[lang]/drivers/DriversClient.tsx b/docs/app/[lang]/drivers/DriversClient.tsx index 222670b7a..07545968e 100644 --- a/docs/app/[lang]/drivers/DriversClient.tsx +++ b/docs/app/[lang]/drivers/DriversClient.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { LandingNav } from "@/components/landing/LandingNav"; -import { fetchAgentDownloadCatalog, formatSize, type AgentDownloadCatalog, type JreDisplayEntry, type NativeAgentDisplayEntry, type OfflineBundleEntry } from "@/lib/agentRegistry"; +import { downloadLinksFor, fetchAgentDownloadCatalog, formatSize, type AgentDownloadCatalog, type DownloadSource, type JreDisplayEntry, type NativeAgentDisplayEntry, type OfflineBundleEntry } from "@/lib/agentRegistry"; import { AlertTriangle, Archive, Cpu, Database, Download, Loader2, Plug, Search, Terminal, X } from "lucide-react"; const i18n = { @@ -37,6 +37,14 @@ const i18n = { showing: "Showing", of: "of", clearSearch: "Clear search", + mirrorHint: "CNB and AtomGit mirrors are available for GitHub release assets on mainland China networks.", + downloadSources: "Download source", + sources: { + github: "GitHub", + cnb: "CNB", + atomgit: "AtomGit", + official: "Official", + }, downloadHint: "For air-gapped environments: download the bundle for your platform on an internet-connected machine, then transfer it to the offline machine and import it in DBX from Settings > Driver Manager. Use the driver and JRE tabs only when you need individual artifacts.", }, cn: { @@ -69,6 +77,14 @@ const i18n = { showing: "显示", of: "/", clearSearch: "清空搜索", + mirrorHint: "中国大陆网络可选择 CNB 或 AtomGit 镜像下载,GitHub Release 资源保持同步。", + downloadSources: "下载来源", + sources: { + github: "GitHub", + cnb: "CNB", + atomgit: "AtomGit", + official: "官方下载", + }, downloadHint: "内网环境使用说明:在有网的电脑上下载对应平台的整包,然后传输到内网机器,在 DBX 的“设置 > 驱动管理”中导入。只有需要单个产物时再使用驱动和 JRE 标签页。", }, }; @@ -94,6 +110,27 @@ type NativeAgentGroup = { options: NativeAgentDisplayEntry[]; }; +type DriverTranslations = (typeof i18n)["en"]; + +function DownloadLinks({ url, t }: { url: string; t: DriverTranslations }) { + return ( +
+ {downloadLinksFor(url).map((link) => ( + + + {t.sources[link.source as DownloadSource]} + + ))} +
+ ); +} + function matchesSearch(values: Array, query: string): boolean { if (!query) return true; return values.filter(Boolean).join(" ").toLowerCase().includes(query); @@ -227,6 +264,10 @@ export function DriversClient() { {catalog && !loading && ( <> +
+ + {t.mirrorHint} +
@@ -284,13 +325,13 @@ export function DriversClient() { {activeTab === "jdbcPlugin" && ( <>

{t.jdbcPluginDesc}

- +
- + - @@ -303,13 +344,10 @@ export function DriversClient() {

{t.jdbcPluginInstallHint}

- + - ))} @@ -322,13 +360,13 @@ export function DriversClient() { {activeTab === "bundles" && ( <>

{t.bundlesDesc}

-
{t.jdbcPluginFile}{t.jdbcPluginFile} {t.filename} {t.installMethod} + {t.downloadSources}
{plugin.filename}{plugin.filename} {t.jdbcPluginInstallHint} - - - {t.download} - + +
+
- + - - + @@ -340,13 +378,10 @@ export function DriversClient() { ZIP - - - + + ))} @@ -359,15 +394,15 @@ export function DriversClient() { {activeTab === "drivers" && ( <>

{t.driversDesc}

-
{t.platform}{t.platform} {t.filename}{t.size} + {t.size}{t.downloadSources}
{bundle.filename}{formatSize(bundle.size)} - - - {t.download} - + {bundle.filename}{formatSize(bundle.size)} +
+
- - + + - - + @@ -384,12 +419,9 @@ export function DriversClient() { - - + ))} @@ -402,14 +434,14 @@ export function DriversClient() { {activeTab === "native" && ( <>

{t.nativeAgentsDesc}

-
DriverKeyDriverKey {t.version} {t.requiresJre}{t.size} + {t.size}{t.downloadSources}
{d.version} {d.jre}{formatSize(d.jar.size)} - - - {t.download} - + {formatSize(d.jar.size)} +
+
- + - - + @@ -439,12 +471,9 @@ export function DriversClient() { - - + ); @@ -458,14 +487,14 @@ export function DriversClient() { {activeTab === "jre" && ( <>

{t.jreDesc}

-
AgentAgent {t.platform} {t.version}{t.size} + {t.size}{t.downloadSources}
{group.version}{formatSize(selectedAgent.info.size)} - - - {t.download} - + {formatSize(selectedAgent.info.size)} +
+
- + - - + @@ -483,12 +512,9 @@ export function DriversClient() { JRE {j.jreKey} - - + ); diff --git a/docs/app/[lang]/page.tsx b/docs/app/[lang]/page.tsx index 85073c2be..1da8213d6 100644 --- a/docs/app/[lang]/page.tsx +++ b/docs/app/[lang]/page.tsx @@ -73,6 +73,8 @@ const databaseSupport = [ { name: "Qdrant", icon: "/icons/database/qdrant.svg", tone: "#dc244c" }, { name: "Milvus", icon: "/icons/database/milvus.png", tone: "#00a1ea" }, { name: "Weaviate", icon: "/icons/database/weaviate.png", tone: "#00b894" }, + { name: "ChromaDB", shortLabel: "CH", tone: "#ff7a59" }, + { name: "Cloudflare D1", shortLabel: "D1", tone: "#f6821f" }, { name: "MariaDB", icon: "/icons/database/mariadb.svg", tone: "#003545" }, { name: "Doris", icon: "/icons/database/doris.svg", tone: "#5b7cfa" }, { name: "StarRocks", icon: "/icons/database/starrocks.svg", tone: "#6750ff" }, @@ -100,6 +102,7 @@ const databaseSupport = [ { name: "Trino", icon: "/icons/database/trino.svg", tone: "#dd00a1" }, { name: "PrestoSQL", icon: "/icons/database/presto.svg", tone: "#5890ff" }, { name: "Hive", icon: "/icons/database/hive.svg", tone: "#fdcb00" }, + { name: "Apache Spark", shortLabel: "SP", tone: "#e25a1c" }, { name: "DB2", icon: "/icons/database/db2.svg", tone: "#054ada" }, { name: "SAP HANA", icon: "/icons/database/saphana.webp", tone: "#008fd3" }, { name: "Teradata", icon: "/icons/database/teradata.webp", tone: "#f37440" }, @@ -110,6 +113,8 @@ const databaseSupport = [ { name: "Neo4j", icon: "/icons/database/neo4j.svg", tone: "#018bff" }, { name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" }, { name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" }, + { name: "Dremio", shortLabel: "DR", tone: "#30bdbe" }, + { name: "OSCAR", shortLabel: "OS", tone: "#1b8dff" }, { name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" }, { name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" }, { name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" }, @@ -125,6 +130,7 @@ const databaseSupport = [ { name: "Etcd", icon: "/icons/database/etcd.svg", tone: "#419eda" }, { name: "ZooKeeper", icon: "/icons/database/zookeeper.svg", tone: "#3b82f6" }, { name: "Pulsar", icon: "/icons/database/pulsar.svg", tone: "#188fff" }, + { name: "Apache Kafka", shortLabel: "KF", tone: "#231f20" }, { name: "Nacos", icon: "/icons/database/nacos.png", tone: "#2f80ed" }, { name: "IRIS", icon: "/icons/database/iris.png", tone: "#0085ca" }, { name: "JDBC", icon: "/icons/database/jdbc.svg", tone: "#6ea8ff" }, @@ -549,7 +555,7 @@ export default async function LandingPage({ params }: { params: Promise<{ lang: ) : db.icon ? ( ) : ( - {db.name.slice(0, 2).toUpperCase()} + {db.shortLabel ?? db.name.slice(0, 2).toUpperCase()} )} {db.name} diff --git a/docs/content/docs/databases.cn.mdx b/docs/content/docs/databases.cn.mdx index f5ae9babc..bdcb57fa9 100644 --- a/docs/content/docs/databases.cn.mdx +++ b/docs/content/docs/databases.cn.mdx @@ -12,12 +12,13 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能 | 分组 | 类型 | 说明 | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | 核心 SQL 数据库 | MySQL、PostgreSQL、SQLite、DuckDB、ClickHouse、SQL Server、Oracle、Redshift、DM、GaussDB、KWDB、Teradata、Vertica、Exasol、Firebird、SAP HANA、崖山 YashanDB、GBase | 直接出现在 DBX 连接选择器中 | -| 文档、键值与配置服务 | Redis、MongoDB、Elasticsearch、etcd、Nacos | 根据数据模型使用专门浏览器、管理控制台或查询流程,不完全等同于关系型表格 | +| 文档、键值、向量与配置服务 | Redis、MongoDB、Elasticsearch、Qdrant、Milvus、Weaviate、ChromaDB、etcd、Apache ZooKeeper、Nacos | 根据数据模型使用专门浏览器、管理控制台或查询流程,不完全等同于关系型表格 | | MySQL 兼容类型 | MariaDB、TiDB、OceanBase、Doris、SelectDB、StarRocks、Manticore Search、GoldenDB、TDSQL、PolarDB、GreatSQL、自定义 MySQL | 复用 MySQL 风格连接能力 | | PostgreSQL 兼容类型 | openGauss、KingBase、HighGo、Vastbase、CockroachDB、自定义 PostgreSQL | 复用 PostgreSQL 风格连接能力 | | 文件型数据库 | SQLite、DuckDB、Microsoft Access、RQLite、Turso | 选择本地数据库文件或 HTTP 端点,不填写主机和端口 | | 时序与边缘数据库 | Cloudflare D1、InfluxDB、IoTDB、TDengine、Databend、QuestDB | 托管 SQLite、时序、IoT 或边缘场景 | -| Agent/JDBC 扩展类型 | H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、XuguDB、Databricks、IRIS、JDBC | 功能覆盖取决于对应驱动路径 | +| 分析与消息服务 | Apache Spark、Dremio、Apache Pulsar、Apache Kafka | 用于分布式分析、消息和流管理场景 | +| Agent/JDBC 扩展类型 | H2、Snowflake、Trino、PrestoSQL、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、OSCAR、XuguDB、Databricks、IRIS、JDBC | 功能覆盖取决于对应驱动路径 | ## 默认端口 @@ -31,6 +32,10 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能 | SQL Server | 1433 | | Oracle | 1521 | | Elasticsearch | 9200 | +| Qdrant | 6333 | +| Milvus | 19530 | +| Weaviate | 8080 | +| ChromaDB | 8000 | | TiDB | 4000 | | OceanBase | 2881 | | Doris / SelectDB / StarRocks / Databend | 9030 | @@ -53,6 +58,7 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能 | InfluxDB | 8086 | | IoTDB | 6667 | | etcd | 2379 | +| Apache ZooKeeper | 2181 | | Nacos | 8848 | | Nacos 控制台/admin API | 8085 | | IRIS | 1972 | @@ -62,16 +68,21 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能 | Trino | 8080 | | PrestoSQL | 8080 | | Hive | 10000 | +| Apache Spark | 10015 | | DB2 | 50000 | | Informix | 9088 | +| Dremio | 31010 | | Neo4j | 7687 | | Cassandra | 9042 | | BigQuery | 443 | | Kylin | 7070 | | SunDB | 22000 | +| OSCAR | 2003 | | TDengine | 6041 | | XuguDB | 5138 | | QuestDB | 8812 | +| Apache Pulsar Admin API | 8080 | +| Apache Kafka | 9092 | SQLite、DuckDB、Access 和 JDBC 在不需要网络端口时,会在连接模型中保存为端口 `0`。 diff --git a/docs/content/docs/databases.mdx b/docs/content/docs/databases.mdx index ca77869a3..8ad5147a8 100644 --- a/docs/content/docs/databases.mdx +++ b/docs/content/docs/databases.mdx @@ -12,12 +12,13 @@ The connection dialog exposes ready-to-use profiles with default ports and drive | Group | Profiles | Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Core SQL engines | MySQL, PostgreSQL, SQLite, DuckDB, ClickHouse, SQL Server, Oracle, Redshift, DM, GaussDB, KWDB, Teradata, Vertica, Exasol, Firebird, SAP HANA, YashanDB, GBase | Available directly in the DBX connection picker | -| Document, key-value, and config services | Redis, MongoDB, Elasticsearch, etcd, Nacos | Use dedicated browsers, admin consoles, or query flows instead of the generic relational grid where appropriate | +| Document, key-value, vector, and config services | Redis, MongoDB, Elasticsearch, Qdrant, Milvus, Weaviate, ChromaDB, etcd, Apache ZooKeeper, Nacos | Use dedicated browsers, admin consoles, or query flows instead of the generic relational grid where appropriate | | MySQL-compatible profiles | MariaDB, TiDB, OceanBase, Doris, SelectDB, StarRocks, Manticore Search, GoldenDB, TDSQL, PolarDB, GreatSQL, custom MySQL | Reuse MySQL-style connection handling where the engine speaks a compatible protocol | | PostgreSQL-compatible profiles | openGauss, KingBase, HighGo, Vastbase, CockroachDB, custom PostgreSQL | Reuse PostgreSQL-style connection handling where the engine speaks a compatible protocol | | File-based engines | SQLite, DuckDB, Microsoft Access, RQLite, Turso | Choose a local database file or HTTP endpoint instead of host and port | | Time-series and edge | Cloudflare D1, InfluxDB, IoTDB, TDengine, Databend, QuestDB | Managed SQLite, time-series, IoT, or edge workloads | -| Agent/JDBC-oriented engines | H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, XuguDB, Databricks, IRIS, JDBC | Feature coverage depends on the driver path used by that engine | +| Analytics and messaging services | Apache Spark, Dremio, Apache Pulsar, Apache Kafka | Distributed analytics, messaging, and stream-management workloads | +| Agent/JDBC-oriented engines | H2, Snowflake, Trino, PrestoSQL, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, OSCAR, XuguDB, Databricks, IRIS, JDBC | Feature coverage depends on the driver path used by that engine | ## Default Ports @@ -31,6 +32,10 @@ The connection dialog exposes ready-to-use profiles with default ports and drive | SQL Server | 1433 | | Oracle | 1521 | | Elasticsearch | 9200 | +| Qdrant | 6333 | +| Milvus | 19530 | +| Weaviate | 8080 | +| ChromaDB | 8000 | | TiDB | 4000 | | OceanBase | 2881 | | Doris / SelectDB / StarRocks / Databend | 9030 | @@ -53,6 +58,7 @@ The connection dialog exposes ready-to-use profiles with default ports and drive | InfluxDB | 8086 | | IoTDB | 6667 | | etcd | 2379 | +| Apache ZooKeeper | 2181 | | Nacos | 8848 | | Nacos console/admin API | 8085 | | IRIS | 1972 | @@ -62,16 +68,21 @@ The connection dialog exposes ready-to-use profiles with default ports and drive | Trino | 8080 | | PrestoSQL | 8080 | | Hive | 10000 | +| Apache Spark | 10015 | | DB2 | 50000 | | Informix | 9088 | +| Dremio | 31010 | | Neo4j | 7687 | | Cassandra | 9042 | | BigQuery | 443 | | Kylin | 7070 | | SunDB | 22000 | +| OSCAR | 2003 | | TDengine | 6041 | | XuguDB | 5138 | | QuestDB | 8812 | +| Apache Pulsar Admin API | 8080 | +| Apache Kafka | 9092 | SQLite, DuckDB, Access, and JDBC profiles use port `0` in the saved connection model when a network port is not part of the connection. diff --git a/docs/lib/agentRegistry.test.ts b/docs/lib/agentRegistry.test.ts index ec6b4cf94..9370d41ff 100644 --- a/docs/lib/agentRegistry.test.ts +++ b/docs/lib/agentRegistry.test.ts @@ -1,6 +1,10 @@ import assert from "node:assert/strict"; -import { test } from "vitest"; -import { buildAgentDownloadCatalog } from "./agentRegistry"; +import { afterEach, test, vi } from "vitest"; +import { buildAgentDownloadCatalog, downloadLinksFor, fetchAgentDownloadCatalog, formatSize } from "./agentRegistry"; + +afterEach(() => { + vi.restoreAllMocks(); +}); test("offline download catalog includes the JDBC plugin ZIP", () => { const catalog = buildAgentDownloadCatalog([]); @@ -11,3 +15,53 @@ test("offline download catalog includes the JDBC plugin ZIP", () => { url: "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip", }); }); + +test("release assets expose GitHub, CNB, and AtomGit download links", () => { + assert.deepEqual(downloadLinksFor("https://github.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip"), [ + { source: "github", url: "https://github.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" }, + { source: "cnb", url: "https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" }, + { source: "atomgit", url: "https://atomgit.com/t8y2/dbx/releases/download/agents-latest/dbx-agents-offline-macos-aarch64.zip" }, + ]); +}); + +test("non-release assets retain their official download link", () => { + assert.deepEqual(downloadLinksFor("https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip"), [ + { source: "official", url: "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip" }, + ]); +}); + +test("catalog falls back from GitHub to CNB and then AtomGit", async () => { + const requestedUrls: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = String(input); + requestedUrls.push(url); + if (url.includes("api.github.com")) return new Response("rate limited", { status: 403 }); + if (url.includes("cnb.cool")) throw new TypeError("CORS blocked"); + return Response.json({ + tag_name: "agents-latest", + assets: [ + { name: "dbx-agent-access.jar", type: "attach" }, + { name: "dbx-jre-21-macos-aarch64.tar.gz", type: "attach" }, + { name: "dbx-agents-offline-macos-aarch64.zip", type: "attach" }, + ], + }); + }), + ); + + const catalog = await fetchAgentDownloadCatalog(); + + assert.deepEqual(requestedUrls, [ + "https://api.github.com/repos/t8y2/dbx/releases/tags/agents-latest", + "https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/agent-registry.json", + "https://api.atomgit.com/api/v5/repos/t8y2/dbx/releases/agents-latest", + ]); + assert.equal(catalog?.drivers[0]?.key, "access"); + assert.equal(catalog?.jres[0]?.platformKey, "macos-aarch64"); + assert.equal(catalog?.bundles[0]?.platformKey, "macos-aarch64"); +}); + +test("unknown fallback asset sizes render as unavailable", () => { + assert.equal(formatSize(0), "—"); +}); diff --git a/docs/lib/agentRegistry.ts b/docs/lib/agentRegistry.ts index 223d526a2..78fcf01bd 100644 --- a/docs/lib/agentRegistry.ts +++ b/docs/lib/agentRegistry.ts @@ -5,6 +5,13 @@ export interface ArtifactInfo { size: number; } +export type DownloadSource = "github" | "cnb" | "atomgit" | "official"; + +export interface DownloadLink { + source: DownloadSource; + url: string; +} + export interface GitHubReleaseAsset { name: string; size: number; @@ -15,6 +22,26 @@ interface GitHubRelease { assets?: GitHubReleaseAsset[]; } +interface AgentRegistryArtifact { + url: string; + size: number; +} + +interface AgentRegistryDriver { + jar?: AgentRegistryArtifact; + native?: Record; +} + +interface AgentRegistry { + jres?: Record }>; + drivers?: Record; +} + +interface AtomGitRelease { + tag_name?: string; + assets?: Array<{ name?: string; type?: string }>; +} + export interface OfflineBundleEntry { platformKey: string; platformLabel: string; @@ -65,7 +92,12 @@ export interface AgentDownloadCatalog { } const AGENTS_LATEST_RELEASE_API_URL = "https://api.github.com/repos/t8y2/dbx/releases/tags/agents-latest"; +const CNB_AGENT_REGISTRY_URL = "https://cnb.cool/dbxio.com/dbx/-/releases/download/agents-latest/agent-registry.json"; +const ATOMGIT_LATEST_RELEASE_API_URL = "https://api.atomgit.com/api/v5/repos/t8y2/dbx/releases/agents-latest"; const JDBC_PLUGIN_DOWNLOAD_URL = "https://dl.dbxio.com/releases/latest/dbx-jdbc-plugin-latest.zip"; +const GITHUB_RELEASE_DOWNLOAD_PREFIX = "https://github.com/t8y2/dbx/releases/download/"; +const CNB_RELEASE_DOWNLOAD_PREFIX = "https://cnb.cool/dbxio.com/dbx/-/releases/download/"; +const ATOMGIT_RELEASE_DOWNLOAD_PREFIX = "https://atomgit.com/t8y2/dbx/releases/download/"; const MIN_APP_VERSION = "0.6.0"; const driverVersionMap = driverVersions as Record; const nativeDriverKeys = new Set(["oracle", "xugu"]); @@ -136,23 +168,100 @@ function assetInfo(asset: GitHubReleaseAsset): ArtifactInfo { }; } +function releaseAssetName(url: string, fallback: string): string { + try { + return decodeURIComponent(new URL(url).pathname.split("/").pop() || fallback); + } catch { + return fallback; + } +} + +function githubReleaseAsset(name: string, size: number, tag = "agents-latest"): GitHubReleaseAsset { + return { + name, + size, + browser_download_url: `${GITHUB_RELEASE_DOWNLOAD_PREFIX}${tag}/${name}`, + }; +} + +function registryReleaseAssets(registry: AgentRegistry): GitHubReleaseAsset[] { + const assets = new Map(); + const addArtifact = (artifact: AgentRegistryArtifact | undefined, fallbackName: string) => { + if (!artifact?.url) return; + const name = releaseAssetName(artifact.url, fallbackName); + assets.set(name, { name, size: artifact.size || 0, browser_download_url: artifact.url }); + }; + + for (const [jreKey, jre] of Object.entries(registry.jres ?? {})) { + for (const [platformKey, artifact] of Object.entries(jre.platforms ?? {})) { + addArtifact(artifact, `dbx-jre-${jreKey}-${platformKey}.tar.gz`); + } + } + + for (const [driverKey, driver] of Object.entries(registry.drivers ?? {})) { + addArtifact(driver.jar, `dbx-agent-${driverKey}.jar`); + for (const [platformKey, artifact] of Object.entries(driver.native ?? {})) { + addArtifact(artifact, `dbx-agent-${driverKey}-${platformKey}`); + } + } + + for (const platformKey of Object.keys(registry.jres?.["21"]?.platforms ?? {})) { + const name = `dbx-agents-offline-${platformKey}.zip`; + assets.set(name, githubReleaseAsset(name, 0)); + } + + return Array.from(assets.values()); +} + +function atomGitReleaseAssets(release: AtomGitRelease): GitHubReleaseAsset[] { + const tag = release.tag_name || "agents-latest"; + return (release.assets ?? []) + .filter((asset): asset is { name: string; type?: string } => Boolean(asset.name) && asset.type !== "source") + .map((asset) => githubReleaseAsset(asset.name, 0, tag)); +} + +function hasDownloadAssets(catalog: AgentDownloadCatalog): boolean { + return catalog.bundles.length > 0 || catalog.drivers.length > 0 || catalog.jres.length > 0 || catalog.nativeAgents.length > 0; +} + +async function fetchJson(url: string, headers?: HeadersInit): Promise { + const response = await fetch(url, { cache: "no-store", headers }); + if (!response.ok) throw new Error(`Download catalog request failed with ${response.status}`); + return response.json() as Promise; +} + +export function downloadLinksFor(url: string): DownloadLink[] { + const releasePath = url.startsWith(GITHUB_RELEASE_DOWNLOAD_PREFIX) ? url.slice(GITHUB_RELEASE_DOWNLOAD_PREFIX.length) : null; + if (!releasePath) return [{ source: "official", url }]; + + return [ + { source: "github", url }, + { source: "cnb", url: `${CNB_RELEASE_DOWNLOAD_PREFIX}${releasePath}` }, + { source: "atomgit", url: `${ATOMGIT_RELEASE_DOWNLOAD_PREFIX}${releasePath}` }, + ]; +} + function assetMap(assets: GitHubReleaseAsset[]): Map { return new Map(assets.map((asset) => [asset.name, asset])); } export async function fetchAgentDownloadCatalog(): Promise { - try { - const res = await fetch(AGENTS_LATEST_RELEASE_API_URL, { - cache: "no-store", - headers: { Accept: "application/vnd.github+json" }, - }); - if (!res.ok) return null; + const loaders: Array<() => Promise> = [ + async () => (await fetchJson(AGENTS_LATEST_RELEASE_API_URL, { Accept: "application/vnd.github+json" })).assets ?? [], + async () => registryReleaseAssets(await fetchJson(CNB_AGENT_REGISTRY_URL)), + async () => atomGitReleaseAssets(await fetchJson(ATOMGIT_LATEST_RELEASE_API_URL, { Accept: "application/json" })), + ]; - const release = (await res.json()) as GitHubRelease; - return buildAgentDownloadCatalog(release.assets ?? []); - } catch { - return null; + for (const loadAssets of loaders) { + try { + const catalog = buildAgentDownloadCatalog(await loadAssets()); + if (hasDownloadAssets(catalog)) return catalog; + } catch { + // Try the next synchronized release source. + } } + + return null; } export function buildAgentDownloadCatalog(assets: GitHubReleaseAsset[]): AgentDownloadCatalog { @@ -255,6 +364,7 @@ export function buildOfflineBundleEntries(assets: GitHubReleaseAsset[]): Offline } export function formatSize(bytes: number): string { + if (bytes <= 0) return "—"; if (bytes >= 1024 * 1024 * 1024) { return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; }
{t.platform}{t.platform} JRE {t.version}{t.size} + {t.size}{t.downloadSources}
{j.jreVersion}{formatSize(j.info.size)} - - - {t.download} - + {formatSize(j.info.size)} +