From afaab5931fd3e9cbf168ec274ed93b86c3ed4e57 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sat, 8 Aug 2026 19:37:09 +0200 Subject: [PATCH] fix: harden qdrant restore safety --- scripts/test-vector-backup-restore-safety.sh | 287 ++++++++++++++++--- scripts/vector-backup.sh | 66 ++++- scripts/vector-restore.sh | 191 +++++++++--- 3 files changed, 461 insertions(+), 83 deletions(-) diff --git a/scripts/test-vector-backup-restore-safety.sh b/scripts/test-vector-backup-restore-safety.sh index 1351ed3d..9dcb0a56 100755 --- a/scripts/test-vector-backup-restore-safety.sh +++ b/scripts/test-vector-backup-restore-safety.sh @@ -9,9 +9,10 @@ mkdir "$fakebin" project="thoth-task8" volume_name="${project}_qdrant-data" -mountpoint="$tmp/docker-volumes/$volume_name/_data" -mkdir -p "$mountpoint/collections/demo" -printf '%s' before-backup >"$mountpoint/collections/demo/state.json" +volume_root="$tmp/volumes" +volume_dir="$volume_root/$volume_name" +mkdir -p "$volume_dir/collections/demo" +printf '%s' before-backup >"$volume_dir/collections/demo/state.json" cat >"$fakebin/docker" <<'SH' #!/bin/sh @@ -19,8 +20,49 @@ set -eu log_file=${DOCKER_LOG:?} printf '%s\n' "$*" >>"$log_file" +volume_dir_for() { + printf '%s/%s\n' "${VOLUME_ROOT:?}" "$1" +} + +run_backup() { + volume_name=$1 + backup_dir=$2 + output_name=$3 + staging=$(mktemp -d "${TMPDIR:-/tmp}/fake-qdrant-backup.XXXXXX") + mkdir -p "$staging/payload" + cat >"$staging/manifest.env" </dev/null test -s "$backup_output" -tar -tf "$backup_output" | grep -q '^./collections/demo/state.json$' -grep -q "volume ls --filter label=com.docker.compose.project=$project --filter label=com.docker.compose.volume=qdrant-data" "$docker_log" -grep -q "compose --project-name $project ps --status running -q qdrant" "$docker_log" +tar -tf "$backup_output" | grep -qx 'manifest.env' +tar -tf "$backup_output" | grep -qx 'payload/collections/demo/state.json' +tar -xOf "$backup_output" manifest.env | grep -qx 'format=thothii-qdrant-backup-v1' +tar -xOf "$backup_output" manifest.env | grep -qx "project_name=$project" +tar -xOf "$backup_output" manifest.env | grep -qx "volume_name=$volume_name" +grep -q "run --rm --mount type=volume,src=$volume_name,dst=/qdrant-data,readonly --mount type=bind,src=$tmp,dst=/backup" "$docker_log" grep -q "compose --project-name $project stop qdrant" "$docker_log" grep -q "compose --project-name $project start qdrant" "$docker_log" +if grep -q "volume inspect --format {{ .Mountpoint }}" "$docker_log"; then + echo "backup consulted Docker mountpoints" >&2 + exit 1 +fi +if grep -q "prune" "$docker_log"; then + echo "backup attempted global docker cleanup" >&2 + exit 1 +fi existing="$tmp/existing.tar" printf '%s' sentinel >"$existing" -if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/existing.log" PROJECT_NAME="$project" MOUNTPOINT="$mountpoint" \ - ./scripts/vector-backup.sh --project-name "$project" --output "$existing" >"$tmp/existing.out" 2>"$tmp/existing.err"; then +if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/existing.log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-backup.sh --project-name "$project" --output "$existing" >"$tmp/existing.out" 2>"$tmp/existing.err"; then echo "backup overwrote an existing archive" >&2 exit 1 fi test "$(cat "$existing")" = sentinel -if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/ambiguous.log" PROJECT_NAME="$project" MOUNTPOINT="$mountpoint" VOLUME_LS_OUTPUT=multiple \ - ./scripts/vector-backup.sh --project-name "$project" --output "$tmp/ambiguous.tar" >"$tmp/ambiguous.out" 2>"$tmp/ambiguous.err"; then +if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/ambiguous.log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +VOLUME_LS_OUTPUT=multiple HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-backup.sh --project-name "$project" --output "$tmp/ambiguous.tar" >"$tmp/ambiguous.out" 2>"$tmp/ambiguous.err"; then echo "backup accepted an ambiguous qdrant-data target" >&2 exit 1 fi grep -q 'exactly one qdrant-data volume' "$tmp/ambiguous.err" -restore_input="$tmp/restore.tar" -restore_source="$tmp/restore-source" -mkdir -p "$restore_source/collections/demo" -printf '%s' restored >"$restore_source/collections/demo/state.json" -tar -C "$restore_source" -cf "$restore_input" . +foreign_archive="$tmp/foreign.tar" +make_foreign_archive "$foreign_archive" +printf '%s' before-malicious >"$volume_dir/collections/demo/state.json" +foreign_log="$tmp/foreign.log" +: >"$foreign_log" +if PATH="$fakebin:$PATH" DOCKER_LOG="$foreign_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +MOUNTPOINT="/" HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-restore.sh --project-name "$project" --input "$foreign_archive" --confirm-project "$project" \ + >"$tmp/foreign.out" 2>"$tmp/foreign.err"; then + echo "restore accepted a malformed or foreign archive" >&2 + exit 1 +fi +grep -Eq 'manifest|archive|foreign|unexpected|traversal' "$tmp/foreign.err" +test "$(cat "$volume_dir/collections/demo/state.json")" = before-malicious +if grep -q "compose --project-name $project stop qdrant" "$foreign_log"; then + echo "restore stopped qdrant before archive validation" >&2 + exit 1 +fi +if grep -q '^run ' "$foreign_log"; then + echo "restore ran helper container before archive validation" >&2 + exit 1 +fi +if grep -q "volume inspect --format {{ .Mountpoint }}" "$foreign_log"; then + echo "restore consulted Docker mountpoints during malformed archive rejection" >&2 + exit 1 +fi -if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/restore-refuse.log" PROJECT_NAME="$project" MOUNTPOINT="$mountpoint" \ - ./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project wrong-project \ - >"$tmp/restore-refuse.out" 2>"$tmp/restore-refuse.err"; then +symlink_archive="$tmp/symlink.tar" +make_symlink_archive "$symlink_archive" +symlink_log="$tmp/symlink.log" +: >"$symlink_log" +if PATH="$fakebin:$PATH" DOCKER_LOG="$symlink_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-restore.sh --project-name "$project" --input "$symlink_archive" --confirm-project "$project" \ + >"$tmp/symlink.out" 2>"$tmp/symlink.err"; then + echo "restore accepted a symlink-bearing archive" >&2 + exit 1 +fi +grep -Eq 'symlink|hardlink|device|fifo|socket|archive' "$tmp/symlink.err" +if grep -q "compose --project-name $project stop qdrant" "$symlink_log"; then + echo "restore stopped qdrant before symlink archive rejection" >&2 + exit 1 +fi + +restore_source="$tmp/restore-source" +mkdir -p "$restore_source/payload/collections/demo" +cat >"$restore_source/manifest.env" <"$restore_source/payload/collections/demo/state.json" +restore_input="$tmp/restore.tar" +tar -C "$restore_source" -cf "$restore_input" manifest.env payload + +if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/confirm.log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project wrong-project \ + >"$tmp/confirm.out" 2>"$tmp/confirm.err"; then echo "restore skipped explicit project confirmation" >&2 exit 1 fi -grep -q 'confirmation must match --project-name exactly' "$tmp/restore-refuse.err" -test "$(cat "$mountpoint/collections/demo/state.json")" = before-backup +grep -q 'confirmation must match --project-name exactly' "$tmp/confirm.err" -printf '%s' modified-live >"$mountpoint/collections/demo/state.json" +printf '%s' modified-live >"$volume_dir/collections/demo/state.json" restore_log="$tmp/restore-ok.log" -PATH="$fakebin:$PATH" DOCKER_LOG="$restore_log" PROJECT_NAME="$project" MOUNTPOINT="$mountpoint" \ +PATH="$fakebin:$PATH" DOCKER_LOG="$restore_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ ./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project "$project" >/dev/null -test "$(cat "$mountpoint/collections/demo/state.json")" = restored +test "$(cat "$volume_dir/collections/demo/state.json")" = restored grep -q "compose --project-name $project stop qdrant" "$restore_log" grep -q "compose --project-name $project start qdrant" "$restore_log" -grep -q "volume inspect --format {{ .Mountpoint }} $volume_name" "$restore_log" +grep -q "run --rm --mount type=volume,src=$volume_name,dst=/qdrant-data --mount type=bind,src=$tmp,dst=/restore-backup,readonly" "$restore_log" +if grep -q "volume inspect --format {{ .Mountpoint }}" "$restore_log"; then + echo "restore consulted Docker mountpoints during normal restore" >&2 + exit 1 +fi +if grep -q "prune" "$restore_log"; then + echo "restore attempted global docker cleanup" >&2 + exit 1 +fi -echo "qdrant backup/restore target resolution, refusal, and service-state contracts passed." +printf '%s' rollback-source >"$volume_dir/collections/demo/state.json" +rollback_log="$tmp/restore-rollback.log" +if PATH="$fakebin:$PATH" DOCKER_LOG="$rollback_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \ +RUN_FAIL_AFTER_CLEANUP=1 HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \ + ./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project "$project" \ + >"$tmp/rollback.out" 2>"$tmp/rollback.err"; then + echo "restore ignored helper failure after cleanup" >&2 + exit 1 +fi +test "$(cat "$volume_dir/collections/demo/state.json")" = rollback-source +grep -q "compose --project-name $project stop qdrant" "$rollback_log" +grep -q "compose --project-name $project start qdrant" "$rollback_log" + +echo "qdrant backup/restore archive validation, scoped helper execution, and rollback safety passed." diff --git a/scripts/vector-backup.sh b/scripts/vector-backup.sh index a449215d..47c3f52c 100755 --- a/scripts/vector-backup.sh +++ b/scripts/vector-backup.sh @@ -1,6 +1,10 @@ #!/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 @@ -23,37 +27,43 @@ 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}" + resolve_volume() { names=$(docker volume ls \ --filter "label=com.docker.compose.project=$project_name" \ - --filter "label=com.docker.compose.volume=qdrant-data" \ + --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 } - printf '%s\n' "$names" | sed -n '/./{p;q;}' + 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" } -resolve_mountpoint() { - mountpoint=$(docker volume inspect --format '{{ .Mountpoint }}' "$1") - [ -n "$mountpoint" ] || { echo "docker did not return a qdrant-data mountpoint" >&2; exit 2; } - case "$mountpoint" in - /*) ;; - *) echo "qdrant-data mountpoint is not absolute: $mountpoint" >&2; exit 2 ;; - esac - [ -d "$mountpoint" ] || { echo "qdrant-data mountpoint is not a directory: $mountpoint" >&2; exit 2; } - printf '%s\n' "$mountpoint" +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; } } volume_name=$(resolve_volume) -mountpoint=$(resolve_mountpoint "$volume_name") +validate_volume_metadata "$volume_name" + running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant) restart_qdrant=0 +temporary_output= cleanup() { status=$? - if [ "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then + if [ -n "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then rm -f "$temporary_output" fi if [ "$restart_qdrant" -eq 1 ]; then @@ -70,7 +80,35 @@ fi umask 077 temporary_output=$(mktemp "$output_dir/.${output_name}.tmp.XXXXXX") -tar -C "$mountpoint" -cf "$temporary_output" . +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 diff --git a/scripts/vector-restore.sh b/scripts/vector-restore.sh index 5faa51d3..cc26b6d6 100755 --- a/scripts/vector-restore.sh +++ b/scripts/vector-restore.sh @@ -1,6 +1,10 @@ #!/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 --input FILE --confirm-project NAME" >&2 exit 2 @@ -25,49 +29,149 @@ done } [ -r "$input" ] || { echo "backup input is not readable" >&2; exit 2; } +input_dir=$(dirname "$input") +input_name=$(basename "$input") +expected_volume_name="${project_name}_${volume_role}" + resolve_volume() { names=$(docker volume ls \ --filter "label=com.docker.compose.project=$project_name" \ - --filter "label=com.docker.compose.volume=qdrant-data" \ + --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 } - printf '%s\n' "$names" | sed -n '/./{p;q;}' + 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" } -resolve_mountpoint() { - mountpoint=$(docker volume inspect --format '{{ .Mountpoint }}' "$1") - [ -n "$mountpoint" ] || { echo "docker did not return a qdrant-data mountpoint" >&2; exit 2; } - case "$mountpoint" in - /*) ;; - *) echo "qdrant-data mountpoint is not absolute: $mountpoint" >&2; exit 2 ;; - esac - [ -d "$mountpoint" ] || { echo "qdrant-data mountpoint is not a directory: $mountpoint" >&2; exit 2; } - printf '%s\n' "$mountpoint" +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; } } +validate_archive_paths() { + paths=$(tar -tf "$input") || { + echo "archive listing failed" >&2 + exit 2 + } + saw_manifest=0 + saw_payload=0 + while IFS= read -r path; do + [ -n "$path" ] || continue + case "$path" in + /*) + echo "archive contains absolute paths" >&2 + exit 2 + ;; + esac + case "/$path/" in + */../*|*/..//*) + echo "archive contains parent traversal" >&2 + exit 2 + ;; + esac + case "$path" in + manifest.env) + saw_manifest=$((saw_manifest + 1)) + ;; + payload|payload/*) + case "$path" in + payload/*) saw_payload=1 ;; + esac + ;; + *) + echo "archive contains unexpected top-level layout" >&2 + exit 2 + ;; + esac + done <&2; exit 2; } + [ "$saw_payload" -eq 1 ] || { echo "archive payload is missing" >&2; exit 2; } +} + +validate_archive_types() { + tar -tvf "$input" | while IFS= read -r entry; do + [ -n "$entry" ] || continue + type=$(printf '%.1s' "$entry") + case "$type" in + -|d) ;; + l|h) + echo "archive contains symlink or hardlink entries" >&2 + exit 2 + ;; + b|c|p|s) + echo "archive contains device or special-file entries" >&2 + exit 2 + ;; + *) + echo "archive contains unsupported entry types" >&2 + exit 2 + ;; + esac + done +} + +validate_archive_manifest() { + manifest=$(tar -xOf "$input" manifest.env 2>/dev/null) || { + echo "archive manifest could not be read" >&2 + exit 2 + } + format= + manifest_project= + manifest_volume= + manifest_role= + manifest_helper= + created_utc= + seen= + while IFS='=' read -r key value; do + [ -n "$key" ] || continue + case "$key" in + format) format=$value ;; + project_name) manifest_project=$value ;; + volume_name) manifest_volume=$value ;; + volume_role) manifest_role=$value ;; + helper_image) manifest_helper=$value ;; + created_utc) created_utc=$value ;; + *) + echo "archive manifest contains unexpected fields" >&2 + exit 2 + ;; + esac + seen="$seen $key" + done <&2; exit 2; } + [ "$manifest_project" = "$project_name" ] || { echo "archive project does not match restore target" >&2; exit 2; } + [ "$manifest_volume" = "$expected_volume_name" ] || { echo "archive volume does not match restore target" >&2; exit 2; } + [ "$manifest_role" = "$volume_role" ] || { echo "archive volume role is invalid" >&2; exit 2; } + [ "$manifest_helper" = "$helper_image" ] || { echo "archive helper image is invalid" >&2; exit 2; } + [ -n "$created_utc" ] || { echo "archive manifest is incomplete" >&2; exit 2; } +} + +validate_archive_paths +validate_archive_types +validate_archive_manifest + volume_name=$(resolve_volume) -mountpoint=$(resolve_mountpoint "$volume_name") +validate_volume_metadata "$volume_name" + running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant) restart_qdrant=0 -staging_dir= -extract_dir= - cleanup() { status=$? - if [ "$status" -ne 0 ] && [ -n "${staging_dir:-}" ] && [ -d "${staging_dir:-}" ]; then - find "$mountpoint" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + - find "$staging_dir" -mindepth 1 -maxdepth 1 -exec mv {} "$mountpoint"/ \; - fi - if [ -n "${staging_dir:-}" ] && [ -d "${staging_dir:-}" ]; then - rm -rf "$staging_dir" - fi - if [ -n "${extract_dir:-}" ] && [ -d "${extract_dir:-}" ]; then - rm -rf "$extract_dir" - fi if [ "$restart_qdrant" -eq 1 ]; then docker compose --project-name "$project_name" start qdrant >/dev/null fi @@ -80,16 +184,31 @@ if [ -n "$running_container" ]; then restart_qdrant=1 fi -parent_dir=$(dirname "$mountpoint") -staging_dir=$(mktemp -d "$parent_dir/.qdrant-restore-staging.XXXXXX") -extract_dir=$(mktemp -d "${TMPDIR:-/tmp}/qdrant-restore.XXXXXX") -tar -C "$extract_dir" -xf "$input" +docker run --rm \ + --mount "type=volume,src=$volume_name,dst=/qdrant-data" \ + --mount "type=bind,src=$input_dir,dst=/restore-backup,readonly" \ + "$helper_image" \ + /bin/sh -eu -c ' + input_name=$1 + rollback=$(mktemp -d /tmp/qdrant-rollback.XXXXXX) + extracted=$(mktemp -d /tmp/qdrant-extract.XXXXXX) + restore_original() { + find /qdrant-data -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + cp -R "$rollback"/. /qdrant-data/ + } + cleanup() { + status=$? + if [ "$status" -ne 0 ] && [ -d "$rollback" ]; then + restore_original + fi + rm -rf "$rollback" "$extracted" + exit "$status" + } + trap cleanup EXIT HUP INT TERM + cp -R /qdrant-data/. "$rollback"/ + tar -C "$extracted" -xf "/restore-backup/$input_name" + find /qdrant-data -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + cp -R "$extracted/payload"/. /qdrant-data/ + ' sh "$input_name" -find "$mountpoint" -mindepth 1 -maxdepth 1 -exec mv {} "$staging_dir"/ \; -find "$extract_dir" -mindepth 1 -maxdepth 1 -exec mv {} "$mountpoint"/ \; - -rm -rf "$staging_dir" -staging_dir= -rm -rf "$extract_dir" -extract_dir= echo "Qdrant restore completed into $volume_name for compose project $project_name"