From 2a08be931c7b69111b82a78bb3ccbaeb6d1460b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 11:50:50 +0000 Subject: [PATCH 01/19] fix: replace bad substitution \${} with \${NOCOLOR} in size_calculation Line 377 contained \${} which causes a bash "bad substitution" error, silently swallowing the hint message when input size is not evenly divisible. Also added the missing closing parenthesis. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddpar.sh b/ddpar.sh index dd57755..6ff949f 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -374,7 +374,7 @@ function size_calculation { for ((i=${NUM_JOBS}; i<$((${NUM_JOBS}**2)); i++)); do if [ $((INPUT_SIZE % i)) -eq 0 ] && [ $(( $((INPUT_SIZE / i)) % BLOCKSIZEBYTES)) -eq 0 ]; then #echo "i=${i} - ${INPUT_SIZE}/${NUM_JOBS} = $((INPUT_SIZE % i)) - SPLIT_SIZE: $(( $((INPUT_SIZE / i)) % BLOCKSIZEBYTES))" - echo -e "${SUCCESSCOLOR}INFO: The next higher usable Threadnumber is $i (at same Blocksize of ${BLOCKSIZEBYTES}${}" + echo -e "${SUCCESSCOLOR}INFO: The next higher usable Threadnumber is $i (at same Blocksize of ${BLOCKSIZEBYTES})${NOCOLOR}" break fi done From 266f50da505945ea18adf241b0a11a4c71e3ef97 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 11:53:30 +0000 Subject: [PATCH 02/19] fix: anchor grep patterns in ddpar-restore.sh to prevent multi-line matches grep "FILE_NAME" matched both FILE_NAME= and INPUT_FILE_NAME=, grep "COMPRESSION" matched both COMPRESSION= and COMPRESSION_LEVEL=. Added ^ anchor so each pattern only matches its intended metadata key. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar-restore.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddpar-restore.sh b/ddpar-restore.sh index 2ae39b1..f931158 100755 --- a/ddpar-restore.sh +++ b/ddpar-restore.sh @@ -83,12 +83,12 @@ if [ ! -e "$METADATA_FILE" ]; then exit 1 fi NUM_JOBS=$(grep "NUM_JOBS" $METADATA_FILE | cut -d "=" -f 2) -FILE_NAME=$(grep "FILE_NAME" $METADATA_FILE | cut -d "=" -f 2) +FILE_NAME=$(grep "^FILE_NAME=" $METADATA_FILE | cut -d "=" -f 2) SPLIT_SIZE=$(grep "SPLIT_SIZE" $METADATA_FILE | cut -d "=" -f 2) INPUT_SIZE=$(grep "INPUT_SIZE" $METADATA_FILE | cut -d "=" -f 2) INPUT_FILE_TYPE=$(grep "FILE_TYPE" $METADATA_FILE | cut -d "=" -f 2) BLOCKSIZEBYTES=$(grep "BLOCKSIZEBYTES" $METADATA_FILE | cut -d "=" -f 2) -COMPRESSION=$(grep "COMPRESSION" $METADATA_FILE | cut -d "=" -f 2) +COMPRESSION=$(grep "^COMPRESSION=" $METADATA_FILE | cut -d "=" -f 2) COMPRESSION_LEVEL=$(grep "COMPRESSION_LEVEL" $METADATA_FILE | cut -d "=" -f 2) # Überprüfung der erforderlichen Parameter From 209833e78f0581d8c4bbb142ba857f44fdde9338 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 11:55:31 +0000 Subject: [PATCH 03/19] fix: use consistent ss -tln (TCP only) for remote port checks Data transfer uses nc over TCP, so UDP monitoring (-u flag) is irrelevant. Removed -u from the two process-running checks to match check_remote_port_availability which already used -tln. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddpar.sh b/ddpar.sh index 6ff949f..074e6de 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -515,7 +515,7 @@ function clone_file { echo -e "${INFOCOLOR}Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)...${NOCOLOR}" # Remote-Befehl zum Prüfen, ob der Prozess auf dem Port läuft - if execute_remote_command "ss -tuln | grep -q :${CURRENT_REMOTE_PORT}"; then + if execute_remote_command "ss -tln | grep -q :${CURRENT_REMOTE_PORT}"; then echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}" break else @@ -629,7 +629,7 @@ function clone_block { echo -e "${INFOCOLOR}Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)...${NOCOLOR}" # Remote-Befehl zum Prüfen, ob der Prozess auf dem Port läuft - if execute_remote_command "ss -tuln | grep -q :${CURRENT_REMOTE_PORT}"; then + if execute_remote_command "ss -tln | grep -q :${CURRENT_REMOTE_PORT}"; then echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}" break else From 16ea99e9ce22e2e0fca2fabab58bf127ce9810ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 11:58:36 +0000 Subject: [PATCH 04/19] fix: prevent port substring false-positives in ss grep patterns grep -q \":PORT\" matched partial ports (e.g. :100 matched :10000). Changed to grep -qE with [^0-9] suffix so only the exact port number matches. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ddpar.sh b/ddpar.sh index 074e6de..9b99808 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -348,7 +348,7 @@ function remote_port_generation { function check_remote_port_availability { [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 - execute_remote_command "ss -tln | grep -q \":${CURRENT_REMOTE_PORT}\"" + execute_remote_command "ss -tln | grep -qE \":${CURRENT_REMOTE_PORT}[^0-9]\"" # Port is free, if exit code is not zero if [[ $? != 0 ]]; then return 0 @@ -515,7 +515,7 @@ function clone_file { echo -e "${INFOCOLOR}Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)...${NOCOLOR}" # Remote-Befehl zum Prüfen, ob der Prozess auf dem Port läuft - if execute_remote_command "ss -tln | grep -q :${CURRENT_REMOTE_PORT}"; then + if execute_remote_command "ss -tln | grep -qE :${CURRENT_REMOTE_PORT}[^0-9]"; then echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}" break else @@ -629,7 +629,7 @@ function clone_block { echo -e "${INFOCOLOR}Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)...${NOCOLOR}" # Remote-Befehl zum Prüfen, ob der Prozess auf dem Port läuft - if execute_remote_command "ss -tln | grep -q :${CURRENT_REMOTE_PORT}"; then + if execute_remote_command "ss -tln | grep -qE :${CURRENT_REMOTE_PORT}[^0-9]"; then echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}" break else From b617f2af6b43fd0c8306f70eb86aba066c3148aa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 11:58:57 +0000 Subject: [PATCH 05/19] fix: quote INPUT and OUTPUT path variables to handle paths with spaces Unquoted \${INPUT} and \${OUTPUT} in file, stat, and blockdev calls are subject to word splitting. Also fixed two \${INPUT_SIZE=} typos to \${INPUT_SIZE} in the same block. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ddpar.sh b/ddpar.sh index 9b99808..8b4446e 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -306,17 +306,17 @@ function input_analysis { [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 # Determine the type of the input file echo -e "${INFOCOLOR}Analysiere INPUT${NOCOLOR}" - INPUT_FILE_TYPE=$(file -b ${INPUT}) + INPUT_FILE_TYPE=$(file -b "${INPUT}") echo "\$INPUT_FILE_TYPE = $INPUT_FILE_TYPE" - + # Use the appropriate command to calculate the size of the input file if [[ "${INPUT_FILE_TYPE}" == "block special"* ]]; then #echo "INPUT_SIZE=$(blockdev --getsize64 $INPUT)" - INPUT_SIZE=$(blockdev --getsize64 ${INPUT}) - echo "\$INPUT_SIZE=${INPUT_SIZE=}" + INPUT_SIZE=$(blockdev --getsize64 "${INPUT}") + echo "\$INPUT_SIZE=${INPUT_SIZE}" else - INPUT_SIZE=$(stat -c %s ${INPUT}) - echo "\$INPUT_SIZE=${INPUT_SIZE=}" + INPUT_SIZE=$(stat -c %s "${INPUT}") + echo "\$INPUT_SIZE=${INPUT_SIZE}" fi } @@ -324,15 +324,15 @@ function output_analysis { [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 # Determine the type of the output file echo -e "${INFOCOLOR}Analysiere OUTPUT${NOCOLOR}" - OUTPUT_FILE_TYPE=$(execute_command "file -b ${OUTPUT}") - + OUTPUT_FILE_TYPE=$(execute_command "file -b \"${OUTPUT}\"") + # Use the appropriate command to calculate the size of the output file echo "\$OUTPUT_FILE_TYPE: ${OUTPUT_FILE_TYPE}" if [[ "${OUTPUT_FILE_TYPE}" == "block special"* ]]; then - OUTPUT_SIZE=$(execute_command "blockdev --getsize64 ${OUTPUT}") + OUTPUT_SIZE=$(execute_command "blockdev --getsize64 \"${OUTPUT}\"") echo "\$OUTPUT_SIZE = $OUTPUT_SIZE" else - OUTPUT_SIZE=$(execute_command "stat -c %s ${OUTPUT}") + OUTPUT_SIZE=$(execute_command "stat -c %s \"${OUTPUT}\"") echo "\$OUTPUT_SIZE = $OUTPUT_SIZE" fi echo -e "${INFOCOLOR}${FUNCNAME[0]} abgeschlossen${NOCOLOR}" From 124a4009756477ffee72ccdfadc6062ee194abfa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 12:12:17 +0000 Subject: [PATCH 06/19] fix: add missing -e flag to echo in backup mode for color output Without -e the ANSI escape codes were printed as literal text instead of being interpreted as colors. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddpar.sh b/ddpar.sh index 8b4446e..6449232 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -778,7 +778,7 @@ case $MODE in OUTPUT_CMD="dd of=${OUTPUT_FILE}${PART_NUM}.part bs=${BLOCKSIZEBYTES}" FULL_CMD="${FULL_CMD} | $OUTPUT_CMD &" fi - echo "${INFOCOLOR}${FULL_CMD}${NOCOLOR}" + echo -e "${INFOCOLOR}${FULL_CMD}${NOCOLOR}" eval "${FULL_CMD}" done From 42b6ab0b89bd471c72291182ac8f05591990439d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 12:12:36 +0000 Subject: [PATCH 07/19] fix: move fallocate out of restore loop to only run once fallocate pre-allocates the output file once before writing starts. Running it once per job caused redundant syscalls and potential conflicts when jobs ran in parallel. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar-restore.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ddpar-restore.sh b/ddpar-restore.sh index f931158..05c3020 100755 --- a/ddpar-restore.sh +++ b/ddpar-restore.sh @@ -39,6 +39,10 @@ function restore_split_image { # done echo "Starte die Prozesse ..." + if [[ ${OUTPUT_FILE_TYPE} != "block special"* ]]; then + echo "fallocate -l ${INPUT_SIZE} $OUTPUT_FILE" + fallocate -l ${INPUT_SIZE} $OUTPUT_FILE + fi for ((PART_NUM=0; PART_NUM<${NUM_JOBS}; PART_NUM++)); do # Build individual subcommands and concatinate, if enabled if [ ! -z "$COMPRESSION" ]; then @@ -56,11 +60,6 @@ function restore_split_image { FULL_CMD="${INPUT_CMD}" OUTPUT_CMD="dd of=${OUTPUT_FILE} bs=${BLOCKSIZEBYTES} count=$((SPLIT_SIZE / ${BLOCKSIZEBYTES})) seek=$((START / ${BLOCKSIZEBYTES})) iflag=fullblock" FULL_CMD="${FULL_CMD} | $OUTPUT_CMD &" - if [[ ${OUTPUT_FILE_TYPE} != "block special"* ]]; then - #touch $OUTPUT_FILE - echo "fallocate -l ${INPUT_SIZE} $OUTPUT_FILE" - fallocate -l ${INPUT_SIZE} $OUTPUT_FILE - fi echo "$FULL_CMD" eval "${FULL_CMD}" done From 05c4fb37c1e28cb6523b43cbac2d3f414be1388b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 12:12:51 +0000 Subject: [PATCH 08/19] fix: correct RANDOM port range comment and simplify to one line RANDOM is 0-32767, so RANDOM % 55001 is identical to RANDOM. The comments claimed a range of 10000-55000 which was never reachable. Simplified to REMOTE_PORT=$(( RANDOM + 10000 )) with accurate comment. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ddpar.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ddpar.sh b/ddpar.sh index 6449232..c4a41d2 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -340,10 +340,8 @@ function output_analysis { function remote_port_generation { [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 - # Generiere eine Zufallszahl zwischen 0 und 45000 - REMOTE_PORT=$(( RANDOM % 55001 )) - # Füge 10000 hinzu, um den Bereich auf 10000 bis 55000 zu erweitern und addiere zusätzlich - REMOTE_PORT=$(( REMOTE_PORT + 10000 )) + # RANDOM yields 0-32767, so the effective port range is 10000-42767 + REMOTE_PORT=$(( RANDOM + 10000 )) } function check_remote_port_availability { From a1fb82e83b3723b29806a4f044c08a237494be6d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 06:36:40 +0000 Subject: [PATCH 09/19] docs: add TESTING.md with example commands for all transfer scenarios Covers prerequisites, local clone/backup/restore (compressed and uncompressed), check scenarios, remote netcat/SSH modes, and additional options (-s, -j, -b, -d, -f). https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- TESTING.md | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..92fa73e --- /dev/null +++ b/TESTING.md @@ -0,0 +1,274 @@ +# TESTING + +Dieses Dokument enthält Beispiel-Kommandos zum Testen aller implementierten Funktionen von ddpar. + +--- + +## Voraussetzungen + +### Variablen definieren + +Die folgenden Variablen in der Shell setzen und in allen Kommandos verwenden: + +```bash +# Lokal – Quelllaufwerk (Block Device) +SOURCE_DEV=/dev/sdb + +# Lokal – Ziellaufwerk für Clone (Block Device, mindestens so groß wie SOURCE_DEV) +DEST_DEV=/dev/sdc + +# Lokal – Testdatei (wird unter "Testdaten erstellen" angelegt) +SOURCE_FILE=/tmp/ddpar_test.img + +# Lokal – Zielverzeichnis für File-Clone +DEST_DIR=/tmp/ddpar_clone_dest + +# Lokal – Backup-Verzeichnis +BACKUP_DIR=/mnt/backup + +# Remote – SSH-Verbindung +REMOTE_HOST=user@192.168.1.100 + +# Remote – Ziellaufwerk auf dem Remote-Host (Block Device) +REMOTE_DEST_DEV=/dev/sdb + +# Remote – Zielverzeichnis für File-Clone auf dem Remote-Host +REMOTE_DEST_DIR=/tmp/ddpar_clone_dest +``` + +### Testdaten erstellen + +Eine 64 MiB große Testdatei anlegen (teilbar durch 4 Jobs × 1 MiB Blocksize): + +```bash +dd if=/dev/urandom of=$SOURCE_FILE bs=1M count=64 +``` + +Zielverzeichnisse anlegen: + +```bash +mkdir -p $DEST_DIR +mkdir -p $BACKUP_DIR +``` + +--- + +## 1. Lokale Tests – unkomprimiert + +### 1.1 Clone – Block Device + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $DEST_DEV -m clone +``` + +> **Hinweis:** `$DEST_DEV` muss mindestens so groß sein wie `$SOURCE_DEV`. + +### 1.2 Clone – Datei + +```bash +./ddpar.sh -i $SOURCE_FILE -o $DEST_DIR -m clone +``` + +> **Hinweis:** Der Output `-o` muss ein vorhandenes Verzeichnis sein. Das geklonte +> File wird dort als `ddpar_test.img` abgelegt. + +### 1.3 Backup – Block Device + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $BACKUP_DIR -m backup +``` + +Erzeugte Dateien: `$BACKUP_DIR/sdb-0.part` … `sdb-3.part` + `sdb-metadata.txt` + +### 1.4 Backup – Datei + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup +``` + +Erzeugte Dateien: `$BACKUP_DIR/ddpar_test.img-0.part` … + `ddpar_test.img-metadata.txt` + +### 1.5 Restore – Block Device + +```bash +sudo ./ddpar-restore.sh -i $BACKUP_DIR/sdb -o $DEST_DEV +``` + +> **Hinweis:** Erfordert ein vorher erstelltes Backup aus Test 1.3. +> Der Parameter `-i` ist der Basispfad **ohne** das abschließende `-`. + +### 1.6 Restore – Datei + +```bash +./ddpar-restore.sh -i $BACKUP_DIR/ddpar_test.img -o $DEST_DIR/ddpar_test.img +``` + +> **Hinweis:** Erfordert ein vorher erstelltes Backup aus Test 1.4. + +--- + +## 2. Lokale Tests – komprimiert (gzip) + +### 2.1 Backup – Block Device (gzip) + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $BACKUP_DIR -m backup -c +``` + +Erzeugte Dateien: `$BACKUP_DIR/sdb-0.gz` … `sdb-3.gz` + `sdb-metadata.txt` + +### 2.2 Backup – Datei (gzip) + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -c +``` + +### 2.3 Restore – Block Device (gzip) + +```bash +sudo ./ddpar-restore.sh -i $BACKUP_DIR/sdb -o $DEST_DEV +``` + +> **Hinweis:** Erfordert ein vorher erstelltes komprimiertes Backup aus Test 2.1. +> Das Skript erkennt die Komprimierung automatisch über die Metadaten. + +### 2.4 Restore – Datei (gzip) + +```bash +./ddpar-restore.sh -i $BACKUP_DIR/ddpar_test.img -o $DEST_DIR/ddpar_test.img +``` + +> **Hinweis:** Erfordert ein vorher erstelltes komprimiertes Backup aus Test 2.2. + +--- + +## 3. Prüfung (ddpar-check.sh) + +> **Hinweis:** Die Prüfung eines Clones ist laut Status-Tabelle noch nicht implementiert (🛑). +> Die folgenden Tests setzen ein vorhandenes Backup voraus. + +### 3.1 Quelle gegen Backup prüfen (Source ↔ Backup) + +Block Device: +```bash +sudo ./ddpar-check.sh -b $BACKUP_DIR/sdb -s $SOURCE_DEV +``` + +Datei: +```bash +./ddpar-check.sh -b $BACKUP_DIR/ddpar_test.img -s $SOURCE_FILE +``` + +### 3.2 Backup gegen Ziel prüfen (Backup ↔ Destination) + +Block Device: +```bash +sudo ./ddpar-check.sh -b $BACKUP_DIR/sdb -d $DEST_DEV +``` + +Datei: +```bash +./ddpar-check.sh -b $BACKUP_DIR/ddpar_test.img -d $DEST_DIR/ddpar_test.img +``` + +--- + +## 4. Remote Tests – SSH + Netcat + +> **Voraussetzung:** SSH-Zugang zu `$REMOTE_HOST` muss eingerichtet sein +> (passwortlos per Key oder interaktiv per Passwort). +> `nc` (netcat) und `ss` müssen auf dem Remote-Host verfügbar sein. + +### 4.1 Remote Clone – Block Device, unkomprimiert (Modus n) + +Modus `n`: SSH-Verbindungsaufbau verschlüsselt, Dateiübertragung über Netcat unverschlüsselt. + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_DEST_DEV -m clone -r n -R $REMOTE_HOST +``` + +### 4.2 Remote Clone – Datei, unkomprimiert (Modus n) + +> **Abweichung:** `-o` ist hier ein Verzeichnis auf dem Remote-Host. + +```bash +./ddpar.sh -i $SOURCE_FILE -o $REMOTE_DEST_DIR -m clone -r n -R $REMOTE_HOST +``` + +### 4.3 Remote Clone – Block Device, vollständig verschlüsselt (Modus l) + +Modus `l`: Gesamte Übertragung läuft durch den SSH-Tunnel. + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_DEST_DEV -m clone -r l -R $REMOTE_HOST +``` + +### 4.4 Remote Clone – Block Device, lokale Kompression (Modus n + -c) + +Daten werden lokal mit gzip komprimiert, dann per Netcat übertragen und remote dekomprimiert. + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_DEST_DEV -m clone -r n -c -R $REMOTE_HOST +``` + +### 4.5 Remote Clone – Block Device, remote Kompression (Modus c) + +Kompression findet auf der Remote-Seite statt. + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_DEST_DEV -m clone -r c -R $REMOTE_HOST +``` + +### 4.6 Remote Backup – nicht implementiert 🛑 + +Remote Backup (`-m backup` mit `-r`) ist laut Status-Tabelle noch nicht implementiert. + +### 4.7 Remote Restore – nicht implementiert 🛑 + +Remote Restore über `ddpar-restore.sh` ist laut Status-Tabelle noch nicht implementiert. + +--- + +## 5. Zusatzoptionen + +Die folgenden Optionen können mit den meisten Szenarien oben kombiniert werden. + +### 5.1 Checksummen aktivieren (-s) + +Beim Backup wird pro Teil eine SHA256-Checksumme erstellt: + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -s +``` + +### 5.2 Anzahl der Jobs anpassen (-j) + +> **Abweichung:** Die Eingabegröße muss durch die Anzahl der Jobs und die Blockgröße +> gleichmäßig teilbar sein. Bei `$SOURCE_FILE` (64 MiB) sind z.B. 2 oder 8 Jobs möglich. + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -j 2 +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -j 8 +``` + +### 5.3 Blockgröße anpassen (-b) + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -b 4194304 +``` + +### 5.4 Debug-Modus (-d) + +> **Achtung:** Im Debug-Modus werden Passwörter im Klartext ausgegeben. + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -d +``` + +### 5.5 Force-Modus (-f) + +Überschreibt vorhandene Dateien oder ignoriert Speicherplatz-Warnungen: + +```bash +./ddpar.sh -i $SOURCE_FILE -o $BACKUP_DIR -m backup -f +``` From f099ff1ab774cbca388a2cb666bc3ccb7c6212f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:34:55 +0000 Subject: [PATCH 10/19] docs: add CLAUDE.md with project context for Claude Code sessions Covers file structure, coding conventions, metadata format, known limitations, and branch strategy. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- CLAUDE.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9e89273 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,68 @@ +# CLAUDE.md – Projektkontext für Claude Code + +## Projektübersicht + +**ddpar** (dd parallel) ist eine Sammlung von Bash-Skripten zum parallelen Klonen, +Sichern und Wiederherstellen von Block-Devices und Dateien mittels `dd`. +Die Parallelisierung erfolgt durch Aufteilung der Eingabe in gleichgroße Segmente, +die gleichzeitig über separate `dd`-Prozesse verarbeitet werden. + +## Dateien + +| Datei | Aufgabe | +|---|---| +| `ddpar.sh` | Hauptskript: Clone- und Backup-Modus, lokal und remote | +| `ddpar-restore.sh` | Wiederherstellen eines mit `ddpar.sh -m backup` erstellten Backups | +| `ddpar-check.sh` | Prüfung der Integrität via SHA256 (Quelle↔Backup oder Backup↔Ziel) | +| `TESTING.md` | Manuelle Testszenarien mit Beispielkommandos | +| `ARCHITECTURE.md` | Design-Dokumentation: Parallelisierung, Pipes, Remote-Konzept | +| `CHANGELOG.md` | Versionshistorie und Bug-Fix-Dokumentation | + +## Coding-Konventionen + +- **Shell:** Bash (`#!/bin/bash`), keine POSIX-only-Syntax erforderlich +- **Linter:** ShellCheck (siehe `.shellcheckrc`). Vor jedem Commit ausführen: `make lint` +- **Variablen:** Immer in doppelten Anführungszeichen, wenn sie Pfade enthalten können +- **`eval`:** Immer `eval "${VAR}"` mit Quotes — nie `eval $VAR` +- **Tests:** `[ -z "$VAR" ]` mit Quotes — nie `[ -z $VAR ]` +- **grep in Metadaten:** Immer mit `^`-Anker, z.B. `grep "^KEY="`, um Prefix-Matches zu vermeiden +- **Farb-Echo:** Immer `echo -e` wenn ANSI-Codes (`${INFOCOLOR}` etc.) ausgegeben werden +- **Port-Prüfung:** `grep -qE ":PORT[^0-9]"` — niemals `:PORT` ohne Suffix (Substring-Matching) + +## Metadaten-Format + +Backup-Metadatei `-metadata.txt` — zeilenweise `KEY=VALUE`: + +``` +NUM_JOBS=4 +FILE_NAME=sdb +BLOCKSIZEBYTES=1048576 +INPUT_SIZE=68719476736 +INPUT_FILE_NAME=sdb +FILE_TYPE=block special (8/16) +SPLIT_SIZE=17179869184 +COMPRESSION=1 # nur vorhanden wenn komprimiert +COMPRESSION_LEVEL=6 # nur vorhanden wenn komprimiert +``` + +## Änderungen validieren + +```bash +make lint # ShellCheck auf alle .sh-Dateien +``` + +Manuelle Tests: siehe `TESTING.md`. + +## Bekannte Einschränkungen / offene Baustellen + +- Remote Backup und Remote Restore sind noch nicht implementiert (🛑 in README) +- Clone-Check (`ddpar-check.sh` nach Clone) ist noch nicht implementiert +- `RANDOM` in Bash liefert nur 0–32767 → Remote-Ports werden aus dem Bereich 10000–42767 gewählt +- `fallocate` funktioniert nicht auf Block-Devices (wird korrekt übersprungen) +- Eingabegröße muss durch `NUM_JOBS × BLOCKSIZEBYTES` ganzzahlig teilbar sein +- Remote-Modus-Flags (`-r l/n/c`) sind noch nicht vollständig implementiert + +## Branch-Strategie + +- `main` — stabiler Stand +- `claude/*` — von Claude Code erstellte Feature-/Fix-Branches From a066c9801c4e7a7fdd42860b1afa8cd24d8ac740 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:35:07 +0000 Subject: [PATCH 11/19] config: add .shellcheckrc for bash linting Sets shell=bash and disables SC2034 (unused color variables) and SC2207 (array splitting) which are intentional patterns in this codebase. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- .shellcheckrc | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .shellcheckrc diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..c6dd3cb --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,11 @@ +# ShellCheck-Konfiguration für ddpar +shell=bash + +# SC2034: Variable assigned but not used +# Wird für Farbvariablen (NOCOLOR, INFOCOLOR etc.) ignoriert, +# da sie nur gesetzt werden wenn das Terminal Farben unterstützt. +disable=SC2034 + +# SC2207: Prefer mapfile or read -a to split command output into array +# Wird vorerst ignoriert, da die betroffenen Stellen keine Arrays benötigen. +disable=SC2207 From a809370f0b09ee93c3e30a3ab7cd4a462aed0a10 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:35:43 +0000 Subject: [PATCH 12/19] docs: add ARCHITECTURE.md with design documentation Covers parallelization concept, dynamic pipe construction, metadata format, SSH multiplexing, port selection, and function overview. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- ARCHITECTURE.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1519bd2 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,146 @@ +# Architektur + +## Grundprinzip: Paralleles `dd` + +ddpar teilt die Eingabe in `NUM_JOBS` gleichgroße Segmente auf. Jedes Segment +wird von einem eigenen `dd`-Prozess gelesen und geschrieben. Die Prozesse laufen +gleichzeitig im Hintergrund (`&`), am Ende wartet das Skript mit `wait` auf alle. + +### Segmentberechnung + +``` +SPLIT_SIZE = INPUT_SIZE / NUM_JOBS + +Segment N: + Lesen: dd if=INPUT skip=$((N * SPLIT_SIZE / BLOCKSIZEBYTES)) count=$((SPLIT_SIZE / BLOCKSIZEBYTES)) + Schreiben: dd of=OUTPUT seek=$((N * SPLIT_SIZE / BLOCKSIZEBYTES)) +``` + +`INPUT_SIZE` muss durch `NUM_JOBS × BLOCKSIZEBYTES` ganzzahlig teilbar sein. +Das Skript prüft dies in `size_calculation()` und gibt Hinweise auf alternative +Job-Zahlen oder Blockgrößen, falls die Teilung nicht aufgeht. + +--- + +## Pipe-Architektur + +Je nach aktivierten Optionen wird die Pipe dynamisch zusammengebaut in `$FULL_CMD` +und dann per `eval "${FULL_CMD}"` ausgeführt. + +### Backup lokal, unkomprimiert + +``` +dd if=INPUT ... | dd of=OUTPUT_PART ... & +``` + +### Backup lokal, mit Checksumme + +``` +dd if=INPUT ... | tee >(sha256sum > PART.sha256) | dd of=OUTPUT_PART ... & +``` + +### Backup lokal, mit gzip-Kompression + +``` +dd if=INPUT ... | gzip -LEVEL > OUTPUT_PART.gz & +``` + +### Backup lokal, mit Checksumme + gzip + +``` +dd if=INPUT ... | tee >(sha256sum > PART.sha256) | gzip -LEVEL > OUTPUT_PART.gz & +``` + +### Remote Clone (Netcat) + +**Lokal:** +``` +dd if=INPUT ... | nc REMOTE_HOST PORT & +``` + +**Remote (startet zuerst als Listener):** +``` +nc -N -l PORT | dd of=OUTPUT ... & (läuft im Hintergrund via nohup + SSH) +``` + +Das Skript prüft nach dem Start des Remote-Listeners per `ss -tln`, ob der Port +tatsächlich belegt ist (bis zu 3 Versuche mit 1 s Pause) bevor der lokale +`nc`-Client verbindet. + +--- + +## Metadaten-Datei + +Beim Backup schreibt `ddpar.sh` eine Metadaten-Datei `-metadata.txt`, +die alle Parameter enthält, die `ddpar-restore.sh` und `ddpar-check.sh` für +die Rekonstruktion benötigen: + +``` +NUM_JOBS=4 +FILE_NAME=sdb +BLOCKSIZEBYTES=1048576 +INPUT_SIZE=68719476736 +INPUT_FILE_NAME=sdb +FILE_TYPE=block special (8/16) +SPLIT_SIZE=17179869184 +COMPRESSION=1 # nur wenn komprimiert +COMPRESSION_LEVEL=6 # nur wenn komprimiert +``` + +**Wichtig:** Alle `grep`-Zugriffe auf diese Datei müssen mit `^`-Anker arbeiten +(`grep "^KEY="`), da sonst Präfix-Matches auftreten können (z.B. `COMPRESSION` +matcht auch `COMPRESSION_LEVEL`). + +--- + +## Remote-Konzept + +### SSH-Multiplexing + +`ddpar.sh` öffnet eine persistente SSH-Master-Verbindung über einen Unix-Socket +(`SSH_SOCKET_PATH`). Alle weiteren SSH-Aufrufe (Befehle, Hintergrundprozesse) +laufen über diesen Kontroll-Socket, ohne erneute Authentifizierung. + +``` +ssh -o ControlMaster=auto -o ControlPersist=yes -S SOCKET HOST true +``` + +### Port-Auswahl + +Für jeden Job wird ein Port aus dem Bereich **10000–42767** zufällig gewählt +(`REMOTE_PORT + PART_NUM`). Vor der Nutzung prüft `check_remote_port_availability()` +via `ss -tln`, ob der Port auf dem Remote-Host frei ist. Bei Kollision wird +ein neuer Port generiert. + +### Remote-Modi (`-r`) + +| Flag | Bedeutung | Implementierungsstatus | +|---|---|---| +| `n` | Netcat ohne Datenverschlüsselung | ✅ | +| `l` | Vollständig über SSH (verschlüsselt) | ⚙️ teilweise | +| `c` | Kompression auf der Remote-Seite | ⚙️ teilweise | + +--- + +## Funktionsübersicht (`ddpar.sh`) + +| Funktion | Aufgabe | +|---|---| +| `option_analysis` | Kommandozeilenparameter parsen und validieren | +| `set_colors` | ANSI-Farbvariablen setzen (nur wenn Terminal Farben unterstützt) | +| `establish_ssh_connection` | SSH-Verbindung aufbauen (mit oder ohne Passwort via sshpass) | +| `connect_ssh` | SSH-Verbindung prüfen und ggf. aufbauen | +| `is_ssh_socket_alive` | Prüft ob der SSH-Kontroll-Socket noch aktiv ist | +| `execute_command` | Befehl lokal oder remote ausführen | +| `execute_remote_command` | Befehl immer remote via SSH ausführen | +| `execute_remote_background_command` | Befehl remote im Hintergrund starten (nohup) | +| `close_ssh_connection` | SSH-Multiplexing-Verbindung schließen | +| `check_commands_availability` | Prüft ob benötigte Tools lokal vorhanden sind | +| `check_remote_commands_availability` | Prüft ob benötigte Tools remote vorhanden sind | +| `input_analysis` | Typ und Größe der Eingabe bestimmen | +| `output_analysis` | Typ und Größe des Ziels bestimmen | +| `remote_port_generation` | Zufälligen Port im Bereich 10000–42767 generieren | +| `check_remote_port_availability` | Prüft ob ein Port auf dem Remote-Host frei ist | +| `size_calculation` | SPLIT_SIZE berechnen, Teilbarkeit prüfen | +| `clone_file` | Paralleler Clone einer regulären Datei | +| `clone_block` | Paralleler Clone eines Block-Devices | From cb4c0bd135e3a95e68006c144c60c49adc02ae06 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:35:55 +0000 Subject: [PATCH 13/19] config: add Makefile with lint target Provides 'make lint' to run ShellCheck on all scripts and 'make install-deps' for installing ShellCheck on Debian/Ubuntu. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- Makefile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15b6bae --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +SCRIPTS := ddpar.sh ddpar-restore.sh ddpar-check.sh + +.PHONY: lint install-deps help + +help: + @echo "Verfügbare Targets:" + @echo " make lint ShellCheck auf alle .sh-Dateien ausführen" + @echo " make install-deps ShellCheck installieren (Debian/Ubuntu)" + +lint: + @command -v shellcheck > /dev/null 2>&1 || \ + { echo "shellcheck nicht gefunden. Installation: make install-deps"; exit 1; } + shellcheck $(SCRIPTS) + +install-deps: + sudo apt-get install -y shellcheck From d729fb1cd95a242e8577fe1045c9e4b7b0e8eefa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:36:26 +0000 Subject: [PATCH 14/19] docs: add CHANGELOG.md documenting all bug fixes from this session Lists all 13 fixes with commit hashes and short explanation of each bug and its impact. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c24e96c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## [Unreleased] – branch claude/review-ddpar-bugs-bc3la + +### Hinzugefügt +- `TESTING.md` mit Beispielkommandos für alle Übertragungsvarianten + +### Behoben +- **ddpar.sh:** `${}` Bad-Substitution in `size_calculation()` — der Hinweis auf + die nächsthöhere Thread-Zahl wurde nie ausgegeben, stattdessen ein Shell-Fehler + produziert (`2a08be9`) +- **ddpar-restore.sh:** `grep "COMPRESSION"` matchte auch `COMPRESSION_LEVEL=` + und `grep "FILE_NAME"` matchte auch `INPUT_FILE_NAME=`; Muster mit `^`-Anker + verankert (`266f50d`) +- **ddpar.sh:** `ss -tln` vs. `ss -tuln` Inkonsistenz — alle Port-Prüfungen + nutzen jetzt einheitlich `ss -tln` (nur TCP, da Netcat TCP verwendet) (`209833e`) +- **ddpar.sh:** Port-Substring-Matching — `grep -q ":PORT"` matchte z.B. + Port `100` in `:10000`; ersetzt durch `grep -qE ":PORT[^0-9]"` (`16ea99e`) +- **ddpar.sh:** Unquotierte Pfadvariablen `${INPUT}` und `${OUTPUT}` in `file`, + `stat`, `blockdev`-Aufrufen; Tippfehler `${INPUT_SIZE=}` → `${INPUT_SIZE}` + (`b617f2a`) +- **ddpar.sh:** Fehlendes `-e` bei `echo` im Backup-Modus — ANSI-Farbcodes + wurden als Klartext ausgegeben (`124a400`) +- **ddpar-restore.sh:** `fallocate` wurde innerhalb der Job-Schleife für jeden + Job aufgerufen statt einmal vor der Schleife (`42b6ab0`) +- **ddpar.sh:** `RANDOM % 55001` ist identisch zu `RANDOM` da `RANDOM` max. + 32767 liefert; vereinfacht zu `REMOTE_PORT=$(( RANDOM + 10000 ))`, Kommentar + korrigiert auf effektiven Bereich 10000–42767 (`05c4fb3`) + +--- + +## [Merged] – PR #1 (branch claude/review-ddpar-bugs-bc3la → main) + +### Behoben +- **ddpar.sh:** Off-by-One in Post-Loop-Bedingung — Erfolg beim letzten Versuch + (ATTEMPT == MAX_ATTEMPTS) wurde fälschlich als Fehler gewertet (`-ge` → `-gt`) + (`075de3c`) +- **ddpar.sh, ddpar-restore.sh:** Unquotiertes `eval ${FULL_CMD}` — durch + Word-Splitting und Glob-Expansion konnte es zu unvorhersehbarer Befehlsausführung + kommen; ersetzt durch `eval "${FULL_CMD}"` (`927b3a8`) +- **ddpar-check.sh:** `[ ! -z $SOURCE ]` und `[ ! -z $DESTINATION ]` ohne Quotes + — scheitert bei leeren Variablen; Quotes ergänzt (`bf1a62f`) +- **ddpar-restore.sh:** Größenprüfung verglich `BLOCKSIZEBYTES` (~1 MB) mit + `OUTPUT_SIZE` statt `INPUT_SIZE` (Backup-Größe) — sinnloser Vergleich, der nie + anschlug (`57b6fce`) +- **ddpar.sh:** Unquotiertes `[ -z ${REMOTE_PORT} ]` — Quotes ergänzt (`323c254`) From 73669684d1f5fbf2bb80767e9b02d066877c86b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 15:45:08 +0000 Subject: [PATCH 15/19] feat: implement local clone check (source vs destination) Adds check_cloned_image() which compares source and destination segment by segment via parallel SHA256 hashing, without requiring backup metadata or .sha256 files. New -j and -B options set job count and block size (defaults 4 / 1048576). Fills the previously empty SOURCE+DESTINATION branch and validates divisibility. Marks the local clone-check cells as done in README. https://claude.ai/code/session_015DcyEK7Zc8YCewMCV9Ayum --- README.md | 4 ++-- ddpar-check.sh | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 73f3916..27389ca 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Attempting scripted parallel dd execution. | | clone (check) || backup (check) | restore (check) | |----------|----------|-|----------|----------| -| block dev | :heavy_check_mark: (:stop_sign:) || :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | -| file | :heavy_check_mark: (:stop_sign:) || :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | +| block dev | :heavy_check_mark: (:heavy_check_mark:) || :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | +| file | :heavy_check_mark: (:heavy_check_mark:) || :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | #### compressed | | backup gzip (check) | restore gzip (check) | diff --git a/ddpar-check.sh b/ddpar-check.sh index dec5207..fb75173 100755 --- a/ddpar-check.sh +++ b/ddpar-check.sh @@ -9,6 +9,8 @@ #OUTPUT_FILE_TYPE="$(file -b $OUTPUT_FILE)" BASE_PATH="" BASE_FILE_NAME="" +NUM_JOBS=4 +BLOCKSIZEBYTES=1048576 function show_help { SCRIPT_NAME=$(basename "$0") @@ -19,6 +21,8 @@ function show_help { echo "-b PATH Der Basisname (opt. mit Pfad) des geteilten Abbildes" echo "-s PATH Source to compare against" echo "-d PATH Destination to compare against" + echo "-j NUM Anzahl der Jobs für den Clone-Check (Default: 4, nur ohne -b)" + echo "-B NUM Blockgröße in Bytes für den Clone-Check (Default: 1048576, nur ohne -b)" echo "-h, --help Zeigt diese Hilfemeldung an" } @@ -38,8 +42,29 @@ function check_backuped_image { done } +function check_cloned_image { + # Vergleicht Quelle und Ziel eines Clones segmentweise und parallel. + # Es existieren keine .sha256-Dateien, daher werden die Hashes beider + # Seiten direkt berechnet und verglichen. + for ((i=0; i Date: Sat, 23 May 2026 16:58:38 +0000 Subject: [PATCH 16/19] feat: add remote netcat backup (uncompressed, no checks) Implements the missing remote_backup_commands function and wires the backup mode to transfer split parts to a remote host over netcat, mirroring the existing remote clone path. Metadata and free-space handling are made remote-aware via an append_metadata helper. https://claude.ai/code/session_01Snu46hRFQBRdJTC9DDGZ6r --- README.md | 6 +-- ddpar.sh | 158 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 73f3916..7233629 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Attempting scripted parallel dd execution. | check_remote_port_availability | :heavy_check_mark: | | output_analysis | :heavy_check_mark: | | remote_cloning_commands | :gear: | -| remote_backup_commands | :stop_sign: | +| remote_backup_commands | :heavy_check_mark: | @@ -42,8 +42,8 @@ Attempting scripted parallel dd execution. #### uncompressed | | clone (check) | | backup (check) | restore (check) | |-|----------|-|----------|----------| -| block dev | :heavy_check_mark: (:stop_sign:) | | :stop_sign: (:stop_sign:) | :stop_sign: (:stop_sign:) | -| file | :heavy_check_mark: (:stop_sign:) | | :stop_sign: (:stop_sign:) | :stop_sign: (:stop_sign:) | +| block dev | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :stop_sign: (:stop_sign:) | +| file | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :stop_sign: (:stop_sign:) | #### local [de]compression | | backup gzip (check) | restore gzip (check) | diff --git a/ddpar.sh b/ddpar.sh index dd57755..925007c 100755 --- a/ddpar.sh +++ b/ddpar.sh @@ -663,6 +663,73 @@ function clone_block { done } +function append_metadata { + [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 + # Schreibt eine Zeile in das Metadatenfile, lokal oder remote + local line=$1 + if [ $REMOTE -eq 1 ]; then + execute_remote_command "echo \"${line}\" >> \"${METADATA_FILE}\"" + else + echo "${line}" >> "${METADATA_FILE}" + fi +} + +function remote_backup_commands { + [ "$DEBUG" -eq 1 ] && echo -e "${DEBUGCOLOR}[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen${NOCOLOR}" >&2 + # Richtet auf der Remote-Maschine einen netcat-Empfänger ein, der die + # übertragenen Daten in den übergebenen Befehl (z.B. "dd of=...") schreibt. + # Setzt anschließend INPUT_CMD_REMOTE_EXTENSION für die lokale Senderseite. + local remote_output_cmd=$1 + + # Generate and check remote ports + if [ -z "${REMOTE_PORT}" ]; then + remote_port_generation + fi + CURRENT_REMOTE_PORT=$(( REMOTE_PORT + PART_NUM )) + # Schleife zum Generieren eines freien Ports + while true; do + if check_remote_port_availability; then + break + else + echo -e "${INFOCOLOR}Port ${CURRENT_REMOTE_PORT} on remote machine already in use, generate new port.${NOCOLOR}" + remote_port_generation + CURRENT_REMOTE_PORT=$(( REMOTE_PORT + PART_NUM )) + fi + done + + echo -e "${INFOCOLOR}REMOTE COMMAND: nc -N -l ${CURRENT_REMOTE_PORT} | ${remote_output_cmd}${NOCOLOR}" + execute_remote_background_command "nc -N -l ${CURRENT_REMOTE_PORT} | ${remote_output_cmd}" + + # Check if execute_remote_background_command is running + MAX_ATTEMPTS=3 # Anzahl der maximalen Versuche + SLEEP_INTERVAL=1 # Wartezeit zwischen den Versuchen in Sekunden + ATTEMPT=1 # Zähler für die aktuellen Versuche + + # Schleife, die den Status des Ports überprüft + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo -e "${INFOCOLOR}Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)...${NOCOLOR}" + if execute_remote_command "ss -tuln | grep -q :${CURRENT_REMOTE_PORT}"; then + echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}" + break + else + echo -e "${INFOCOLOR}Process not found on port ${CURRENT_REMOTE_PORT}.${NOCOLOR}" + fi + ATTEMPT=$((ATTEMPT + 1)) + if [ $ATTEMPT -le $MAX_ATTEMPTS ]; then + sleep $SLEEP_INTERVAL + fi + done + + # Wenn nach allen Versuchen der Prozess nicht gefunden wurde, mit Fehler beenden + if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then + echo -e "${INFOCOLOR}Process did not start on port ${CURRENT_REMOTE_PORT} after $MAX_ATTEMPTS attempts." + INTERNAL_EXITCODE=2 + return 1 + fi + + INPUT_CMD_REMOTE_EXTENSION="nc ${REMOTE_HOST#*@} ${CURRENT_REMOTE_PORT}" +} + ################ # Script Start # @@ -719,16 +786,18 @@ case $MODE in # Hier kannst du den Code für den Fehlerfall des Ausgabe-Typs einfügen exit 1 fi - # Freier Speicher im Zielpfad analysieren - FREE_SPACE=$(df -P -B 1 "${OUTPUT}" | awk 'NR==2 {print $4}') - if [ -z "$FORCE" ] && (( INPUT_SIZE > FREE_SPACE )); then - echo -e "${ERRORCOLOR}Fehler: Eingabegröße (${INPUT_SIZE}) überschreitet den verfügbaren Speicherplatz (${FREE_SPACE}).${NOCOLOR}" - exit 1 + # Freier Speicher im Zielpfad analysieren (lokal; remote netcat backup ohne diese Pruefung) + if [ $REMOTE -ne 1 ]; then + FREE_SPACE=$(df -P -B 1 "${OUTPUT}" | awk 'NR==2 {print $4}') + if [ -z "$FORCE" ] && (( INPUT_SIZE > FREE_SPACE )); then + echo -e "${ERRORCOLOR}Fehler: Eingabegröße (${INPUT_SIZE}) überschreitet den verfügbaren Speicherplatz (${FREE_SPACE}).${NOCOLOR}" + exit 1 + fi + if [ ! -z "$FORCE" ] && (( INPUT_SIZE > FREE_SPACE )); then + echo -e "${WARNCOLOR}Warnung: Eingabegröße (${INPUT_SIZE}) überschreitet den verfügbaren Speicherplatz (${FREE_SPACE}). Mit aktiver Komprimierung koennte es dennoch passen.${NOCOLOR}" + fi fi - if [ ! -z "$FORCE" ] && (( INPUT_SIZE > FREE_SPACE )); then - echo -e "${WARNCOLOR}Warnung: Eingabegröße (${INPUT_SIZE}) überschreitet den verfügbaren Speicherplatz (${FREE_SPACE}). Mit aktiver Komprimierung koennte es dennoch passen.${NOCOLOR}" - fi - + echo -e "${SUCCESSCOLOR}Führe die Backup-Aktion durch.${NOCOLOR}" # generate further spinoff variables @@ -737,22 +806,29 @@ case $MODE in OUTPUT_FILE="${OUTPUT}/${OUTPUT_FILE_NAME}-" METADATA_FILE="${OUTPUT_FILE}metadata.txt" - # Write metadata file - if [ -f ${METADATA_FILE} ]; then - echo "Metadatafile already exists, copying it to ${METADATA_FILE}.old" - cp -p ${METADATA_FILE} ${METADATA_FILE}.old - cat /dev/null > ${METADATA_FILE} + # Write metadata file (lokal oder remote) + if [ $REMOTE -eq 1 ]; then + if execute_remote_command "[ -f \"${METADATA_FILE}\" ]"; then + echo "Metadatafile already exists, copying it to ${METADATA_FILE}.old" + execute_remote_command "cp -p \"${METADATA_FILE}\" \"${METADATA_FILE}.old\" && cat /dev/null > \"${METADATA_FILE}\"" + fi + else + if [ -f ${METADATA_FILE} ]; then + echo "Metadatafile already exists, copying it to ${METADATA_FILE}.old" + cp -p ${METADATA_FILE} ${METADATA_FILE}.old + cat /dev/null > ${METADATA_FILE} + fi fi - echo "NUM_JOBS=${NUM_JOBS}" >> ${METADATA_FILE} - echo "FILE_NAME=${INPUT_FILE_NAME}" >> ${METADATA_FILE} - echo "BLOCKSIZEBYTES=${BLOCKSIZEBYTES}" >> ${METADATA_FILE} - echo "INPUT_SIZE=${INPUT_SIZE}" >> ${METADATA_FILE} - echo "INPUT_FILE_NAME=${INPUT_FILE_NAME}" >> ${METADATA_FILE} - echo "FILE_TYPE=${INPUT_FILE_TYPE}" >> ${METADATA_FILE} - + append_metadata "NUM_JOBS=${NUM_JOBS}" + append_metadata "FILE_NAME=${INPUT_FILE_NAME}" + append_metadata "BLOCKSIZEBYTES=${BLOCKSIZEBYTES}" + append_metadata "INPUT_SIZE=${INPUT_SIZE}" + append_metadata "INPUT_FILE_NAME=${INPUT_FILE_NAME}" + append_metadata "FILE_TYPE=${INPUT_FILE_TYPE}" + # Write to metadata file - echo "SPLIT_SIZE=${SPLIT_SIZE}" >> ${METADATA_FILE} + append_metadata "SPLIT_SIZE=${SPLIT_SIZE}" echo -e "${INFOCOLOR}Starte die Prozesse ...${NOCOLOR}" for ((PART_NUM=0; PART_NUM<${NUM_JOBS}; PART_NUM++)); do @@ -761,22 +837,32 @@ case $MODE in START=$((PART_NUM * SPLIT_SIZE)) INPUT_CMD="dd if=${INPUT} bs=${BLOCKSIZEBYTES} count=$((SPLIT_SIZE / ${BLOCKSIZEBYTES})) skip=$((START / ${BLOCKSIZEBYTES}))" FULL_CMD="${INPUT_CMD}" - if [ $CHECKSUM -eq 1 ]; then - CHECKSUM_CMD="tee >(sha256sum > ${OUTPUT_FILE}${PART_NUM}.sha256)" - FULL_CMD="${FULL_CMD} | $CHECKSUM_CMD" - fi - if [ $COMPRESSION -eq 1 ]; then - if [ $PART_NUM -eq 0 ]; then - #echo "Compression is enabled with \$COMPRESSION_LEVEL ${COMPRESSION_LEVEL}" - # Append compression and its level to metadata file - echo "COMPRESSION=${COMPRESSION}" >> ${METADATA_FILE} - echo "COMPRESSION_LEVEL=${COMPRESSION_LEVEL}" >> ${METADATA_FILE} + if [ $REMOTE -eq 1 ]; then + # Remote netcat backup, unkomprimiert, ohne Checksumme + OUTPUT_CMD="dd of=${OUTPUT_FILE}${PART_NUM}.part bs=${BLOCKSIZEBYTES}" + if ! remote_backup_commands "${OUTPUT_CMD}"; then + echo -e "${ERRORCOLOR}Remote-Backup-Empfänger für Teil ${PART_NUM} konnte nicht gestartet werden.${NOCOLOR}" + break fi - COMPRESSION_CMD="gzip -${COMPRESSION_LEVEL} > ${OUTPUT_FILE}${PART_NUM}.gz" - FULL_CMD="${FULL_CMD} | $COMPRESSION_CMD &" + FULL_CMD="${FULL_CMD} | ${INPUT_CMD_REMOTE_EXTENSION} &" else - OUTPUT_CMD="dd of=${OUTPUT_FILE}${PART_NUM}.part bs=${BLOCKSIZEBYTES}" - FULL_CMD="${FULL_CMD} | $OUTPUT_CMD &" + if [ $CHECKSUM -eq 1 ]; then + CHECKSUM_CMD="tee >(sha256sum > ${OUTPUT_FILE}${PART_NUM}.sha256)" + FULL_CMD="${FULL_CMD} | $CHECKSUM_CMD" + fi + if [ $COMPRESSION -eq 1 ]; then + if [ $PART_NUM -eq 0 ]; then + #echo "Compression is enabled with \$COMPRESSION_LEVEL ${COMPRESSION_LEVEL}" + # Append compression and its level to metadata file + echo "COMPRESSION=${COMPRESSION}" >> ${METADATA_FILE} + echo "COMPRESSION_LEVEL=${COMPRESSION_LEVEL}" >> ${METADATA_FILE} + fi + COMPRESSION_CMD="gzip -${COMPRESSION_LEVEL} > ${OUTPUT_FILE}${PART_NUM}.gz" + FULL_CMD="${FULL_CMD} | $COMPRESSION_CMD &" + else + OUTPUT_CMD="dd of=${OUTPUT_FILE}${PART_NUM}.part bs=${BLOCKSIZEBYTES}" + FULL_CMD="${FULL_CMD} | $OUTPUT_CMD &" + fi fi echo "${INFOCOLOR}${FULL_CMD}${NOCOLOR}" eval "${FULL_CMD}" From 1975ce69342b5d3da01f41fb9978249845870dda Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 19:21:03 +0000 Subject: [PATCH 17/19] docs: add remote netcat backup tests to TESTING.md Replaces the previously unimplemented section 4.6 with test commands for remote uncompressed backup (block device and file), including a fake-ssh stub recipe for validating the remote code path without an SSH server. https://claude.ai/code/session_01Snu46hRFQBRdJTC9DDGZ6r --- TESTING.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/TESTING.md b/TESTING.md index 92fa73e..e88e40e 100644 --- a/TESTING.md +++ b/TESTING.md @@ -34,6 +34,9 @@ REMOTE_DEST_DEV=/dev/sdb # Remote – Zielverzeichnis für File-Clone auf dem Remote-Host REMOTE_DEST_DIR=/tmp/ddpar_clone_dest + +# Remote – Backup-Verzeichnis auf dem Remote-Host +REMOTE_BACKUP_DIR=/tmp/ddpar_backup ``` ### Testdaten erstellen @@ -219,11 +222,65 @@ Kompression findet auf der Remote-Seite statt. sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_DEST_DEV -m clone -r c -R $REMOTE_HOST ``` -### 4.6 Remote Backup – nicht implementiert 🛑 +### 4.6 Remote Backup – Block Device, unkomprimiert (Modus n) + +Die Split-Teile werden per Netcat zum `$REMOTE_HOST` übertragen und dort als +`*.part`-Dateien abgelegt. Die Übertragung ist unkomprimiert und ohne Prüfsumme +(`-c`/`-s` werden auf dem Remote-Pfad nicht angewendet). + +```bash +sudo ./ddpar.sh -i $SOURCE_DEV -o $REMOTE_BACKUP_DIR -m backup -r n -R $REMOTE_HOST +``` + +> **Voraussetzung:** `$REMOTE_BACKUP_DIR` muss auf dem Remote-Host als Verzeichnis +> existieren. +> Erzeugte Dateien auf dem Remote-Host: `$REMOTE_BACKUP_DIR/sdb-0.part` … +> `sdb-3.part` + `sdb-metadata.txt`. + +### 4.7 Remote Backup – Datei, unkomprimiert (Modus n) + +```bash +./ddpar.sh -i $SOURCE_FILE -o $REMOTE_BACKUP_DIR -m backup -r n -R $REMOTE_HOST +``` + +> Erzeugte Dateien auf dem Remote-Host: +> `$REMOTE_BACKUP_DIR/ddpar_test.img-0.part` … + `ddpar_test.img-metadata.txt`. + +#### Test ohne echten Remote-Host (Fake-`ssh`-Stub) + +Ist kein SSH-Server verfügbar, lässt sich der Remote-Backup-Codepfad mit einem +`ssh`-Stub im `PATH` validieren (prüft Befehlsaufbau und sequentielle Ports, +überträgt aber keine echten Daten): + +```bash +mkdir -p /tmp/fakebin +cat > /tmp/fakebin/ssh <<'STUB' +#!/bin/bash +args="$*" +case "$args" in + *"-O check"*) exit 1;; # kein bestehender Socket + *"-O exit"*) exit 0;; +esac +cmd="${@: -1}" +case "$cmd" in + *"file -b"*) echo "directory"; exit 0;; + *"ss -tuln"*) exit 0;; # Verifikationsschleife: Prozess läuft + *"ss -tln"*) exit 1;; # Portprüfung: Port frei + *"nohup"*) exit 0;; # Empfänger-Start im Hintergrund + *"command -v"*) exit 0;; + *) exit 0;; +esac +STUB +chmod +x /tmp/fakebin/ssh + +PATH="/tmp/fakebin:$PATH" ./ddpar.sh -i $SOURCE_FILE -o /tmp/ddpar_backup \ + -m backup -r n -R localhost -j 4 -b 1048576 +``` -Remote Backup (`-m backup` mit `-r`) ist laut Status-Tabelle noch nicht implementiert. +Erwartung: pro Teil eine Zeile `REMOTE COMMAND: nc -N -l | dd of=…-N.part` +mit aufsteigenden Ports sowie lokal `dd if=… | nc localhost &`. -### 4.7 Remote Restore – nicht implementiert 🛑 +### 4.8 Remote Restore – nicht implementiert 🛑 Remote Restore über `ddpar-restore.sh` ist laut Status-Tabelle noch nicht implementiert. From ad2f48ad6c0f72eafaf695bc47be4b8a653db218 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 21:11:22 +0000 Subject: [PATCH 18/19] feat: add remote netcat restore (uncompressed) to ddpar-restore.sh Adds SSH/netcat remote support to the restore script: the backup parts live on the remote host and are streamed back to a local target device via netcat (remote sends, local receives). Metadata is read from the remote host. Uncompressed only; compressed remote backups are rejected. Also adds conv=notrunc to the reassembly dd so parallel writers no longer truncate each other when restoring to a regular file. https://claude.ai/code/session_01Snu46hRFQBRdJTC9DDGZ6r --- CLAUDE.md | 2 +- README.md | 5 +- TESTING.md | 59 ++++++++++- ddpar-restore.sh | 260 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 297 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9e89273..c1b468e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ Manuelle Tests: siehe `TESTING.md`. ## Bekannte Einschränkungen / offene Baustellen -- Remote Backup und Remote Restore sind noch nicht implementiert (🛑 in README) +- Remote Backup und Remote Restore (netcat, unkomprimiert, ohne Check) sind implementiert; komprimierter Remote-Transfer fehlt noch (🛑 in README) - Clone-Check (`ddpar-check.sh` nach Clone) ist noch nicht implementiert - `RANDOM` in Bash liefert nur 0–32767 → Remote-Ports werden aus dem Bereich 10000–42767 gewählt - `fallocate` funktioniert nicht auf Block-Devices (wird korrekt übersprungen) diff --git a/README.md b/README.md index 4cd56c8..0ffdb45 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Attempting scripted parallel dd execution. | output_analysis | :heavy_check_mark: | | remote_cloning_commands | :gear: | | remote_backup_commands | :heavy_check_mark: | +| remote_restore_commands | :heavy_check_mark: | @@ -42,8 +43,8 @@ Attempting scripted parallel dd execution. #### uncompressed | | clone (check) | | backup (check) | restore (check) | |-|----------|-|----------|----------| -| block dev | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :stop_sign: (:stop_sign:) | -| file | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :stop_sign: (:stop_sign:) | +| block dev | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :heavy_check_mark: (:stop_sign:) | +| file | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :heavy_check_mark: (:stop_sign:) | #### local [de]compression | | backup gzip (check) | restore gzip (check) | diff --git a/TESTING.md b/TESTING.md index e88e40e..8a341f1 100644 --- a/TESTING.md +++ b/TESTING.md @@ -280,7 +280,64 @@ PATH="/tmp/fakebin:$PATH" ./ddpar.sh -i $SOURCE_FILE -o /tmp/ddpar_backup \ Erwartung: pro Teil eine Zeile `REMOTE COMMAND: nc -N -l | dd of=…-N.part` mit aufsteigenden Ports sowie lokal `dd if=… | nc localhost &`. -### 4.8 Remote Restore – nicht implementiert 🛑 +### 4.8 Remote Restore – Block Device, unkomprimiert (Modus n) + +Die Backup-Teile liegen auf dem `$REMOTE_HOST`; das Zielgerät `-o` ist **lokal**. +Der Remote-Host sendet die Teile per Netcat, lokal werden sie empfangen und +geschrieben. Nur unkomprimiert (komprimierte Backups werden remote abgelehnt). +Der `-i`-Basispfad ist der Pfad **auf dem Remote-Host** ohne abschließendes `-`. + +```bash +sudo ./ddpar-restore.sh -i $REMOTE_BACKUP_DIR/sdb -o $DEST_DEV -r n -R $REMOTE_HOST +``` + +> **Voraussetzung:** Ein vorher erstelltes Remote-Backup aus Test 4.6. +> Die Metadaten werden automatisch vom `$REMOTE_HOST` gelesen. + +### 4.9 Remote Restore – Datei, unkomprimiert (Modus n) + +```bash +./ddpar-restore.sh -i $REMOTE_BACKUP_DIR/ddpar_test.img -o $DEST_DIR/ddpar_test.img -r n -R $REMOTE_HOST +``` + +> **Voraussetzung:** Ein vorher erstelltes Remote-Backup aus Test 4.7. + +#### Test ohne echten Remote-Host (Fake-`ssh`-Stub) + +Wie bei Test 4.6/4.7 lässt sich auch der Remote-Restore-Codepfad ohne SSH-Server +mit einem `ssh`-Stub im `PATH` validieren. Der Stub muss zusätzlich beim +`cat …metadata.txt` gültige Metadaten liefern: + +```bash +mkdir -p /tmp/fakebin +cat > /tmp/fakebin/ssh <<'STUB' +#!/bin/bash +args="$*" +case "$args" in + *"-O check"*) exit 1;; + *"-O exit"*) exit 0;; +esac +cmd="${@: -1}" +case "$cmd" in + *metadata.txt*) + printf 'NUM_JOBS=4\nFILE_NAME=in.img\nBLOCKSIZEBYTES=1048576\nINPUT_SIZE=4194304\nINPUT_FILE_NAME=in.img\nFILE_TYPE=data\nSPLIT_SIZE=1048576\n' + exit 0;; + *"ss -tuln"*) exit 0;; # Sender-Listener läuft + *"ss -tln"*) exit 1;; # Port frei + *"nohup"*) exit 0;; # Remote-Sender im Hintergrund + *true) exit 0;; + *) exit 0;; +esac +STUB +chmod +x /tmp/fakebin/ssh + +echo "y" | PATH="/tmp/fakebin:$PATH" ./ddpar-restore.sh \ + -i /tmp/ddpar_backup/in.img -o /tmp/restore_out.img -r n -R localhost +``` + +Erwartung: pro Teil `REMOTE COMMAND: dd if=…-N.part … | nc -N -l ` (Remote +sendet) und lokal `nc localhost | dd of=… seek=N count=1` mit aufsteigenden +Ports und Offsets. Remote Restore über `ddpar-restore.sh` ist laut Status-Tabelle noch nicht implementiert. diff --git a/ddpar-restore.sh b/ddpar-restore.sh index 05c3020..2f6f916 100755 --- a/ddpar-restore.sh +++ b/ddpar-restore.sh @@ -4,6 +4,10 @@ INPUT_FILE_BASENAME="" OUTPUT_FILE="" +REMOTE=0 +REMOTE_HOST="" +SSH_SOCKET_PATH="/tmp/ssh_socket_ddpar" +DEBUG=0 # Hilfemeldung anzeigen function show_help { @@ -12,24 +16,189 @@ function show_help { echo "Verwendung: $SCRIPT_NAME [Optionen]" echo "" echo "Optionen:" - echo "-i, --input PATH Der Basisname des geteilten Abbildes" - echo "-o, --output PATH Vollständiger Pfad des Zielgeräts" + echo "-i, --input PATH Der Basisname des geteilten Abbildes (bei Remote: Pfad auf dem Remote-Host)" + echo "-o, --output PATH Vollständiger Pfad des (lokalen) Zielgeräts" + echo "-r [n] Remote-Restore über SSH+Netcat (nur unkomprimiert)" + echo "-R user@host Angabe des Remote-Host, auf dem das Backup liegt" echo "-h, --help Diese Hilfe anzeigen" echo "" echo "Die Anzahl der Jobs und Blockgröße kann nicht geändert werden. Sie wird beim Erstellen des Abbildes festgelegt." } # Verwendung von getopts zur Verarbeitung der Optionen -while getopts ":i:o:h" opt; do +while getopts ":i:o:r::R:h" opt; do case $opt in i|-input) INPUT="$OPTARG";; o|-output) OUTPUT="$OPTARG";; + r) + REMOTE=1 + if [[ ${OPTARG} =~ ^[lnc]+$ ]]; then + REMOTE_MODE="${OPTARG}" + else + REMOTE_MODE="n" + fi + ;; + R) + REMOTE=1 + if [ -n "${OPTARG}" ]; then + REMOTE_HOST="${OPTARG}" + fi + ;; h|-help) show_help; exit 1;; \?) echo "Ungültige Option: -$OPTARG";; esac done +function establish_ssh_connection { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + local target=$1 + local control_path=$2 + local password=$3 + if [ -n "$password" ]; then + if ! which sshpass > /dev/null; then + echo "Der Befehl \"sshpass\" existiert nicht. Bitte installieren Sie das entsprechende Paket über ihren Paketmanager." + exit 1 + fi + sshpass -p "$password" ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=yes -S "${control_path}" "${target}" true + else + echo "Verbindungsaufbau mit Sockel ${control_path} zu ${target}" + ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=yes -S "${control_path}" "${target}" true + fi + return $? +} + +function is_ssh_socket_alive { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + ssh -o ControlPath="${SSH_SOCKET_PATH}" -O check "${REMOTE_HOST}" 2>/dev/null + return $? +} + +function connect_ssh { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + if [ -z "${REMOTE_HOST}" ]; then + echo "Fehler: Kein Remote-Host angegeben." + exit 1 + fi + if is_ssh_socket_alive; then + echo "SSH-Verbindung zu ${REMOTE_HOST} besteht bereits." + return 0 + fi + output=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 ${REMOTE_HOST} true 2>&1) + if [[ $? -eq 0 ]]; then + echo "Passwortloser Verbindungsaufbau war erfolgreich." + establish_ssh_connection "${REMOTE_HOST}" "${SSH_SOCKET_PATH}" + elif echo "$output" | grep -q "Permission denied"; then + echo "Host ist erreichbar, aber passwortlose Authentifizierung fehlgeschlagen." + echo -n "Bitte geben Sie das SSH-Passwort für ${REMOTE_HOST} ein: " + read -s USER_PASSWORD + echo + establish_ssh_connection "${REMOTE_HOST}" "${SSH_SOCKET_PATH}" "$USER_PASSWORD" + if [ $? -ne 0 ]; then + echo "Verbindung zu ${REMOTE_HOST} konnte nicht hergestellt werden." + exit 1 + fi + else + echo "Unbekannter Fehler oder Host nicht erreichbar. Ausgabe:" + echo "$output" + fi + echo "SSH-Verbindung zu ${REMOTE_HOST} wurde erfolgreich aufgebaut." +} + +function execute_remote_command { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + local command=$1 + if [ -z "${command}" ]; then + echo "Fehler: Kein Befehl zum Ausführen angegeben." + return 1 + fi + ssh -S "${SSH_SOCKET_PATH}" "${REMOTE_HOST}" "${command}" + return $? +} + +function execute_remote_background_command { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + local command=$1 + if [ -z "${command}" ]; then + echo "Fehler: Kein Befehl zum Ausführen angegeben." + return 1 + fi + ssh -S "${SSH_SOCKET_PATH}" "${REMOTE_HOST}" "nohup sh -c \"${command}\" > /tmp/ddpar.log 2>&1 &" +} + +function close_ssh_connection { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + ssh -S "${SSH_SOCKET_PATH}" -O exit "${REMOTE_HOST}" + if [ $? -ne 0 ]; then + echo "Warnung: Fehler beim Schließen der SSH-Verbindung zu ${REMOTE_HOST}." + fi +} + +function remote_port_generation { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + REMOTE_PORT=$(( RANDOM % 55001 )) + REMOTE_PORT=$(( REMOTE_PORT + 10000 )) +} + +function check_remote_port_availability { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + execute_remote_command "ss -tln | grep -q \":${CURRENT_REMOTE_PORT}\"" + if [[ $? != 0 ]]; then + return 0 + else + [ "$DEBUG" -eq 1 ] && echo "Port ${CURRENT_REMOTE_PORT} bereits in Benutzung." + return 1 + fi +} + +function remote_restore_commands { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + # Startet auf dem Remote-Host einen netcat-Sender, der die übergebene Quelle + # (z.B. "dd if=...part") an den verbindenden lokalen Client liefert. + # Setzt anschließend OUTPUT_CMD_REMOTE_SOURCE für die lokale Empfängerseite. + local remote_input_cmd=$1 + + if [ -z "${REMOTE_PORT}" ]; then + remote_port_generation + fi + CURRENT_REMOTE_PORT=$(( REMOTE_PORT + PART_NUM )) + while true; do + if check_remote_port_availability; then + break + else + echo "Port ${CURRENT_REMOTE_PORT} on remote machine already in use, generate new port." + remote_port_generation + CURRENT_REMOTE_PORT=$(( REMOTE_PORT + PART_NUM )) + fi + done + + echo "REMOTE COMMAND: ${remote_input_cmd} | nc -N -l ${CURRENT_REMOTE_PORT}" + execute_remote_background_command "${remote_input_cmd} | nc -N -l ${CURRENT_REMOTE_PORT}" + + MAX_ATTEMPTS=3 + SLEEP_INTERVAL=1 + ATTEMPT=1 + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "Checking if remote process is running on port ${CURRENT_REMOTE_PORT} (attempt $ATTEMPT)..." + if execute_remote_command "ss -tuln | grep -q :${CURRENT_REMOTE_PORT}"; then + echo "Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop." + break + else + echo "Process not found on port ${CURRENT_REMOTE_PORT}." + fi + ATTEMPT=$((ATTEMPT + 1)) + if [ $ATTEMPT -le $MAX_ATTEMPTS ]; then + sleep $SLEEP_INTERVAL + fi + done + if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then + echo "Process did not start on port ${CURRENT_REMOTE_PORT} after $MAX_ATTEMPTS attempts." + return 1 + fi + + OUTPUT_CMD_REMOTE_SOURCE="nc ${REMOTE_HOST#*@} ${CURRENT_REMOTE_PORT}" +} + function restore_split_image { # for ((i=0; i<$NUM_JOBS; i++)); do # START=$((i * SPLIT_SIZE)) @@ -44,22 +213,34 @@ function restore_split_image { fallocate -l ${INPUT_SIZE} $OUTPUT_FILE fi for ((PART_NUM=0; PART_NUM<${NUM_JOBS}; PART_NUM++)); do - # Build individual subcommands and concatinate, if enabled - if [ ! -z "$COMPRESSION" ]; then + START=$((PART_NUM * SPLIT_SIZE)) + OUTPUT_CMD="dd of=${OUTPUT_FILE} bs=${BLOCKSIZEBYTES} count=$((SPLIT_SIZE / ${BLOCKSIZEBYTES})) seek=$((START / ${BLOCKSIZEBYTES})) iflag=fullblock conv=notrunc" + if [ $REMOTE -eq 1 ]; then + # Remote netcat restore, unkomprimiert: Remote sendet, lokal wird empfangen und geschrieben if [ $PART_NUM -eq 0 ]; then - echo "Source is compressed" + echo "Source is remote (uncompressed)" + fi + REMOTE_INPUT_CMD="dd if=${INPUT_FILES}${PART_NUM}.part bs=${BLOCKSIZEBYTES} iflag=fullblock" + if ! remote_restore_commands "${REMOTE_INPUT_CMD}"; then + echo "Remote-Restore-Sender für Teil ${PART_NUM} konnte nicht gestartet werden." + break fi - INPUT_CMD="zcat ${INPUT_FILES}${PART_NUM}.gz" + FULL_CMD="${OUTPUT_CMD_REMOTE_SOURCE} | ${OUTPUT_CMD} &" else - if [ $PART_NUM -eq 0 ]; then - echo "Source is uncompressed" + # Build individual subcommands and concatinate, if enabled + if [ ! -z "$COMPRESSION" ]; then + if [ $PART_NUM -eq 0 ]; then + echo "Source is compressed" + fi + INPUT_CMD="zcat ${INPUT_FILES}${PART_NUM}.gz" + else + if [ $PART_NUM -eq 0 ]; then + echo "Source is uncompressed" + fi + INPUT_CMD="dd if=${INPUT_FILES}${PART_NUM}.part bs=${BLOCKSIZEBYTES} iflag=fullblock" fi - INPUT_CMD="dd if=${INPUT_FILES}${PART_NUM}.part bs=${BLOCKSIZEBYTES} iflag=fullblock" + FULL_CMD="${INPUT_CMD} | ${OUTPUT_CMD} &" fi - START=$((PART_NUM * SPLIT_SIZE)) - FULL_CMD="${INPUT_CMD}" - OUTPUT_CMD="dd of=${OUTPUT_FILE} bs=${BLOCKSIZEBYTES} count=$((SPLIT_SIZE / ${BLOCKSIZEBYTES})) seek=$((START / ${BLOCKSIZEBYTES})) iflag=fullblock" - FULL_CMD="${FULL_CMD} | $OUTPUT_CMD &" echo "$FULL_CMD" eval "${FULL_CMD}" done @@ -76,19 +257,40 @@ INPUT_FILES="${INPUT_PATH}/${INPUT_FILE_BASENAME}-" OUTPUT_FILE_TYPE="$(file -b $OUTPUT)" METADATA_FILE="${INPUT_FILES}metadata.txt" -# Get parameters from metadata file -if [ ! -e "$METADATA_FILE" ]; then - echo "Die Datei existiert $METADATA_FILE nicht." +# Get parameters from metadata file (lokal oder remote) +if [ $REMOTE -eq 1 ]; then + connect_ssh + METADATA_SRC=$(mktemp) + execute_remote_command "cat \"$METADATA_FILE\"" > "$METADATA_SRC" 2>/dev/null + if [ ! -s "$METADATA_SRC" ]; then + echo "Die Metadatendatei $METADATA_FILE auf $REMOTE_HOST existiert nicht oder ist leer." + rm -f "$METADATA_SRC" + close_ssh_connection + exit 1 + fi +else + METADATA_SRC="$METADATA_FILE" + if [ ! -e "$METADATA_SRC" ]; then + echo "Die Datei existiert $METADATA_FILE nicht." + exit 1 + fi +fi +NUM_JOBS=$(grep "^NUM_JOBS=" "$METADATA_SRC" | cut -d "=" -f 2) +FILE_NAME=$(grep "^FILE_NAME=" "$METADATA_SRC" | cut -d "=" -f 2) +SPLIT_SIZE=$(grep "^SPLIT_SIZE=" "$METADATA_SRC" | cut -d "=" -f 2) +INPUT_SIZE=$(grep "^INPUT_SIZE=" "$METADATA_SRC" | cut -d "=" -f 2) +INPUT_FILE_TYPE=$(grep "^FILE_TYPE=" "$METADATA_SRC" | cut -d "=" -f 2) +BLOCKSIZEBYTES=$(grep "^BLOCKSIZEBYTES=" "$METADATA_SRC" | cut -d "=" -f 2) +COMPRESSION=$(grep "^COMPRESSION=" "$METADATA_SRC" | cut -d "=" -f 2) +COMPRESSION_LEVEL=$(grep "^COMPRESSION_LEVEL=" "$METADATA_SRC" | cut -d "=" -f 2) + +# Remote-Restore unterstützt derzeit nur unkomprimierte Backups (netcat, uncompressed) +if [ $REMOTE -eq 1 ] && [ ! -z "$COMPRESSION" ]; then + echo "Remote-Restore unterstützt derzeit nur unkomprimierte Backups (netcat, uncompressed)." + rm -f "$METADATA_SRC" + close_ssh_connection exit 1 fi -NUM_JOBS=$(grep "NUM_JOBS" $METADATA_FILE | cut -d "=" -f 2) -FILE_NAME=$(grep "^FILE_NAME=" $METADATA_FILE | cut -d "=" -f 2) -SPLIT_SIZE=$(grep "SPLIT_SIZE" $METADATA_FILE | cut -d "=" -f 2) -INPUT_SIZE=$(grep "INPUT_SIZE" $METADATA_FILE | cut -d "=" -f 2) -INPUT_FILE_TYPE=$(grep "FILE_TYPE" $METADATA_FILE | cut -d "=" -f 2) -BLOCKSIZEBYTES=$(grep "BLOCKSIZEBYTES" $METADATA_FILE | cut -d "=" -f 2) -COMPRESSION=$(grep "^COMPRESSION=" $METADATA_FILE | cut -d "=" -f 2) -COMPRESSION_LEVEL=$(grep "COMPRESSION_LEVEL" $METADATA_FILE | cut -d "=" -f 2) # Überprüfung der erforderlichen Parameter if [ -z "$INPUT_PATH" ] || [ -z "$INPUT_FILE_BASENAME" ] || [ -z "$OUTPUT" ]; then @@ -159,6 +361,10 @@ while true; do n|N|"") echo "Abbruch." # Fügen Sie hier den Code hinzu, der bei "Nein" ausgeführt werden soll + if [ $REMOTE -eq 1 ]; then + rm -f "$METADATA_SRC" + close_ssh_connection + fi exit 0 ;; *) @@ -181,3 +387,7 @@ done #fi wait +if [ $REMOTE -eq 1 ]; then + rm -f "$METADATA_SRC" + close_ssh_connection +fi From 41b44a84ec565423870a36a5eb0ae3c841c813b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 06:52:50 +0000 Subject: [PATCH 19/19] feat: add remote netcat checks (uncompressed) to ddpar-check.sh Adds SSH support so source/backup/destination checks work when the backup parts or cloned target live on a remote host. Per-segment SHA256 hashes are computed locally and via SSH on the remote side and compared; no netcat data transfer is needed. Covers all three comparisons: - backup check (source local <-> backup remote .part) - restore check (backup remote .part <-> destination local) - clone check (source local <-> destination remote) Metadata is read from the remote host; compressed remote backups are rejected. Local checks (incl. the .sha256-based paths) are unchanged. https://claude.ai/code/session_01Snu46hRFQBRdJTC9DDGZ6r --- CLAUDE.md | 4 +- README.md | 4 +- TESTING.md | 58 +++++++++++- ddpar-check.sh | 242 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 279 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c1b468e..60b66b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,8 +55,8 @@ Manuelle Tests: siehe `TESTING.md`. ## Bekannte Einschränkungen / offene Baustellen -- Remote Backup und Remote Restore (netcat, unkomprimiert, ohne Check) sind implementiert; komprimierter Remote-Transfer fehlt noch (🛑 in README) -- Clone-Check (`ddpar-check.sh` nach Clone) ist noch nicht implementiert +- Remote Backup, Remote Restore und Remote Checks (netcat, unkomprimiert) sind implementiert; komprimierter Remote-Transfer fehlt noch (🛑 in README) +- Remote Checks (`ddpar-check.sh -r`) vergleichen nur SHA256-Hashes je Segment (lokal vs. per SSH) — kein netcat-Datentransfer nötig - `RANDOM` in Bash liefert nur 0–32767 → Remote-Ports werden aus dem Bereich 10000–42767 gewählt - `fallocate` funktioniert nicht auf Block-Devices (wird korrekt übersprungen) - Eingabegröße muss durch `NUM_JOBS × BLOCKSIZEBYTES` ganzzahlig teilbar sein diff --git a/README.md b/README.md index 0ffdb45..f1d52e5 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ Attempting scripted parallel dd execution. #### uncompressed | | clone (check) | | backup (check) | restore (check) | |-|----------|-|----------|----------| -| block dev | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :heavy_check_mark: (:stop_sign:) | -| file | :heavy_check_mark: (:stop_sign:) | | :heavy_check_mark: (:stop_sign:) | :heavy_check_mark: (:stop_sign:) | +| block dev | :heavy_check_mark: (:heavy_check_mark:) | | :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | +| file | :heavy_check_mark: (:heavy_check_mark:) | | :heavy_check_mark: (:heavy_check_mark:) | :heavy_check_mark: (:heavy_check_mark:) | #### local [de]compression | | backup gzip (check) | restore gzip (check) | diff --git a/TESTING.md b/TESTING.md index 8a341f1..8675ce9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -339,7 +339,63 @@ Erwartung: pro Teil `REMOTE COMMAND: dd if=…-N.part … | nc -N -l ` (Re sendet) und lokal `nc localhost | dd of=… seek=N count=1` mit aufsteigenden Ports und Offsets. -Remote Restore über `ddpar-restore.sh` ist laut Status-Tabelle noch nicht implementiert. +### 4.10 Remote Check – Quelle gegen Remote-Backup (Source ↔ Backup) + +> **Hinweis:** Der Remote-Check überträgt **keine** Nutzdaten über netcat – die +> SHA256-Hashes werden je Segment lokal bzw. per SSH auf dem Remote-Host berechnet +> und nur verglichen. Nur unkomprimierte Remote-Backups werden unterstützt; die +> `-b`-Seite liegt auf dem Remote-Host. + +```bash +./ddpar-check.sh -s $SOURCE_FILE -b $REMOTE_BACKUP_DIR/ddpar_test.img -r n -R $REMOTE_HOST +``` + +> **Voraussetzung:** Remote-Backup aus Test 4.6/4.7. +> Erwartung: `Segment N: OK ()` pro Segment, sonst `MISMATCH`. + +### 4.11 Remote Check – Remote-Backup gegen lokales Ziel (Backup ↔ Destination) + +```bash +sudo ./ddpar-check.sh -b $REMOTE_BACKUP_DIR/sdb -d $DEST_DEV -r n -R $REMOTE_HOST +``` + +> **Voraussetzung:** Lokaler Remote-Restore aus Test 4.8 nach `$DEST_DEV`. + +### 4.12 Remote Check – Clone (Source ↔ Remote-Destination) + +Für einen Remote-Clone (Test 4.1/4.2): das geklonte Ziel liegt auf dem Remote-Host. + +```bash +sudo ./ddpar-check.sh -s $SOURCE_DEV -d $REMOTE_DEST_DEV -r n -R $REMOTE_HOST -j 4 -B 1048576 +``` + +> **Abweichung:** Ohne `-b` gibt es keine Metadaten; `-j`/`-B` müssen zum +> ursprünglichen Clone-Aufruf passen. + +#### Test ohne echten Remote-Host (Fake-`ssh`-Stub) + +Da der Check nur Hashes vergleicht, kann ein `ssh`-Stub das Remote-Kommando lokal +ausführen (Remote == localhost), wodurch echte Hashes über reale Dateien berechnet +werden: + +```bash +mkdir -p /tmp/fakebin +cat > /tmp/fakebin/ssh <<'STUB' +#!/bin/bash +args="$*" +case "$args" in + *"-O check"*) exit 1;; + *"-O exit"*) exit 0;; +esac +cmd="${@: -1}" # Remote-Kommando lokal ausführen +bash -c "$cmd" +STUB +chmod +x /tmp/fakebin/ssh + +# Backup-Check gegen ein lokal erzeugtes "Remote"-Backup +PATH="/tmp/fakebin:$PATH" ./ddpar-check.sh \ + -s /tmp/ddpartest/in.img -b /tmp/ddpar_backup/in.img -r n -R localhost +``` --- diff --git a/ddpar-check.sh b/ddpar-check.sh index fb75173..1f0b511 100755 --- a/ddpar-check.sh +++ b/ddpar-check.sh @@ -11,6 +11,10 @@ BASE_PATH="" BASE_FILE_NAME="" NUM_JOBS=4 BLOCKSIZEBYTES=1048576 +REMOTE=0 +REMOTE_HOST="" +SSH_SOCKET_PATH="/tmp/ssh_socket_ddpar" +DEBUG=0 function show_help { SCRIPT_NAME=$(basename "$0") @@ -23,22 +27,148 @@ function show_help { echo "-d PATH Destination to compare against" echo "-j NUM Anzahl der Jobs für den Clone-Check (Default: 4, nur ohne -b)" echo "-B NUM Blockgröße in Bytes für den Clone-Check (Default: 1048576, nur ohne -b)" + echo "-r [n] Remote-Check über SSH (nur unkomprimiert). Die gesicherte/geklonte" + echo " Seite (-b bzw. bei Clone-Check -d) liegt auf dem Remote-Host." + echo "-R user@host Angabe des Remote-Host" echo "-h, --help Zeigt diese Hilfemeldung an" } +function establish_ssh_connection { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + local target=$1 + local control_path=$2 + local password=$3 + if [ -n "$password" ]; then + if ! which sshpass > /dev/null; then + echo "Der Befehl \"sshpass\" existiert nicht. Bitte installieren Sie das entsprechende Paket über ihren Paketmanager." + exit 1 + fi + sshpass -p "$password" ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=yes -S "${control_path}" "${target}" true + else + echo "Verbindungsaufbau mit Sockel ${control_path} zu ${target}" + ssh -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=yes -S "${control_path}" "${target}" true + fi + return $? +} + +function is_ssh_socket_alive { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + ssh -o ControlPath="${SSH_SOCKET_PATH}" -O check "${REMOTE_HOST}" 2>/dev/null + return $? +} + +function connect_ssh { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + if [ -z "${REMOTE_HOST}" ]; then + echo "Fehler: Kein Remote-Host angegeben." + exit 1 + fi + if is_ssh_socket_alive; then + echo "SSH-Verbindung zu ${REMOTE_HOST} besteht bereits." + return 0 + fi + output=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 ${REMOTE_HOST} true 2>&1) + if [[ $? -eq 0 ]]; then + echo "Passwortloser Verbindungsaufbau war erfolgreich." + establish_ssh_connection "${REMOTE_HOST}" "${SSH_SOCKET_PATH}" + elif echo "$output" | grep -q "Permission denied"; then + echo "Host ist erreichbar, aber passwortlose Authentifizierung fehlgeschlagen." + echo -n "Bitte geben Sie das SSH-Passwort für ${REMOTE_HOST} ein: " + read -s USER_PASSWORD + echo + establish_ssh_connection "${REMOTE_HOST}" "${SSH_SOCKET_PATH}" "$USER_PASSWORD" + if [ $? -ne 0 ]; then + echo "Verbindung zu ${REMOTE_HOST} konnte nicht hergestellt werden." + exit 1 + fi + else + echo "Unbekannter Fehler oder Host nicht erreichbar. Ausgabe:" + echo "$output" + fi + echo "SSH-Verbindung zu ${REMOTE_HOST} wurde erfolgreich aufgebaut." +} + +function execute_remote_command { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + local command=$1 + if [ -z "${command}" ]; then + echo "Fehler: Kein Befehl zum Ausführen angegeben." + return 1 + fi + ssh -S "${SSH_SOCKET_PATH}" "${REMOTE_HOST}" "${command}" + return $? +} + +function close_ssh_connection { + [ "$DEBUG" -eq 1 ] && echo "[DEBUG] Funktion ${FUNCNAME[0]} aufgerufen" >&2 + ssh -S "${SSH_SOCKET_PATH}" -O exit "${REMOTE_HOST}" + if [ $? -ne 0 ]; then + echo "Warnung: Fehler beim Schließen der SSH-Verbindung zu ${REMOTE_HOST}." + fi +} + +function local_seg_hash { + # $1 = Datei/Device (lokal), $2 = Segment-Index. Liefert SHA256 des Segments. + local f=$1 idx=$2 + local count=$((SPLIT_SIZE / BLOCKSIZEBYTES)) + local skip=$((idx * count)) + dd if="$f" bs="$BLOCKSIZEBYTES" count="$count" skip="$skip" status=none | sha256sum | cut -d' ' -f1 +} + +function remote_seg_hash { + # $1 = Datei/Device (auf Remote-Host), $2 = Segment-Index. Liefert SHA256 des Segments. + local f=$1 idx=$2 + local count=$((SPLIT_SIZE / BLOCKSIZEBYTES)) + local skip=$((idx * count)) + execute_remote_command "dd if='$f' bs=$BLOCKSIZEBYTES count=$count skip=$skip status=none | sha256sum" | cut -d' ' -f1 +} + +function remote_part_hash { + # $1 = Segment-Index. Hasht die komplette .part-Datei auf dem Remote-Host + # (entspricht dem Segment, da unkomprimiert exakt SPLIT_SIZE Bytes). + local idx=$1 + execute_remote_command "sha256sum '${BASE_FILES}${idx}.part'" | cut -d' ' -f1 +} + function check_restored_image { for ((i=0; i<$NUM_JOBS; i++)); do - START=$((i * SPLIT_SIZE)) - echo "dd if=$OUTPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# &" - dd if=$OUTPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# & + if [ $REMOTE -eq 1 ]; then + # Backup-Teile liegen remote (.part), Ziel ist lokal + ( + h_bak=$(remote_part_hash "$i") + h_dst=$(local_seg_hash "$OUTPUT_FILE" "$i") + if [ "$h_bak" = "$h_dst" ]; then + echo "Segment $i: OK ($h_bak)" + else + echo "Segment $i: MISMATCH (backup=$h_bak, destination=$h_dst)" + fi + ) & + else + START=$((i * SPLIT_SIZE)) + echo "dd if=$OUTPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# &" + dd if=$OUTPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# & + fi done } function check_backuped_image { for ((i=0; i<$NUM_JOBS; i++)); do - START=$((i * SPLIT_SIZE)) - echo "dd if=$INPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# &" - dd if=$INPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# & + if [ $REMOTE -eq 1 ]; then + # Quelle ist lokal, Backup-Teile liegen remote (.part) + ( + h_src=$(local_seg_hash "$INPUT_FILE" "$i") + h_bak=$(remote_part_hash "$i") + if [ "$h_src" = "$h_bak" ]; then + echo "Segment $i: OK ($h_src)" + else + echo "Segment $i: MISMATCH (source=$h_src, backup=$h_bak)" + fi + ) & + else + START=$((i * SPLIT_SIZE)) + echo "dd if=$INPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# &" + dd if=$INPUT_FILE bs=$BLOCKSIZEBYTES count=$((SPLIT_SIZE / $BLOCKSIZEBYTES)) skip=$((START / BLOCKSIZEBYTES)) status=none | sha256sum -c $BASE_FILES$i.sha256 | sed s#-#$BASE_FILES$i# & + fi done } @@ -47,12 +177,14 @@ function check_cloned_image { # Es existieren keine .sha256-Dateien, daher werden die Hashes beider # Seiten direkt berechnet und verglichen. for ((i=0; i backup, backup <-> destination, source <-> destination, # by making sure, that only 2 out of those 3 parameters are set. @@ -105,16 +269,37 @@ if [ -n "$SOURCE" ] && [ -n "$BASE_PATH" ] && [ -n "$DESTINATION" ]; then fi # Create spinoff variables -if [ ! -z "${BASE_PATH}" ]; then +if [ ! -z "${BASE_PATH}" ]; then BASE_FILES="${BASE_PATH}/${BASE_FILE_NAME}-" METADATA_FILE="${BASE_FILES}metadata.txt" - - # Get parameters from metadata file - NUM_JOBS=$(grep "NUM_JOBS" $METADATA_FILE | cut -d "=" -f 2) - SPLIT_SIZE=$(grep "SPLIT_SIZE" $METADATA_FILE | cut -d "=" -f 2) - BASE_FILE_TYPE=$(grep "FILE_TYPE" $METADATA_FILE | cut -d "=" -f 2) - BLOCKSIZEBYTES=$(grep "BLOCKSIZEBYTES" $METADATA_FILE | cut -d "=" -f 2) - + + # Get parameters from metadata file (lokal oder remote) + if [ $REMOTE -eq 1 ]; then + META_SRC=$(mktemp) + execute_remote_command "cat '$METADATA_FILE'" > "$META_SRC" 2>/dev/null + if [ ! -s "$META_SRC" ]; then + echo "Die Metadatendatei $METADATA_FILE auf $REMOTE_HOST existiert nicht oder ist leer." + rm -f "$META_SRC" + close_ssh_connection + exit 1 + fi + else + META_SRC="$METADATA_FILE" + fi + NUM_JOBS=$(grep "^NUM_JOBS=" "$META_SRC" | cut -d "=" -f 2) + SPLIT_SIZE=$(grep "^SPLIT_SIZE=" "$META_SRC" | cut -d "=" -f 2) + BASE_FILE_TYPE=$(grep "^FILE_TYPE=" "$META_SRC" | cut -d "=" -f 2) + BLOCKSIZEBYTES=$(grep "^BLOCKSIZEBYTES=" "$META_SRC" | cut -d "=" -f 2) + COMPRESSION=$(grep "^COMPRESSION=" "$META_SRC" | cut -d "=" -f 2) + [ $REMOTE -eq 1 ] && rm -f "$META_SRC" + + # Remote-Check unterstützt derzeit nur unkomprimierte Backups + if [ $REMOTE -eq 1 ] && [ ! -z "$COMPRESSION" ]; then + echo "Remote-Check unterstützt derzeit nur unkomprimierte Backups (netcat, uncompressed)." + close_ssh_connection + exit 1 + fi + # Debug Info: echo ${BASE_PATH} echo ${BASE_FILE_NAME} @@ -125,8 +310,13 @@ if [ ! -z "$SOURCE" ]; then INPUT_FILE_TYPE="$(file -b $SOURCE)" fi if [ ! -z "$DESTINATION" ]; then -OUTPUT_FILE=$DESTINATION -OUTPUT_FILE_TYPE="$(file -b $DESTINATION)" + OUTPUT_FILE=$DESTINATION + # Beim Remote-Clone-Check (kein BASE_PATH) liegt das Ziel remote -> file -b nicht lokal aufrufen + if [ $REMOTE -eq 1 ] && [ -z "${BASE_PATH}" ]; then + OUTPUT_FILE_TYPE=$(execute_remote_command "file -b '$DESTINATION'") + else + OUTPUT_FILE_TYPE="$(file -b $DESTINATION)" + fi fi @@ -194,3 +384,7 @@ fi wait + +if [ $REMOTE -eq 1 ]; then + close_ssh_connection +fi