feat: add custom branding, air-gapped deployment script, and updated self-hosting docs
This commit is contained in:
755
scripts/prepare-airgap.sh
Executable file
755
scripts/prepare-airgap.sh
Executable file
@@ -0,0 +1,755 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# BentoPDF Air-Gapped Deployment Preparation Script
|
||||
# ============================================================
|
||||
# Automates the creation of a self-contained deployment bundle
|
||||
# for air-gapped (offline) networks.
|
||||
#
|
||||
# Run this on a machine WITH internet access. The output bundle
|
||||
# contains everything needed to deploy BentoPDF offline.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm
|
||||
# bash scripts/prepare-airgap.sh # interactive mode
|
||||
#
|
||||
# See --help for all options.
|
||||
# ============================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# --- Output formatting ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERR]${NC} $*" >&2; }
|
||||
step() { echo -e "\n${BOLD}==> $*${NC}"; }
|
||||
|
||||
# Disable colors if NO_COLOR is set
|
||||
if [ -n "${NO_COLOR:-}" ]; then
|
||||
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
|
||||
fi
|
||||
|
||||
# --- Defaults ---
|
||||
WASM_BASE_URL=""
|
||||
IMAGE_NAME="bentopdf"
|
||||
OUTPUT_DIR="./bentopdf-airgap-bundle"
|
||||
SIMPLE_MODE=""
|
||||
BASE_URL=""
|
||||
COMPRESSION_MODE=""
|
||||
LANGUAGE=""
|
||||
BRAND_NAME=""
|
||||
BRAND_LOGO=""
|
||||
FOOTER_TEXT=""
|
||||
DOCKERFILE="Dockerfile"
|
||||
SKIP_DOCKER=false
|
||||
SKIP_WASM=false
|
||||
INTERACTIVE=false
|
||||
|
||||
# --- Usage ---
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
BentoPDF Air-Gapped Deployment Preparation
|
||||
|
||||
USAGE:
|
||||
bash scripts/prepare-airgap.sh [OPTIONS]
|
||||
bash scripts/prepare-airgap.sh # interactive mode
|
||||
|
||||
REQUIRED:
|
||||
--wasm-base-url <url> Base URL where WASM files will be hosted
|
||||
in the air-gapped network
|
||||
(e.g. https://internal.example.com/wasm)
|
||||
|
||||
OPTIONS:
|
||||
--image-name <name> Docker image name (default: bentopdf)
|
||||
--output-dir <path> Output bundle directory (default: ./bentopdf-airgap-bundle)
|
||||
--dockerfile <path> Dockerfile to use (default: Dockerfile)
|
||||
--simple-mode Enable Simple Mode
|
||||
--base-url <path> Subdirectory base URL (e.g. /pdf/)
|
||||
--compression <mode> Compression: g, b, o, all (default: all)
|
||||
--language <code> Default UI language (e.g. fr, de, es)
|
||||
--brand-name <name> Custom brand name
|
||||
--brand-logo <path> Logo path relative to public/
|
||||
--footer-text <text> Custom footer text
|
||||
--skip-docker Skip Docker build and export
|
||||
--skip-wasm Skip WASM download (reuse existing .tgz files)
|
||||
--help Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
# Minimal (prompts for WASM URL interactively)
|
||||
bash scripts/prepare-airgap.sh
|
||||
|
||||
# Full automation
|
||||
bash scripts/prepare-airgap.sh \
|
||||
--wasm-base-url https://internal.example.com/wasm \
|
||||
--brand-name "AcmePDF" \
|
||||
--language fr
|
||||
|
||||
# Skip Docker build (reuse existing image)
|
||||
bash scripts/prepare-airgap.sh \
|
||||
--wasm-base-url https://internal.example.com/wasm \
|
||||
--skip-docker
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Parse arguments ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--wasm-base-url) WASM_BASE_URL="$2"; shift 2 ;;
|
||||
--image-name) IMAGE_NAME="$2"; shift 2 ;;
|
||||
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
||||
--simple-mode) SIMPLE_MODE="true"; shift ;;
|
||||
--base-url) BASE_URL="$2"; shift 2 ;;
|
||||
--compression) COMPRESSION_MODE="$2"; shift 2 ;;
|
||||
--language) LANGUAGE="$2"; shift 2 ;;
|
||||
--brand-name) BRAND_NAME="$2"; shift 2 ;;
|
||||
--brand-logo) BRAND_LOGO="$2"; shift 2 ;;
|
||||
--footer-text) FOOTER_TEXT="$2"; shift 2 ;;
|
||||
--dockerfile) DOCKERFILE="$2"; shift 2 ;;
|
||||
--skip-docker) SKIP_DOCKER=true; shift ;;
|
||||
--skip-wasm) SKIP_WASM=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) error "Unknown option: $1"; echo "Run with --help for usage."; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Validate project root ---
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
if [ ! -f "package.json" ] || [ ! -f "src/js/const/cdn-version.ts" ]; then
|
||||
error "This script must be run from the BentoPDF project root."
|
||||
error "Expected to find package.json and src/js/const/cdn-version.ts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Check prerequisites ---
|
||||
check_prerequisites() {
|
||||
local missing=false
|
||||
|
||||
if ! command -v npm &>/dev/null; then
|
||||
error "npm is required but not found. Install Node.js first."
|
||||
missing=true
|
||||
fi
|
||||
|
||||
if [ "$SKIP_DOCKER" = false ] && ! command -v docker &>/dev/null; then
|
||||
error "docker is required but not found (use --skip-docker to skip)."
|
||||
missing=true
|
||||
fi
|
||||
|
||||
if [ "$missing" = true ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Read versions from source code ---
|
||||
read_versions() {
|
||||
PYMUPDF_VERSION=$(grep "pymupdf:" src/js/const/cdn-version.ts | grep -o "'[^']*'" | tr -d "'")
|
||||
GS_VERSION=$(grep "ghostscript:" src/js/const/cdn-version.ts | grep -o "'[^']*'" | tr -d "'")
|
||||
APP_VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
if [ -z "$PYMUPDF_VERSION" ] || [ -z "$GS_VERSION" ]; then
|
||||
error "Failed to read WASM versions from src/js/const/cdn-version.ts"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Interactive mode ---
|
||||
interactive_mode() {
|
||||
echo ""
|
||||
echo -e "${BOLD}============================================================${NC}"
|
||||
echo -e "${BOLD} BentoPDF Air-Gapped Deployment Preparation${NC}"
|
||||
echo -e "${BOLD} App Version: ${APP_VERSION}${NC}"
|
||||
echo -e "${BOLD}============================================================${NC}"
|
||||
echo ""
|
||||
echo " Detected WASM versions from source:"
|
||||
echo " PyMuPDF: ${PYMUPDF_VERSION}"
|
||||
echo " Ghostscript: ${GS_VERSION}"
|
||||
echo " CoherentPDF: latest"
|
||||
echo ""
|
||||
|
||||
# [1] WASM base URL (REQUIRED)
|
||||
echo -e "${BOLD}[1/8] WASM Base URL ${RED}(required)${NC}"
|
||||
echo " The URL where WASM files will be hosted inside the air-gapped network."
|
||||
echo " The script will append /pymupdf/, /gs/, /cpdf/ to this URL."
|
||||
echo ""
|
||||
echo " Examples:"
|
||||
echo " https://internal.example.com/wasm"
|
||||
echo " http://192.168.1.100/assets/wasm"
|
||||
echo " https://cdn.mycompany.local/bentopdf"
|
||||
echo ""
|
||||
while true; do
|
||||
read -r -p " URL: " WASM_BASE_URL
|
||||
if [ -z "$WASM_BASE_URL" ]; then
|
||||
warn "WASM base URL is required. Please enter a URL."
|
||||
elif [[ ! "$WASM_BASE_URL" =~ ^https?:// ]]; then
|
||||
warn "Must start with http:// or https://. Try again."
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# [2] Docker image name (optional)
|
||||
echo -e "${BOLD}[2/8] Docker Image Name ${GREEN}(optional)${NC}"
|
||||
echo " The name used to tag the Docker image (used with 'docker run')."
|
||||
read -r -p " Image name [${IMAGE_NAME}]: " input
|
||||
IMAGE_NAME="${input:-$IMAGE_NAME}"
|
||||
echo ""
|
||||
|
||||
# [3] Simple mode (optional)
|
||||
echo -e "${BOLD}[3/8] Simple Mode ${GREEN}(optional)${NC}"
|
||||
echo " Hides navigation, hero, features, FAQ — shows only PDF tools."
|
||||
read -r -p " Enable Simple Mode? (y/N): " input
|
||||
if [[ "${input:-}" =~ ^[Yy]$ ]]; then
|
||||
SIMPLE_MODE="true"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# [4] Default language (optional)
|
||||
echo -e "${BOLD}[4/8] Default UI Language ${GREEN}(optional)${NC}"
|
||||
echo " Supported: en, ar, be, da, de, es, fr, id, it, nl, pt, tr, vi, zh, zh-TW"
|
||||
while true; do
|
||||
read -r -p " Language [en]: " input
|
||||
LANGUAGE="${input:-}"
|
||||
if [ -z "$LANGUAGE" ] || echo " en ar be da de es fr id it nl pt tr vi zh zh-TW " | grep -q " $LANGUAGE "; then
|
||||
break
|
||||
fi
|
||||
warn "Invalid language code '${LANGUAGE}'. Try again."
|
||||
done
|
||||
echo ""
|
||||
|
||||
# [5] Custom branding (optional)
|
||||
echo -e "${BOLD}[5/8] Custom Branding ${GREEN}(optional)${NC}"
|
||||
echo " Replace the default BentoPDF name, logo, and footer text."
|
||||
read -r -p " Brand name [BentoPDF]: " input
|
||||
BRAND_NAME="${input:-}"
|
||||
if [ -n "$BRAND_NAME" ]; then
|
||||
echo " Place your logo in the public/ folder before building."
|
||||
read -r -p " Logo path relative to public/ [images/favicon-no-bg.svg]: " input
|
||||
BRAND_LOGO="${input:-}"
|
||||
read -r -p " Footer text [© 2026 BentoPDF. All rights reserved.]: " input
|
||||
FOOTER_TEXT="${input:-}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# [6] Base URL (optional)
|
||||
echo -e "${BOLD}[6/8] Base URL ${GREEN}(optional)${NC}"
|
||||
echo " Set this if hosting under a subdirectory (e.g. /pdf/)."
|
||||
read -r -p " Base URL [/]: " input
|
||||
BASE_URL="${input:-}"
|
||||
echo ""
|
||||
|
||||
# [7] Dockerfile (optional)
|
||||
echo -e "${BOLD}[7/8] Dockerfile ${GREEN}(optional)${NC}"
|
||||
echo " Options: Dockerfile (standard) or Dockerfile.nonroot (custom PUID/PGID)"
|
||||
read -r -p " Dockerfile [${DOCKERFILE}]: " input
|
||||
DOCKERFILE="${input:-$DOCKERFILE}"
|
||||
echo ""
|
||||
|
||||
# [8] Output directory (optional)
|
||||
echo -e "${BOLD}[8/8] Output Directory ${GREEN}(optional)${NC}"
|
||||
read -r -p " Path [${OUTPUT_DIR}]: " input
|
||||
OUTPUT_DIR="${input:-$OUTPUT_DIR}"
|
||||
|
||||
# Confirm
|
||||
echo ""
|
||||
echo -e "${BOLD}--- Configuration Summary ---${NC}"
|
||||
echo ""
|
||||
echo " WASM Base URL: ${WASM_BASE_URL}"
|
||||
echo " Image Name: ${IMAGE_NAME}"
|
||||
echo " Dockerfile: ${DOCKERFILE}"
|
||||
echo " Simple Mode: ${SIMPLE_MODE:-false}"
|
||||
echo " Language: ${LANGUAGE:-en (default)}"
|
||||
echo " Brand Name: ${BRAND_NAME:-BentoPDF (default)}"
|
||||
[ -n "$BRAND_NAME" ] && echo " Brand Logo: ${BRAND_LOGO:-images/favicon-no-bg.svg (default)}"
|
||||
[ -n "$BRAND_NAME" ] && echo " Footer Text: ${FOOTER_TEXT:-(default)}"
|
||||
echo " Base URL: ${BASE_URL:-/ (root)}"
|
||||
echo " Output: ${OUTPUT_DIR}"
|
||||
echo ""
|
||||
read -r -p " Proceed? (Y/n): " input
|
||||
if [[ "${input:-Y}" =~ ^[Nn]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# --- SHA-256 checksum (cross-platform) ---
|
||||
sha256() {
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
shasum -a 256 "$1" | awk '{print $1}'
|
||||
else
|
||||
echo "n/a"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- File size (human-readable, cross-platform) ---
|
||||
filesize() {
|
||||
if stat --version &>/dev/null 2>&1; then
|
||||
# GNU stat (Linux)
|
||||
stat --printf='%s' "$1" 2>/dev/null | awk '{
|
||||
if ($1 >= 1073741824) printf "%.1f GB", $1/1073741824;
|
||||
else if ($1 >= 1048576) printf "%.1f MB", $1/1048576;
|
||||
else if ($1 >= 1024) printf "%.1f KB", $1/1024;
|
||||
else printf "%d B", $1;
|
||||
}'
|
||||
else
|
||||
# BSD stat (macOS)
|
||||
stat -f '%z' "$1" 2>/dev/null | awk '{
|
||||
if ($1 >= 1073741824) printf "%.1f GB", $1/1073741824;
|
||||
else if ($1 >= 1048576) printf "%.1f MB", $1/1048576;
|
||||
else if ($1 >= 1024) printf "%.1f KB", $1/1024;
|
||||
else printf "%d B", $1;
|
||||
}'
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
|
||||
check_prerequisites
|
||||
read_versions
|
||||
|
||||
# If no WASM base URL provided, go interactive
|
||||
if [ -z "$WASM_BASE_URL" ]; then
|
||||
INTERACTIVE=true
|
||||
interactive_mode
|
||||
fi
|
||||
|
||||
# Validate language code if provided
|
||||
if [ -n "$LANGUAGE" ]; then
|
||||
VALID_LANGS="en ar be da de es fr id it nl pt tr vi zh zh-TW"
|
||||
if ! echo " $VALID_LANGS " | grep -q " $LANGUAGE "; then
|
||||
error "Invalid language code: ${LANGUAGE}"
|
||||
error "Supported: ${VALID_LANGS}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate WASM base URL format
|
||||
if [[ ! "$WASM_BASE_URL" =~ ^https?:// ]]; then
|
||||
error "WASM base URL must start with http:// or https://"
|
||||
error " Got: ${WASM_BASE_URL}"
|
||||
error " Example: https://internal.example.com/wasm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Strip trailing slash from WASM base URL
|
||||
WASM_BASE_URL="${WASM_BASE_URL%/}"
|
||||
|
||||
# Construct WASM URLs
|
||||
WASM_PYMUPDF_URL="${WASM_BASE_URL}/pymupdf/"
|
||||
WASM_GS_URL="${WASM_BASE_URL}/gs/"
|
||||
WASM_CPDF_URL="${WASM_BASE_URL}/cpdf/"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}============================================================${NC}"
|
||||
echo -e "${BOLD} BentoPDF Air-Gapped Bundle Preparation${NC}"
|
||||
echo -e "${BOLD} App: v${APP_VERSION} | PyMuPDF: ${PYMUPDF_VERSION} | GS: ${GS_VERSION}${NC}"
|
||||
echo -e "${BOLD}============================================================${NC}"
|
||||
|
||||
# --- Phase 1: Prepare output directory ---
|
||||
step "Preparing output directory"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||
|
||||
# Warn if output directory already has bundle files
|
||||
if ls "$OUTPUT_DIR"/*.tgz "$OUTPUT_DIR"/bentopdf.tar "$OUTPUT_DIR"/setup.sh 2>/dev/null | head -1 &>/dev/null; then
|
||||
warn "Output directory already contains files from a previous run."
|
||||
warn "Existing files will be overwritten."
|
||||
if [ "$INTERACTIVE" = true ]; then
|
||||
read -r -p " Continue? (Y/n): " input
|
||||
if [[ "${input:-Y}" =~ ^[Nn]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Output: ${OUTPUT_DIR}"
|
||||
|
||||
# --- Phase 2: Download WASM packages ---
|
||||
if [ "$SKIP_WASM" = true ]; then
|
||||
step "Skipping WASM download (--skip-wasm)"
|
||||
# Verify each file exists with specific errors
|
||||
wasm_missing=false
|
||||
if ! ls "$OUTPUT_DIR"/bentopdf-pymupdf-wasm-*.tgz &>/dev/null; then
|
||||
error "Missing: bentopdf-pymupdf-wasm-*.tgz"
|
||||
wasm_missing=true
|
||||
fi
|
||||
if ! ls "$OUTPUT_DIR"/bentopdf-gs-wasm-*.tgz &>/dev/null; then
|
||||
error "Missing: bentopdf-gs-wasm-*.tgz"
|
||||
wasm_missing=true
|
||||
fi
|
||||
if ! ls "$OUTPUT_DIR"/coherentpdf-*.tgz &>/dev/null; then
|
||||
error "Missing: coherentpdf-*.tgz"
|
||||
wasm_missing=true
|
||||
fi
|
||||
if [ "$wasm_missing" = true ]; then
|
||||
error "Run without --skip-wasm first to download the packages."
|
||||
exit 1
|
||||
fi
|
||||
success "Reusing existing WASM packages"
|
||||
else
|
||||
step "Downloading WASM packages"
|
||||
|
||||
WASM_TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$WASM_TMP"' EXIT
|
||||
|
||||
info "Downloading @bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}..."
|
||||
if ! (cd "$WASM_TMP" && npm pack "@bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}" --quiet 2>&1); then
|
||||
error "Failed to download @bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}"
|
||||
error "Check your internet connection and that the package exists on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Downloading @bentopdf/gs-wasm@${GS_VERSION}..."
|
||||
if ! (cd "$WASM_TMP" && npm pack "@bentopdf/gs-wasm@${GS_VERSION}" --quiet 2>&1); then
|
||||
error "Failed to download @bentopdf/gs-wasm@${GS_VERSION}"
|
||||
error "Check your internet connection and that the package exists on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Downloading coherentpdf..."
|
||||
if ! (cd "$WASM_TMP" && npm pack coherentpdf --quiet 2>&1); then
|
||||
error "Failed to download coherentpdf"
|
||||
error "Check your internet connection and that the package exists on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Move to output directory
|
||||
mv "$WASM_TMP"/*.tgz "$OUTPUT_DIR/"
|
||||
rm -rf "$WASM_TMP"
|
||||
trap - EXIT
|
||||
|
||||
# Resolve CoherentPDF version from filename
|
||||
CPDF_TGZ=$(ls "$OUTPUT_DIR"/coherentpdf-*.tgz 2>/dev/null | head -1)
|
||||
CPDF_VERSION=$(basename "$CPDF_TGZ" | sed 's/coherentpdf-\(.*\)\.tgz/\1/')
|
||||
|
||||
success "Downloaded all WASM packages"
|
||||
info " PyMuPDF: $(filesize "$OUTPUT_DIR"/bentopdf-pymupdf-wasm-*.tgz)"
|
||||
info " Ghostscript: $(filesize "$OUTPUT_DIR"/bentopdf-gs-wasm-*.tgz)"
|
||||
info " CoherentPDF: $(filesize "$CPDF_TGZ") (v${CPDF_VERSION})"
|
||||
fi
|
||||
|
||||
# Resolve CPDF version if we skipped download
|
||||
if [ -z "${CPDF_VERSION:-}" ]; then
|
||||
CPDF_TGZ=$(ls "$OUTPUT_DIR"/coherentpdf-*.tgz 2>/dev/null | head -1)
|
||||
CPDF_VERSION=$(basename "$CPDF_TGZ" | sed 's/coherentpdf-\(.*\)\.tgz/\1/')
|
||||
fi
|
||||
|
||||
# --- Phase 3: Build Docker image ---
|
||||
if [ "$SKIP_DOCKER" = true ]; then
|
||||
step "Skipping Docker build (--skip-docker)"
|
||||
|
||||
# Check if image exists or tar exists
|
||||
if [ -f "$OUTPUT_DIR/bentopdf.tar" ]; then
|
||||
success "Reusing existing bentopdf.tar"
|
||||
elif docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||
step "Exporting existing Docker image"
|
||||
docker save "$IMAGE_NAME" -o "$OUTPUT_DIR/bentopdf.tar"
|
||||
success "Exported: $(filesize "$OUTPUT_DIR/bentopdf.tar")"
|
||||
else
|
||||
warn "No Docker image '${IMAGE_NAME}' found and no bentopdf.tar in output."
|
||||
warn "The bundle will not include a Docker image."
|
||||
fi
|
||||
else
|
||||
step "Building Docker image"
|
||||
|
||||
# Verify Dockerfile exists
|
||||
if [ ! -f "$DOCKERFILE" ]; then
|
||||
error "Dockerfile not found: ${DOCKERFILE}"
|
||||
error "Available Dockerfiles:"
|
||||
ls -1 Dockerfile* 2>/dev/null | sed 's/^/ /' || echo " (none found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify Docker daemon is running
|
||||
if ! docker info &>/dev/null; then
|
||||
error "Docker daemon is not running. Start Docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the docker build command
|
||||
BUILD_ARGS=()
|
||||
BUILD_ARGS+=(--build-arg "VITE_WASM_PYMUPDF_URL=${WASM_PYMUPDF_URL}")
|
||||
BUILD_ARGS+=(--build-arg "VITE_WASM_GS_URL=${WASM_GS_URL}")
|
||||
BUILD_ARGS+=(--build-arg "VITE_WASM_CPDF_URL=${WASM_CPDF_URL}")
|
||||
|
||||
[ -n "$SIMPLE_MODE" ] && BUILD_ARGS+=(--build-arg "SIMPLE_MODE=${SIMPLE_MODE}")
|
||||
[ -n "$BASE_URL" ] && BUILD_ARGS+=(--build-arg "BASE_URL=${BASE_URL}")
|
||||
[ -n "$COMPRESSION_MODE" ] && BUILD_ARGS+=(--build-arg "COMPRESSION_MODE=${COMPRESSION_MODE}")
|
||||
[ -n "$LANGUAGE" ] && BUILD_ARGS+=(--build-arg "VITE_DEFAULT_LANGUAGE=${LANGUAGE}")
|
||||
[ -n "$BRAND_NAME" ] && BUILD_ARGS+=(--build-arg "VITE_BRAND_NAME=${BRAND_NAME}")
|
||||
[ -n "$BRAND_LOGO" ] && BUILD_ARGS+=(--build-arg "VITE_BRAND_LOGO=${BRAND_LOGO}")
|
||||
[ -n "$FOOTER_TEXT" ] && BUILD_ARGS+=(--build-arg "VITE_FOOTER_TEXT=${FOOTER_TEXT}")
|
||||
|
||||
info "Image name: ${IMAGE_NAME}"
|
||||
info "Dockerfile: ${DOCKERFILE}"
|
||||
info "WASM URLs:"
|
||||
info " PyMuPDF: ${WASM_PYMUPDF_URL}"
|
||||
info " Ghostscript: ${WASM_GS_URL}"
|
||||
info " CoherentPDF: ${WASM_CPDF_URL}"
|
||||
echo ""
|
||||
info "Building... this may take a few minutes (npm install + Vite build)."
|
||||
echo ""
|
||||
|
||||
docker build -f "$DOCKERFILE" "${BUILD_ARGS[@]}" -t "$IMAGE_NAME" .
|
||||
|
||||
success "Docker image '${IMAGE_NAME}' built successfully"
|
||||
|
||||
# --- Phase 4: Export Docker image ---
|
||||
step "Exporting Docker image"
|
||||
|
||||
docker save "$IMAGE_NAME" -o "$OUTPUT_DIR/bentopdf.tar"
|
||||
success "Exported: $(filesize "$OUTPUT_DIR/bentopdf.tar")"
|
||||
fi
|
||||
|
||||
# --- Phase 5: Generate setup.sh ---
|
||||
step "Generating setup script"
|
||||
|
||||
cat > "$OUTPUT_DIR/setup.sh" <<SETUP_EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# BentoPDF Air-Gapped Setup Script
|
||||
# Generated on $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
# BentoPDF v${APP_VERSION}
|
||||
# ============================================================
|
||||
# Transfer this entire directory to the air-gapped network,
|
||||
# then run this script.
|
||||
# ============================================================
|
||||
|
||||
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# --- Check prerequisites ---
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "ERROR: docker is required but not found."
|
||||
echo "Install Docker first: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
echo "ERROR: Docker daemon is not running. Start Docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Configuration (baked in at generation time) ---
|
||||
IMAGE_NAME="${IMAGE_NAME}"
|
||||
WASM_BASE_URL="${WASM_BASE_URL}"
|
||||
DOCKER_PORT="\${1:-3000}"
|
||||
|
||||
# Where to extract WASM files (override with WASM_EXTRACT_DIR env var)
|
||||
WASM_DIR="\${WASM_EXTRACT_DIR:-\${SCRIPT_DIR}/wasm}"
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " BentoPDF Air-Gapped Setup"
|
||||
echo " Version: ${APP_VERSION}"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo " Docker image: \${IMAGE_NAME}"
|
||||
echo " WASM base URL: \${WASM_BASE_URL}"
|
||||
echo " WASM extract: \${WASM_DIR}"
|
||||
echo " Port: \${DOCKER_PORT}"
|
||||
echo ""
|
||||
|
||||
# --- Step 1: Load Docker Image ---
|
||||
echo "[1/3] Loading Docker image..."
|
||||
if [ -f "\${SCRIPT_DIR}/bentopdf.tar" ]; then
|
||||
docker load -i "\${SCRIPT_DIR}/bentopdf.tar"
|
||||
echo " Docker image '\${IMAGE_NAME}' loaded."
|
||||
else
|
||||
echo " WARNING: bentopdf.tar not found. Skipping Docker load."
|
||||
echo " Make sure the image '\${IMAGE_NAME}' is already available."
|
||||
fi
|
||||
|
||||
# --- Step 2: Extract WASM Packages ---
|
||||
echo ""
|
||||
echo "[2/3] Extracting WASM packages to \${WASM_DIR}..."
|
||||
|
||||
mkdir -p "\${WASM_DIR}/pymupdf" "\${WASM_DIR}/gs" "\${WASM_DIR}/cpdf"
|
||||
|
||||
# PyMuPDF: package has dist/ and assets/ at root
|
||||
echo " Extracting PyMuPDF..."
|
||||
tar xzf "\${SCRIPT_DIR}"/bentopdf-pymupdf-wasm-*.tgz -C "\${WASM_DIR}/pymupdf" --strip-components=1
|
||||
|
||||
# Ghostscript: browser expects gs.js and gs.wasm at root
|
||||
echo " Extracting Ghostscript..."
|
||||
TEMP_GS="\$(mktemp -d)"
|
||||
tar xzf "\${SCRIPT_DIR}"/bentopdf-gs-wasm-*.tgz -C "\${TEMP_GS}"
|
||||
if [ -d "\${TEMP_GS}/package/assets" ]; then
|
||||
cp -r "\${TEMP_GS}/package/assets/"* "\${WASM_DIR}/gs/"
|
||||
else
|
||||
cp -r "\${TEMP_GS}/package/"* "\${WASM_DIR}/gs/"
|
||||
fi
|
||||
rm -rf "\${TEMP_GS}"
|
||||
|
||||
# CoherentPDF: browser expects coherentpdf.browser.min.js at root
|
||||
echo " Extracting CoherentPDF..."
|
||||
TEMP_CPDF="\$(mktemp -d)"
|
||||
tar xzf "\${SCRIPT_DIR}"/coherentpdf-*.tgz -C "\${TEMP_CPDF}"
|
||||
if [ -d "\${TEMP_CPDF}/package/dist" ]; then
|
||||
cp -r "\${TEMP_CPDF}/package/dist/"* "\${WASM_DIR}/cpdf/"
|
||||
else
|
||||
cp -r "\${TEMP_CPDF}/package/"* "\${WASM_DIR}/cpdf/"
|
||||
fi
|
||||
rm -rf "\${TEMP_CPDF}"
|
||||
|
||||
echo " WASM files extracted to: \${WASM_DIR}"
|
||||
echo ""
|
||||
echo " IMPORTANT: Ensure these paths are served by your internal web server:"
|
||||
echo " \${WASM_BASE_URL}/pymupdf/ -> \${WASM_DIR}/pymupdf/"
|
||||
echo " \${WASM_BASE_URL}/gs/ -> \${WASM_DIR}/gs/"
|
||||
echo " \${WASM_BASE_URL}/cpdf/ -> \${WASM_DIR}/cpdf/"
|
||||
|
||||
# --- Step 3: Start BentoPDF ---
|
||||
echo ""
|
||||
echo "[3/3] Ready to start BentoPDF"
|
||||
echo ""
|
||||
echo " To start manually:"
|
||||
echo " docker run -d --name bentopdf -p \${DOCKER_PORT}:8080 --restart unless-stopped \${IMAGE_NAME}"
|
||||
echo ""
|
||||
echo " Then open: http://localhost:\${DOCKER_PORT}"
|
||||
echo ""
|
||||
|
||||
read -r -p "Start BentoPDF now? (y/N): " REPLY
|
||||
if [[ "\${REPLY:-}" =~ ^[Yy]$ ]]; then
|
||||
docker run -d --name bentopdf -p "\${DOCKER_PORT}:8080" --restart unless-stopped "\${IMAGE_NAME}"
|
||||
echo ""
|
||||
echo " BentoPDF is running at http://localhost:\${DOCKER_PORT}"
|
||||
fi
|
||||
SETUP_EOF
|
||||
|
||||
chmod +x "$OUTPUT_DIR/setup.sh"
|
||||
success "Generated setup.sh"
|
||||
|
||||
# --- Phase 6: Generate README ---
|
||||
step "Generating README"
|
||||
|
||||
cat > "$OUTPUT_DIR/README.md" <<README_EOF
|
||||
# BentoPDF Air-Gapped Deployment Bundle
|
||||
|
||||
**BentoPDF v${APP_VERSION}** | Generated on $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
## Contents
|
||||
|
||||
| File | Description |
|
||||
| --- | --- |
|
||||
| \`bentopdf.tar\` | Docker image |
|
||||
| \`bentopdf-pymupdf-wasm-${PYMUPDF_VERSION}.tgz\` | PyMuPDF WASM module |
|
||||
| \`bentopdf-gs-wasm-${GS_VERSION}.tgz\` | Ghostscript WASM module |
|
||||
| \`coherentpdf-${CPDF_VERSION}.tgz\` | CoherentPDF WASM module |
|
||||
| \`setup.sh\` | Automated setup script |
|
||||
| \`README.md\` | This file |
|
||||
|
||||
## WASM Configuration
|
||||
|
||||
The Docker image was built with these WASM URLs:
|
||||
|
||||
- **PyMuPDF:** \`${WASM_PYMUPDF_URL}\`
|
||||
- **Ghostscript:** \`${WASM_GS_URL}\`
|
||||
- **CoherentPDF:** \`${WASM_CPDF_URL}\`
|
||||
|
||||
These URLs are baked into the app at build time. The user's browser fetches
|
||||
WASM files from these URLs at runtime.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
Transfer this entire directory to the air-gapped network, then:
|
||||
|
||||
\`\`\`bash
|
||||
bash setup.sh
|
||||
\`\`\`
|
||||
|
||||
The setup script will:
|
||||
1. Load the Docker image
|
||||
2. Extract WASM packages to \`./wasm/\` (override with \`WASM_EXTRACT_DIR\`)
|
||||
3. Optionally start the BentoPDF container
|
||||
|
||||
## Manual Setup
|
||||
|
||||
### 1. Load the Docker image
|
||||
|
||||
\`\`\`bash
|
||||
docker load -i bentopdf.tar
|
||||
\`\`\`
|
||||
|
||||
### 2. Extract WASM packages
|
||||
|
||||
Extract to your internal web server's document root:
|
||||
|
||||
\`\`\`bash
|
||||
mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf
|
||||
|
||||
# PyMuPDF
|
||||
tar xzf bentopdf-pymupdf-wasm-${PYMUPDF_VERSION}.tgz -C ./wasm/pymupdf --strip-components=1
|
||||
|
||||
# Ghostscript (extract assets/ to root)
|
||||
TEMP_GS=\$(mktemp -d)
|
||||
tar xzf bentopdf-gs-wasm-${GS_VERSION}.tgz -C \$TEMP_GS
|
||||
cp -r \$TEMP_GS/package/assets/* ./wasm/gs/
|
||||
rm -rf \$TEMP_GS
|
||||
|
||||
# CoherentPDF (extract dist/ to root)
|
||||
TEMP_CPDF=\$(mktemp -d)
|
||||
tar xzf coherentpdf-${CPDF_VERSION}.tgz -C \$TEMP_CPDF
|
||||
cp -r \$TEMP_CPDF/package/dist/* ./wasm/cpdf/
|
||||
rm -rf \$TEMP_CPDF
|
||||
\`\`\`
|
||||
|
||||
### 3. Configure your web server
|
||||
|
||||
Ensure these paths are accessible at the configured URLs:
|
||||
|
||||
| URL | Serves From |
|
||||
| --- | --- |
|
||||
| \`${WASM_PYMUPDF_URL}\` | \`./wasm/pymupdf/\` |
|
||||
| \`${WASM_GS_URL}\` | \`./wasm/gs/\` |
|
||||
| \`${WASM_CPDF_URL}\` | \`./wasm/cpdf/\` |
|
||||
|
||||
### 4. Run BentoPDF
|
||||
|
||||
\`\`\`bash
|
||||
docker run -d --name bentopdf -p 3000:8080 --restart unless-stopped ${IMAGE_NAME}
|
||||
\`\`\`
|
||||
|
||||
Open: http://localhost:3000
|
||||
README_EOF
|
||||
|
||||
success "Generated README.md"
|
||||
|
||||
# --- Phase 7: Summary ---
|
||||
step "Bundle complete"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD} Output: ${OUTPUT_DIR}${NC}"
|
||||
echo ""
|
||||
echo " Files:"
|
||||
for f in "$OUTPUT_DIR"/*; do
|
||||
fname=$(basename "$f")
|
||||
fsize=$(filesize "$f")
|
||||
echo " ${fname} (${fsize})"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD} Next steps:${NC}"
|
||||
echo " 1. Transfer the '$(basename "$OUTPUT_DIR")' directory to the air-gapped network"
|
||||
echo " 2. Run: bash setup.sh"
|
||||
echo " 3. Configure your internal web server to serve the WASM files"
|
||||
echo ""
|
||||
success "Done!"
|
||||
Reference in New Issue
Block a user