#!/usr/bin/env bash # Render a Bitwarden JSON export to PDF inside a dedicated Podman pod. set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) IMAGE=${IMAGE:-localhost/bitwarden-print:latest} POD=${POD:-bitwarden-print-pod} CONTAINER=${CONTAINER:-bitwarden-print-run} usage() { cat <<'EOF' Usage: ./bitwarden-print.sh EXPORT.json [-o OUT.pdf] [options] Creates a PDF with every value from a Bitwarden JSON export, densely typeset. Options: -o, --output FILE output PDF (default: next to the export, same basename) --sort MODE folder (default), name, type or as-is --title TEXT document title --paper SIZE a4 (default) or letter --mask-secrets mask passwords, TOTP, card numbers/codes, SSN, keys -h, --help show this help Environment: IMAGE, POD, CONTAINER override image/pod/container names EOF } INPUT="" OUTPUT="" render_args=() while [ $# -gt 0 ]; do case "$1" in -o|--output) [ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; exit 2; } OUTPUT=$2; shift 2 ;; --sort) [ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; exit 2; } render_args+=(--sort "$2"); shift 2 ;; --paper) [ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; exit 2; } render_args+=(--paper "$2"); shift 2 ;; --title) [ $# -ge 2 ] || { echo "error: $1 needs a value" >&2; exit 2; } render_args+=(--title "$2"); shift 2 ;; --mask-secrets) render_args+=(--mask-secrets); shift ;; -h|--help) usage; exit 0 ;; -*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;; *) [ -z "$INPUT" ] || { echo "error: only one export file can be given" >&2; exit 2; } INPUT=$1; shift ;; esac done [ -n "$INPUT" ] || { usage >&2; exit 2; } [ -f "$INPUT" ] || { echo "error: no such file: $INPUT" >&2; exit 1; } in_dir=$(cd -- "$(dirname -- "$INPUT")" && pwd) in_name=$(basename -- "$INPUT") if [ -z "$OUTPUT" ]; then OUTPUT="$in_dir/${in_name%.json}.pdf" fi out_dir=$(dirname -- "$OUTPUT") mkdir -p -- "$out_dir" out_dir=$(cd -- "$out_dir" && pwd) out_name=$(basename -- "$OUTPUT") cleanup() { podman rm -f "$CONTAINER" >/dev/null 2>&1 || true podman pod exists "$POD" >/dev/null 2>&1 && podman pod rm -f "$POD" >/dev/null 2>&1 || true } trap cleanup EXIT echo "==> building image $IMAGE" podman build -q -t "$IMAGE" "$SCRIPT_DIR" echo "==> creating pod $POD" podman pod rm -f "$POD" >/dev/null 2>&1 || true podman pod create --name "$POD" >/dev/null # Copy the export in instead of bind-mounting it: works for any host path, # including paths the Podman machine does not share (e.g. /tmp) and hosts # where SELinux relabelling is not allowed. echo "==> rendering $in_name" podman create --pod "$POD" --name "$CONTAINER" \ "$IMAGE" "/export.json" -o "/output.pdf" \ ${render_args[@]+"${render_args[@]}"} >/dev/null podman cp "$INPUT" "$CONTAINER:/export.json" status=0 podman start -a "$CONTAINER" || status=$? if [ "$status" -ne 0 ]; then echo "error: rendering failed (exit $status)" >&2 exit "$status" fi tmp_pdf=$(mktemp "${TMPDIR:-/tmp}/bitwarden-print.XXXXXX.pdf") if ! podman cp "$CONTAINER:/output.pdf" "$tmp_pdf"; then echo "error: container produced no PDF at /output.pdf" >&2 exit 1 fi mv -f "$tmp_pdf" "$out_dir/$out_name" echo "==> PDF: $out_dir/$out_name"