Initial commit: Bitwarden export to PDF renderer

Adds the render.py WeasyPrint renderer, the pod-based bitwarden-print.sh
wrapper, a Containerfile, a sample export (JSON + PDF), README and LICENSE.
This commit is contained in:
juergen committed 2026-11-13 14:09:10 +01:00
commit 41385b35e6
8 files changed
+1041

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
#!/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"