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
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 |
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`)
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..60b66b1
--- /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, 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
+- 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
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
diff --git a/README.md b/README.md
index 73f3916..f1d52e5 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) |
@@ -32,7 +32,8 @@ 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: |
+| 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:) | | :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: (: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
new file mode 100644
index 0000000..8675ce9
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,444 @@
+# 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
+
+# Remote – Backup-Verzeichnis auf dem Remote-Host
+REMOTE_BACKUP_DIR=/tmp/ddpar_backup
+```
+
+### 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 – 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
+```
+
+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 – 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.
+
+### 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
+```
+
+---
+
+## 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
+```
diff --git a/ddpar-check.sh b/ddpar-check.sh
index dec5207..1f0b511 100755
--- a/ddpar-check.sh
+++ b/ddpar-check.sh
@@ -9,6 +9,12 @@
#OUTPUT_FILE_TYPE="$(file -b $OUTPUT_FILE)"
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")
@@ -19,40 +25,225 @@ 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 "-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
}
+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 backup, backup <-> destination, source <-> destination,
# by making sure, that only 2 out of those 3 parameters are set.
@@ -78,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}
@@ -98,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
@@ -142,12 +359,32 @@ if [ -z "$SOURCE" ] && [ -n "$BASE_PATH" ] && [ -n "$DESTINATION" ]; then
fi
-# Check if only $SOURCE and $DESTINATION are set
+# Check if only $SOURCE and $DESTINATION are set (Clone-Check)
if [ -n "$SOURCE" ] && [ -z "$BASE_PATH" ] && [ -n "$DESTINATION" ]; then
echo "In the loop: Comparing Source $SOURCE with Destination $DESTINATION ..."
- # Add code for this iteration here if necessary.
+ # Größe der Quelle bestimmen (kein Metadatenfile vorhanden)
+ if [[ "${INPUT_FILE_TYPE}" == "block special"* ]]; then
+ INPUT_SIZE=$(blockdev --getsize64 "$SOURCE")
+ else
+ INPUT_SIZE=$(stat -c %s "$SOURCE")
+ fi
+ SPLIT_SIZE=$((INPUT_SIZE / NUM_JOBS))
+
+ # Teilbarkeit prüfen, damit kein Bereich übersprungen oder doppelt gelesen wird
+ if [ $((INPUT_SIZE % NUM_JOBS)) -ne 0 ] || [ $((SPLIT_SIZE % BLOCKSIZEBYTES)) -ne 0 ]; then
+ echo "Fehler: Quellgröße ($INPUT_SIZE) ist nicht glatt durch NUM_JOBS ($NUM_JOBS) und BLOCKSIZEBYTES ($BLOCKSIZEBYTES) teilbar."
+ echo "Bitte -j und/oder -B passend zum ursprünglichen Clone-Aufruf wählen."
+ exit 1
+ fi
+
+ echo "Beginning to check ..."
+ check_cloned_image
fi
wait
+
+if [ $REMOTE -eq 1 ]; then
+ close_ssh_connection
+fi
diff --git a/ddpar-restore.sh b/ddpar-restore.sh
index 2ae39b1..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))
@@ -39,27 +208,38 @@ 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
+ 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"
- 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 &"
- if [[ ${OUTPUT_FILE_TYPE} != "block special"* ]]; then
- #touch $OUTPUT_FILE
- echo "fallocate -l ${INPUT_SIZE} $OUTPUT_FILE"
- fallocate -l ${INPUT_SIZE} $OUTPUT_FILE
+ FULL_CMD="${INPUT_CMD} | ${OUTPUT_CMD} &"
fi
echo "$FULL_CMD"
eval "${FULL_CMD}"
@@ -77,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
@@ -160,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
;;
*)
@@ -182,3 +387,7 @@ done
#fi
wait
+if [ $REMOTE -eq 1 ]; then
+ rm -f "$METADATA_SRC"
+ close_ssh_connection
+fi
diff --git a/ddpar.sh b/ddpar.sh
index dd57755..f5cd101 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}"
@@ -340,15 +340,13 @@ 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 {
[ "$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
@@ -374,7 +372,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
@@ -515,7 +513,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 -qE :${CURRENT_REMOTE_PORT}[^0-9]"; then
echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}"
break
else
@@ -629,7 +627,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 -qE :${CURRENT_REMOTE_PORT}[^0-9]"; then
echo -e "${INFOCOLOR}Process found on port ${CURRENT_REMOTE_PORT}. Exiting loop.${NOCOLOR}"
break
else
@@ -663,6 +661,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 +784,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 +804,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,24 +835,34 @@ 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}"
+ echo -e "${INFOCOLOR}${FULL_CMD}${NOCOLOR}"
eval "${FULL_CMD}"
done