mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(web): embed static dist and add win7 static browser release
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: 脚本所在目录(等价于 ROOT)
|
||||
set ROOT=%~dp0
|
||||
:: 去掉末尾反斜杠
|
||||
set ROOT=%ROOT:~0,-1%
|
||||
|
||||
set DBX_PACKAGE_ROOT=%ROOT%
|
||||
|
||||
:: static dir
|
||||
if "%DBX_STATIC_DIR%"=="" (
|
||||
:: dist静态资源文件已直接嵌入到二进制文件中,环境变量中没有设置的话,直接用二进制中的静态文件
|
||||
rem set DBX_STATIC_DIR=%ROOT%\dist
|
||||
)
|
||||
:: data dir
|
||||
if "%DBX_DATA_DIR%"=="" (
|
||||
set DBX_DATA_DIR=%ROOT%\data
|
||||
)
|
||||
|
||||
:: port
|
||||
if "%DBX_PORT%"=="" (
|
||||
set DBX_PORT=4224
|
||||
)
|
||||
|
||||
:: base path
|
||||
set BASE_PATH=%DBX_PUBLIC_BASE_PATH%
|
||||
if "%BASE_PATH%"=="" set BASE_PATH=/
|
||||
|
||||
:: 如果不是以 / 开头,补上 /
|
||||
echo %BASE_PATH% | findstr /r "^/" >nul || (
|
||||
set BASE_PATH=/!BASE_PATH!
|
||||
)
|
||||
|
||||
echo DBX_DATA_DIR=%DBX_DATA_DIR%
|
||||
echo DBX browser UI: http://127.0.0.1:%DBX_PORT%%BASE_PATH%
|
||||
|
||||
cd /d "%ROOT%"
|
||||
|
||||
:: 执行程序
|
||||
dbx-web.exe %*
|
||||
@@ -703,6 +703,89 @@ jobs:
|
||||
Remove-Item -LiteralPath $workDir -Recurse -Force
|
||||
}
|
||||
|
||||
build-windows-7-static-browser:
|
||||
needs: release-ready
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-win7-windows-msvc
|
||||
arch: x86_64
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Rust for Windows 7
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly-2026-07-22
|
||||
components: rust-src
|
||||
|
||||
- name: Install NASM
|
||||
run: choco install nasm -y --no-progress
|
||||
|
||||
- name: Add NASM to PATH
|
||||
shell: pwsh
|
||||
run: Add-Content $env:GITHUB_PATH "C:\Program Files\NASM"
|
||||
|
||||
- name: Build DBX for Windows 7 ${{ matrix.arch }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
cargo build --locked --package dbx-web --release --no-default-features --features embed-static,duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled --target ${{ matrix.target }} -Z build-std=std,panic_abort
|
||||
|
||||
- name: Audit Windows 7 PE imports
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/${{ matrix.target }}/release/dbx-web.exe
|
||||
|
||||
- name: Upload Windows portable ${{ matrix.arch }} ZIP
|
||||
timeout-minutes: 20
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
$version = "${env:GITHUB_REF_NAME}".TrimStart("v")
|
||||
$arch = "${{ matrix.arch }}"
|
||||
$portableRoot = "portable"
|
||||
$portableDir = Join-Path $portableRoot "DBX_${version}_${arch}"
|
||||
$zipName = "dbx-web_${version}_${arch}-browser-static.zip"
|
||||
$zipSha256 = "dbx-web_${version}_${arch}-browser-static.zip.sha256"
|
||||
$exePath = "target/${{ matrix.target }}/release/dbx-web.exe"
|
||||
|
||||
if (!(Test-Path $exePath)) {
|
||||
Write-Error "Missing Windows executable: $exePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $portableDir | Out-Null
|
||||
Copy-Item $exePath (Join-Path $portableDir "dbx-web.exe") -Force
|
||||
Copy-Item "LICENSE" (Join-Path $portableDir "LICENSE") -Force
|
||||
Copy-Item "README.md" (Join-Path $portableDir "README.md") -Force
|
||||
Copy-Item ".github\scripts\start-dbx-web.bat" (Join-Path $portableDir "start.bat") -Force
|
||||
|
||||
Compress-Archive -Path (Join-Path $portableDir "*") -DestinationPath $zipName -Force
|
||||
|
||||
$zip_sha256 = (Get-FileHash -LiteralPath $zipName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
Set-Content -Path $zipSha256 -Value $zip_sha256 -Encoding utf8NoBOM
|
||||
|
||||
gh release upload "${env:GITHUB_REF_NAME}" $zipName $zipSha256 --repo "${env:GITHUB_REPOSITORY}" --clobber
|
||||
|
||||
static-browser:
|
||||
# Fully static musl builds of the browser (dbx-web) variant. No glibc
|
||||
# dependency, so the tarball runs on any Linux distribution (verified in
|
||||
@@ -799,7 +882,7 @@ jobs:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
run: |
|
||||
cargo zigbuild --release -p dbx-web --target ${{ matrix.target }} \
|
||||
--no-default-features --features "duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled"
|
||||
--no-default-features --features "embed-static,duckdb-sidecar,dynamodb,mq-admin,dbx-core/sqlite-bundled"
|
||||
|
||||
- name: Package static browser runtime
|
||||
env:
|
||||
@@ -938,7 +1021,8 @@ jobs:
|
||||
|
||||
publish:
|
||||
# Keep the release as a draft until every release-blocking platform job passes.
|
||||
# Nix packaging is advisory and runs independently from publication.
|
||||
# Nix packaging and the Windows 7 static-browser build are advisory and
|
||||
# upload their release assets independently of publication.
|
||||
needs: [cleanup-release-signatures, docker-manifest, jdbc-plugin, static-browser]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
Generated
+37
@@ -2408,9 +2408,11 @@ dependencies = [
|
||||
"dbx-mcp",
|
||||
"futures",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"pbkdf2 0.12.2",
|
||||
"redis",
|
||||
"reqwest 0.12.28",
|
||||
"rust-embed",
|
||||
"rustls 0.23.43",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7654,6 +7656,41 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
|
||||
dependencies = [
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.119",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
|
||||
dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
|
||||
@@ -15,6 +15,10 @@ dynamodb = ["dbx-core/dynamodb"]
|
||||
mq-admin = ["dbx-core/mq-admin", "dbx-mcp/mq-admin"]
|
||||
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
|
||||
system-fonts = ["dbx-core/system-fonts"]
|
||||
# Embed the frontend build output (../../dist) into the binary. When enabled
|
||||
# and DBX_STATIC_DIR is not set, the embedded files are served by default.
|
||||
# Requires the frontend to have been built before invoking cargo.
|
||||
embed-static = ["dep:rust-embed", "dep:mime_guess"]
|
||||
|
||||
[dependencies]
|
||||
dbx-core = { path = "../dbx-core", default-features = false, features = ["openapi"] }
|
||||
@@ -42,6 +46,9 @@ base64 = "0.22"
|
||||
pbkdf2 = "0.12"
|
||||
sha2 = "0.10"
|
||||
tempfile = "3"
|
||||
rust-embed = { version = "8", features = ["debug-embed"], optional = true }
|
||||
mime_guess = { version = "2", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["multipart"] }
|
||||
|
||||
+126
-16
@@ -159,12 +159,96 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn mount_public_base_path(mut app: Router, public_base_path: &str, static_dir: Option<&std::path::Path>) -> Router {
|
||||
if let Some(static_dir) = static_dir {
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
let index_path = static_dir.join("index.html");
|
||||
let serve_dir = ServeDir::new(static_dir).not_found_service(ServeFile::new(index_path));
|
||||
app = app.fallback_service(serve_dir);
|
||||
/// Frontend build output compiled into the binary by the `embed-static` feature.
|
||||
/// The folder is relative to this crate's manifest (crates/dbx-web) and points
|
||||
/// at the workspace root `dist` directory produced by the frontend build.
|
||||
#[cfg(feature = "embed-static")]
|
||||
#[derive(rust_embed::RustEmbed)]
|
||||
#[folder = "../../dist"]
|
||||
struct EmbeddedStaticAssets;
|
||||
|
||||
/// Serves an embedded static asset, falling back to `index.html` so that
|
||||
/// client-side (SPA) routes resolve like they do with the disk-based ServeDir.
|
||||
#[cfg(feature = "embed-static")]
|
||||
async fn serve_embedded_asset(uri: Uri) -> axum::response::Response {
|
||||
use axum::http::header;
|
||||
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
if let Some(asset) = EmbeddedStaticAssets::get(path) {
|
||||
let content_type = mime_guess::from_path(path).first_or_octet_stream();
|
||||
let mut response = axum::response::Response::builder()
|
||||
.header(header::CONTENT_TYPE, content_type.as_ref())
|
||||
.body(axum::body::Body::from(asset.data.into_owned()))
|
||||
.expect("valid embedded asset response");
|
||||
// Vite emits content-hashed, immutable files under assets/. The HTML
|
||||
// entry point must always revalidate so binary upgrades take effect;
|
||||
// other files (favicon, fonts) keep browser heuristic caching, which
|
||||
// matches tower-http's ServeDir behaviour.
|
||||
let cache_control = if path.starts_with("assets/") {
|
||||
Some("public, max-age=31536000, immutable")
|
||||
} else if path == "index.html" {
|
||||
Some("no-cache")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(cache_control) = cache_control {
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
header::HeaderValue::from_str(cache_control).expect("valid cache-control"),
|
||||
);
|
||||
}
|
||||
response
|
||||
} else {
|
||||
serve_embedded_index().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves the embedded SPA entry point. It must never be cached aggressively so
|
||||
/// that upgraded binaries are not shadowed by an old index referencing removed
|
||||
/// hashed assets.
|
||||
#[cfg(feature = "embed-static")]
|
||||
async fn serve_embedded_index() -> axum::response::Response {
|
||||
use axum::http::header;
|
||||
|
||||
let Some(index) = EmbeddedStaticAssets::get("index.html") else {
|
||||
// Mirrors the disk-based variant, where a missing index.html surfaces
|
||||
// as a 404 from the not-found service instead of crashing the handler.
|
||||
return axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::NOT_FOUND)
|
||||
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
|
||||
.body(axum::body::Body::from("embedded dist does not contain index.html"))
|
||||
.expect("valid missing index response");
|
||||
};
|
||||
axum::response::Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
|
||||
.header(header::CACHE_CONTROL, "no-cache")
|
||||
.body(axum::body::Body::from(index.data.into_owned()))
|
||||
.expect("valid embedded index response")
|
||||
}
|
||||
|
||||
/// Selects where the static frontend files are served from.
|
||||
#[derive(Clone, Copy)]
|
||||
enum StaticSource<'a> {
|
||||
/// Serve files from a directory on disk (DBX_STATIC_DIR).
|
||||
Dir(&'a std::path::Path),
|
||||
/// Serve the dist files compiled into the binary (`embed-static` feature).
|
||||
#[cfg(feature = "embed-static")]
|
||||
Embedded,
|
||||
}
|
||||
|
||||
fn mount_static_assets(mut app: Router, public_base_path: &str, source: Option<StaticSource<'_>>) -> Router {
|
||||
match source {
|
||||
Some(StaticSource::Dir(static_dir)) => {
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
let index_path = static_dir.join("index.html");
|
||||
let serve_dir = ServeDir::new(static_dir).not_found_service(ServeFile::new(index_path));
|
||||
app = app.fallback_service(serve_dir);
|
||||
}
|
||||
#[cfg(feature = "embed-static")]
|
||||
Some(StaticSource::Embedded) => {
|
||||
app = app.fallback(serve_embedded_asset);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
if public_base_path == "/" {
|
||||
@@ -173,9 +257,16 @@ fn mount_public_base_path(mut app: Router, public_base_path: &str, static_dir: O
|
||||
|
||||
app = Router::new().nest(public_base_path, app);
|
||||
app = add_public_base_path_redirect(app, public_base_path);
|
||||
if let Some(static_dir) = static_dir {
|
||||
use tower_http::services::ServeFile;
|
||||
app = app.route_service(&format!("{public_base_path}/"), ServeFile::new(static_dir.join("index.html")));
|
||||
match source {
|
||||
Some(StaticSource::Dir(static_dir)) => {
|
||||
use tower_http::services::ServeFile;
|
||||
app = app.route_service(&format!("{public_base_path}/"), ServeFile::new(static_dir.join("index.html")));
|
||||
}
|
||||
#[cfg(feature = "embed-static")]
|
||||
Some(StaticSource::Embedded) => {
|
||||
app = app.route(&format!("{public_base_path}/"), get(serve_embedded_index));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
app
|
||||
}
|
||||
@@ -1278,7 +1369,23 @@ async fn serve() {
|
||||
}
|
||||
|
||||
let static_dir = std::env::var_os("DBX_STATIC_DIR").map(std::path::PathBuf::from);
|
||||
app = mount_public_base_path(app, &public_base_path, static_dir.as_deref());
|
||||
// DBX_STATIC_DIR always wins (frontend development); otherwise serve the
|
||||
// assets embedded into the binary when the embed-static feature is enabled.
|
||||
let static_source = {
|
||||
let dir_source = static_dir.as_deref().map(StaticSource::Dir);
|
||||
#[cfg(feature = "embed-static")]
|
||||
{
|
||||
dir_source.or_else(|| {
|
||||
tracing::info!("Serving embedded frontend assets (DBX_STATIC_DIR not set)");
|
||||
Some(StaticSource::Embedded)
|
||||
})
|
||||
}
|
||||
#[cfg(not(feature = "embed-static"))]
|
||||
{
|
||||
dir_source
|
||||
}
|
||||
};
|
||||
app = mount_static_assets(app, &public_base_path, static_source);
|
||||
|
||||
// Bind address
|
||||
let port: u16 = std::env::var("DBX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(4224);
|
||||
@@ -1320,8 +1427,8 @@ async fn serve() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
mount_public_base_path, normalize_public_base_path, web_agent_dir_from_env, web_body_limit_bytes_from_value,
|
||||
web_compression_predicate, XLSX_CONTENT_TYPE,
|
||||
mount_static_assets, normalize_public_base_path, web_agent_dir_from_env, web_body_limit_bytes_from_value,
|
||||
web_compression_predicate, StaticSource, XLSX_CONTENT_TYPE,
|
||||
};
|
||||
use crate::routes::table_import;
|
||||
use axum::body::Body;
|
||||
@@ -1493,10 +1600,10 @@ mod tests {
|
||||
std::fs::write(static_dir.join("app.js"), "subpath asset").expect("write asset");
|
||||
|
||||
for public_base_path in ["/dbx", "/xxxx/rsu"] {
|
||||
let router = mount_public_base_path(
|
||||
let router = mount_static_assets(
|
||||
Router::new().route("/api/ping", get(|| async { "pong" })),
|
||||
public_base_path,
|
||||
Some(&static_dir),
|
||||
Some(StaticSource::Dir(&static_dir)),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind test listener");
|
||||
let address = listener.local_addr().expect("test listener address");
|
||||
@@ -1559,8 +1666,11 @@ mod tests {
|
||||
std::fs::create_dir_all(&static_dir).expect("create static directory");
|
||||
std::fs::write(static_dir.join("index.html"), "root index").expect("write index");
|
||||
std::fs::write(static_dir.join("app.js"), "root asset").expect("write asset");
|
||||
let router =
|
||||
mount_public_base_path(Router::new().route("/api/ping", get(|| async { "pong" })), "/", Some(&static_dir));
|
||||
let router = mount_static_assets(
|
||||
Router::new().route("/api/ping", get(|| async { "pong" })),
|
||||
"/",
|
||||
Some(StaticSource::Dir(&static_dir)),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind test listener");
|
||||
let address = listener.local_addr().expect("test listener address");
|
||||
let server = tokio::spawn(async move {
|
||||
|
||||
@@ -31,10 +31,6 @@ if [ ! -x "$binary" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${dist_dir}/index.html" ]; then
|
||||
echo "missing frontend build output: ${dist_dir}/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if readelf -l "$binary" | grep -q 'Requesting program interpreter'; then
|
||||
readelf -l "$binary" | grep 'Requesting program interpreter' >&2 || true
|
||||
@@ -51,12 +47,11 @@ fi
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p \
|
||||
"$package_dir/bin" \
|
||||
"$package_dir/dist" \
|
||||
"$package_dir/data"
|
||||
|
||||
cp "$binary" "$package_dir/bin/dbx-web-bin"
|
||||
chmod +x "$package_dir/bin/dbx-web-bin"
|
||||
cp -a "${dist_dir}/." "$package_dir/dist/"
|
||||
#cp -a "${dist_dir}/." "$package_dir/dist/"
|
||||
|
||||
cat > "$package_dir/dbx" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
@@ -69,7 +64,7 @@ while [ -h "$SOURCE" ]; do
|
||||
done
|
||||
ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
||||
export DBX_PACKAGE_ROOT="$ROOT"
|
||||
export DBX_STATIC_DIR="${DBX_STATIC_DIR:-$ROOT/dist}"
|
||||
#export DBX_STATIC_DIR="${DBX_STATIC_DIR:-$ROOT/dist}"
|
||||
export DBX_DATA_DIR="${DBX_DATA_DIR:-$ROOT/data}"
|
||||
port="${DBX_PORT:-4224}"
|
||||
base_path="${DBX_PUBLIC_BASE_PATH:-/}"
|
||||
|
||||
@@ -38,8 +38,7 @@ tar -xzf "$package_tarball" -C "$runtime_dir" --strip-components=1
|
||||
|
||||
for required_runtime_path in \
|
||||
dbx \
|
||||
bin/dbx-web-bin \
|
||||
dist/index.html; do
|
||||
bin/dbx-web-bin ; do
|
||||
if [ ! -e "$runtime_dir/$required_runtime_path" ]; then
|
||||
echo "missing packaged static web runtime path: $required_runtime_path" >&2
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user