From 42a2df33dd45f74b59a693777a7b0be550533956 Mon Sep 17 00:00:00 2001 From: Sunil Thakares Date: Sat, 13 Jun 2026 21:47:14 +0530 Subject: [PATCH] Add root landing page support and package static assets --- Dockerfile | 29 ++-- src/web/pages.rs | 44 ++++++ src/web/routes.rs | 2 + tests/root_landing_tests.rs | 43 ++++++ www/index.html | 281 +++++++++++++++--------------------- 5 files changed, 220 insertions(+), 179 deletions(-) create mode 100644 tests/root_landing_tests.rs diff --git a/Dockerfile b/Dockerfile index c84912d..ee5c44d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ # ========================================== # Stage 1: Build # ========================================== -# FROM rust:1.82-slim-bookworm AS builder FROM rust:1.89-bookworm AS builder WORKDIR /app @@ -13,45 +12,51 @@ RUN apt-get update && apt-get install -y \ git \ && rm -rf /var/lib/apt/lists/* -# Copy configuration files -# COPY Cargo.toml ./ +# Copy Cargo metadata COPY Cargo.toml Cargo.lock ./ -# Pre-build dependencies to cache them +# Pre-build dependencies for layer caching RUN mkdir src && echo "fn main() {}" > src/main.rs RUN cargo build --release RUN rm -rf src -# Copy source and templates +# Copy application source COPY src ./src COPY templates ./templates +COPY www ./www -# Trigger rebuilding with actual source +# Build application RUN touch src/main.rs RUN cargo build --release # ========================================== -# Stage 2: Runner +# Stage 2: Runtime # ========================================== FROM debian:bookworm-slim WORKDIR /app -# Install runtime dependencies +# Runtime dependencies RUN apt-get update && apt-get install -y \ openssl \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* -# Copy binary from builder +# Application binary COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod -# Create non-root user and data directory +# Runtime assets +COPY templates ./templates +COPY www ./www + +# Create non-root user RUN groupadd -g 1000 bzod && \ useradd -u 1000 -g bzod -m -s /bin/bash bzod -RUN mkdir -p /app/data && chown -R bzod:bzod /app/data +# Create writable data directory +RUN mkdir -p /app/data && \ + chown -R bzod:bzod /app USER bzod @@ -66,4 +71,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD curl -f http://localhost:$${PORT:-8654}/status || exit 1 ENTRYPOINT ["bzod"] -CMD ["serve"] +CMD ["serve"] \ No newline at end of file diff --git a/src/web/pages.rs b/src/web/pages.rs index 3e4dd1d..df6f405 100644 --- a/src/web/pages.rs +++ b/src/web/pages.rs @@ -77,3 +77,47 @@ pub async fn resolve_page( None => (StatusCode::NOT_FOUND, "Landing page not found").into_response(), } } + +// GET / +// Serve static root landing page from www/index.html +pub async fn root_landing() -> Response { + let mut target_path = std::path::PathBuf::from("www/index.html"); + + if !target_path.exists() { + // Search relative to executable + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + let path1 = exe_dir.join("www/index.html"); + if path1.exists() { + target_path = path1; + } else if let Some(parent1) = exe_dir.parent() { + let path2 = parent1.join("www/index.html"); + if path2.exists() { + target_path = path2; + } else if let Some(parent2) = parent1.parent() { + let path3 = parent2.join("www/index.html"); + if path3.exists() { + target_path = path3; + } + } + } + } + } + } + + if !target_path.exists() { + // Search in CARGO_MANIFEST_DIR + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + let path = std::path::PathBuf::from(manifest_dir).join("www/index.html"); + if path.exists() { + target_path = path; + } + } + } + + match std::fs::read_to_string(&target_path) { + Ok(content) => Html(content).into_response(), + Err(_) => (StatusCode::NOT_FOUND, "Not Found").into_response(), + } +} + diff --git a/src/web/routes.rs b/src/web/routes.rs index fbd6f10..9e92978 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -7,6 +7,8 @@ use axum::{ pub fn create_router(state: AppState) -> Router { Router::new() + // --- Root Landing Page --- + .route("/", get(pages::root_landing)) // --- Public Redirection Routes --- .route("/:code", get(redirect::resolve_redirect)) .route("/p/:code", get(pages::resolve_page)) diff --git a/tests/root_landing_tests.rs b/tests/root_landing_tests.rs new file mode 100644 index 0000000..73ec074 --- /dev/null +++ b/tests/root_landing_tests.rs @@ -0,0 +1,43 @@ +use axum::http::StatusCode; +use bzod::web::pages::root_landing; +use std::fs; + +#[tokio::test] +async fn test_root_landing_page() { + // --- Test case 1: Successful serving --- + // Read expected content + let expected_content = fs::read_to_string("www/index.html").expect("www/index.html must exist for test"); + + let response = root_landing().await; + + assert_eq!(response.status(), StatusCode::OK); + assert!( + response.headers().get("content-type").unwrap().to_str().unwrap().contains("text/html") + ); + + // Convert response body to bytes using axum::body::to_bytes + let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body_str = String::from_utf8(body_bytes.to_vec()).unwrap(); + assert_eq!(body_str, expected_content); + + // --- Test case 2: File Not Found fallback --- + // Temporarily rename www/index.html to simulate file not found + let orig_path = "www/index.html"; + let temp_path = "www/index.html.tmp_test_bak"; + fs::rename(orig_path, temp_path).unwrap(); + + let response_res = tokio::spawn(async move { + root_landing().await + }).await; + + // Restore index.html immediately in case of panic/error + let rename_res = fs::rename(temp_path, orig_path); + + // Now verify the response status + let response = response_res.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + rename_res.unwrap(); +} diff --git a/www/index.html b/www/index.html index 3d8b149..d77dc64 100644 --- a/www/index.html +++ b/www/index.html @@ -1,182 +1,129 @@ + - - + + - bzo.in - Simple & Private URL Shortener + BZOD — Private • Fast • Beautiful URL Shortener + + - -
- -
+ -
-
BETA
-

Short. Clean.
Privacy-First.

-

Lightning-fast URL shortener with built-in analytics, custom landing pages, and full self-hosting control.

+ +
+
+
+ ● + Self-hosted • Rust • Single Binary +
+ +

+ Short links.
+ Your domain. +

+ +

+ Beautiful, private, and powerful URL shortener with rich landing pages, QR codes, analytics, and full CLI control. +

-
- - -
+ -
-
-

🔗 Compact Hex Codes

-

Short, memorable 4-6 character links that look professional.

-
-
-

📊 Real-time Analytics

-

Track clicks, countries, referrers with beautiful charts.

-
-
-

🎨 Custom Landing Pages

-

Beautiful branded pages before redirect (like this one!).

-
-
-

🔒 Self-Hosted & Private

-

Your data. Your server. No third-party tracking.

+
+ Powered by bzo.in
-
+
-