| #!/usr/bin/env bash |
| set -euo pipefail |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" |
|
|
| |
| 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}"; } |
|
|
| |
| if [ -n "${NO_COLOR:-}" ]; then |
| RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' |
| fi |
|
|
| |
| 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() { |
| cat <<'EOF' |
| BentoPDF Air-Gapped Deployment Preparation |
|
|
| USAGE: |
| bash scripts/prepare-airgap.sh [OPTIONS] |
| bash scripts/prepare-airgap.sh |
|
|
| 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: |
| |
| bash scripts/prepare-airgap.sh |
|
|
| |
| bash scripts/prepare-airgap.sh \ |
| --wasm-base-url https://internal.example.com/wasm \ |
| --brand-name "AcmePDF" \ |
| --language fr |
|
|
| |
| bash scripts/prepare-airgap.sh \ |
| --wasm-base-url https://internal.example.com/wasm \ |
| --skip-docker |
| EOF |
| exit 0 |
| } |
|
|
| |
| 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 |
|
|
| |
| 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() { |
| 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() { |
| 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() { |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| 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 "" |
|
|
| |
| echo -e "${BOLD}[8/8] Output Directory ${GREEN}(optional)${NC}" |
| read -r -p " Path [${OUTPUT_DIR}]: " input |
| OUTPUT_DIR="${input:-$OUTPUT_DIR}" |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| filesize() { |
| if stat --version &>/dev/null 2>&1; then |
| |
| 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 |
| |
| 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 |
| } |
|
|
| |
| |
| |
|
|
| check_prerequisites |
| read_versions |
|
|
| |
| if [ -z "$WASM_BASE_URL" ]; then |
| INTERACTIVE=true |
| interactive_mode |
| fi |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| WASM_BASE_URL="${WASM_BASE_URL%/}" |
|
|
| |
| 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}" |
|
|
| |
| step "Preparing output directory" |
|
|
| mkdir -p "$OUTPUT_DIR" |
| OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" |
|
|
| |
| 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}" |
|
|
| |
| if [ "$SKIP_WASM" = true ]; then |
| step "Skipping WASM download (--skip-wasm)" |
| |
| 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 |
|
|
| |
| mv "$WASM_TMP"/*.tgz "$OUTPUT_DIR/" |
| rm -rf "$WASM_TMP" |
| trap - EXIT |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| if [ "$SKIP_DOCKER" = true ]; then |
| step "Skipping Docker build (--skip-docker)" |
|
|
| |
| 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" |
|
|
| |
| 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 |
|
|
| |
| if ! docker info &>/dev/null; then |
| error "Docker daemon is not running. Start Docker and try again." |
| exit 1 |
| fi |
|
|
| |
| 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" |
|
|
| |
| step "Exporting Docker image" |
|
|
| docker save "$IMAGE_NAME" -o "$OUTPUT_DIR/bentopdf.tar" |
| success "Exported: $(filesize "$OUTPUT_DIR/bentopdf.tar")" |
| fi |
|
|
| |
| 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" |
|
|
| |
| 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" |
|
|
| |
| 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!" |
|
|