#!/bin/sh set -eu helper_image='qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c' archive_format='thothii-qdrant-backup-v1' volume_role='qdrant-data' usage() { echo "usage: $0 --project-name NAME --output FILE" >&2 exit 2 } project_name= output= while [ "$#" -gt 0 ]; do case "$1" in --project-name) project_name=${2-}; shift 2 ;; --output) output=${2-}; shift 2 ;; *) usage ;; esac done [ -n "$project_name" ] && [ -n "$output" ] || usage [ ! -e "$output" ] || { echo "refusing to overwrite existing backup: $output" >&2; exit 2; } output_dir=$(dirname "$output") output_name=$(basename "$output") [ -d "$output_dir" ] || { echo "backup destination directory does not exist" >&2; exit 2; } expected_volume_name="${project_name}_${volume_role}" script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) . "$script_dir/lib/vector-operation-lock.sh" vector_operation_lock_init resolve_volume() { names=$(docker volume ls \ --filter "label=com.docker.compose.project=$project_name" \ --filter "label=com.docker.compose.volume=$volume_role" \ --format '{{.Name}}') count=$(printf '%s\n' "$names" | sed '/^$/d' | wc -l | tr -d ' ') [ "$count" -eq 1 ] || { echo "expected exactly one qdrant-data volume for compose project $project_name" >&2 exit 2 } resolved=$(printf '%s\n' "$names" | sed -n '/./{p;q;}') [ "$resolved" = "$expected_volume_name" ] || { echo "unexpected qdrant-data volume name: $resolved" >&2 exit 2 } printf '%s\n' "$resolved" } validate_volume_metadata() { metadata=$(docker volume inspect --format '{{ .Name }} {{ index .Labels "com.docker.compose.project" }} {{ index .Labels "com.docker.compose.volume" }}' "$1") set -- $metadata [ "${1-}" = "$expected_volume_name" ] || { echo "volume metadata name mismatch" >&2; exit 2; } [ "${2-}" = "$project_name" ] || { echo "volume metadata project label mismatch" >&2; exit 2; } [ "${3-}" = "$volume_role" ] || { echo "volume metadata role label mismatch" >&2; exit 2; } } restart_qdrant=0 temporary_output= cleanup() { status=$? trap - EXIT HUP INT TERM if [ -n "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then rm -f "$temporary_output" fi if [ "$restart_qdrant" -eq 1 ]; then if ! docker compose --project-name "$project_name" start qdrant >/dev/null; then echo "Unable to restart Qdrant after backup" >&2 [ "$status" -ne 0 ] || status=1 fi fi if ! release_vector_operation_lock; then [ "$status" -ne 0 ] || status=1 fi exit "$status" } trap cleanup EXIT HUP INT TERM acquire_vector_operation_lock volume_name=$(resolve_volume) validate_volume_metadata "$volume_name" running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant) if [ -n "$running_container" ]; then restart_qdrant=1 docker compose --project-name "$project_name" stop qdrant >/dev/null fi umask 077 temporary_output=$(mktemp "$output_dir/.${output_name}.tmp.XXXXXX") created_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ") docker run --rm \ --mount "type=volume,src=$volume_name,dst=/qdrant-data,readonly" \ --mount "type=bind,src=$output_dir,dst=/backup" \ "$helper_image" \ /bin/sh -eu -c ' archive_format=$1 project_name=$2 volume_name=$3 volume_role=$4 helper_image=$5 created_utc=$6 output_name=$7 staging=$(mktemp -d /tmp/qdrant-backup.XXXXXX) trap "rm -rf \"$staging\"" EXIT HUP INT TERM mkdir -p "$staging/payload" cat >"$staging/manifest.env" <&2 exit 2 fi rm -f "$temporary_output" temporary_output= echo "Qdrant backup written from $volume_name to $output"