S.M.A.R.T-Werte einmalig per Skript systematisch erfassen um Festplatten zu verkaufen

Veröffentlicht am 12 November 2026 von oemmes — 12 min

S.M.A.R.T-Werte einmalig per Skript systematisch erfassen um Festplatten zu verkaufen

(Ui, htmly unterstützt keine Bildbeschreibung?? Hier: Ein Stapel Festplatten, Planie nach oben zeigend, jede ist zur Platte darunter etwas versetzt/verdreht damit man von allen etwas Platine und die Anschlüsse sieht)

Ich hatte einige Festplatten zu verkaufen und wollte die vorher auf ihre S.M.A.R.T-Werte prüfen.

Die meisten Festplatten sind bei mir verschlüsselt oder sicher gelöscht, aber bei allem was da so im Regal rumliegt, bin ich mir nicht sicher.

Ich benötigte also eine Möglichkeit Festplatten zu prüfen ob die noch OK sind und ob dort noch Daten drauf liegen.

Da ich selbst nicht gut, nicht schnell und nicht gern Skripte schreiben kann, habe ich mir von Claude etwas Vibecoden lassen, das sah etwa so aus (inklusive Tippfehler):

kannst Du mir ein bash script schreiben was festplatten (außer die Systemplatte) auf SMART werte testet und die ausgibt? Hier reicht ein textfile mit Datum, Zeit, Seriennummer, Smart status (laufzeit / defekte sektoren / sonstige auffälligkeiten).

Wenn ich das nochmal ausführe werden die wete angehängt.

Ich habe viele Platten die ich nacheinander testen möchte, aber nur eine 2er Docking für solche platten.

Mien plan:

anstecken skript laufen lassen platten mit ausgegebener liste verkaufen

Danach fiel mir ein, dass eventuell noch Daten drauf sein könnten, daher:

und kannst du das skript verlängern, dass es 15 Minuten lang auf wiederherstellbare daten testet (wie photorec), damit ich bemerke ob ich platten noch nicht sicher gelöscht habe?

Wenn die platte nicht sicher gelöscht wurde, sollten mit 15 Minuten prüfzeit ja ausreichend sektoren geprüft sein um das festzustellen, oder?

Nach dem das Skript erstellt und getestet war, habe ich um eine Readme gebeten:

smart-check.sh

Prüft alle Festplatten außer der Systemplatte per SMART und testet per photorec, ob noch wiederherstellbare Daten darauf liegen (also nicht sicher gelöscht wurde). Nur lesender Zugriff. Gedacht für den Workflow mit einem Dock: Platten anstecken, Skript starten, Platten austauschen.

Voraussetzungen

sudo apt install smartmontools testdisk

Benutzung

sudo ./smart-check.sh              # SMART + 15 Min. Datenrest-Scan
sudo ./smart-check.sh -t           # zusätzlich SMART-Kurztest
sudo ./smart-check.sh -m 5         # Scan nur 5 Minuten
sudo ./smart-check.sh -m 0         # nur SMART, ohne Scan (ca. 1 Sekunde pro Platte)
Option Bedeutung Standard
-m MIN Dauer des Datenrest-Scans (0 = aus) 15
-t SMART-Kurztest zusätzlich ausführen aus
-n N Scan auf N Bereiche der Platte verteilen (1 = nur Anfang) 4
-x MB Scan abbrechen, wenn mehr als MB wiederhergestellt wurden 1024

Variablen:

LOGFILE=/pfad/log.txt          # anderes Logfile (Standard: smart-log.txt neben dem Skript)
EXTRA_EXCLUDE="sdb sdc"        # weitere Platten ausschließen

Ablauf pro Dock-Belegung

  1. Zwei Platten ins Dock stecken, einschalten, kurz warten, bis sie erkannt sind (lsblk zeigt sie).
  2. sudo ./smart-check.sh starten. Beide Platten werden parallel geprüft, ein Lauf dauert etwa 15 Minuten.
  3. Ergebnis im Terminal ansehen. Es wird zusätzlich an smart-log.txt angehängt.
  4. Platten tauschen und wiederholen.

Ergebnis lesen

  • SMART-FAZIT: OK, WARNUNG (z. B. defekte oder ausstehende Sektoren, Fehler im Log) oder DEFEKT (SMART meldet Ausfall). Platten mit WARNUNG besser nicht oder nur mit Hinweis verkaufen.
  • DATEN-FAZIT:
    • NICHT SICHER GELÖSCHT: Es wurden noch Dateien gefunden. Platte vor dem Verkauf vollständig überschreiben (HDD: nwipe, SSD: blkdiscard oder Secure Erase des Herstellers).
    • nichts gefunden: nur eine Stichprobe, kein Beweis. Bei sensiblen Daten trotzdem vollständig überschreiben.
  • CRC-Fehler sind meist ein Kabel- oder Dock-Problem und kein Plattenfehler.
  • "SMART NICHT LESBAR": Das Dock reicht SMART nicht durch. Platte direkt an SATA oder ein anderes Dock hängen.

Hinweise

  • Wiederhergestellte Dateien liegen nur kurz in /var/tmp und werden sofort gelöscht. Pro Platte werden maximal 1 GB geschrieben (-x).
  • Nach Strg+C räumt das Skript Loop-Geräte und Temp-Dateien selbst auf.
  • Automatisch eingehängte Dock-Platten sind kein Problem, es wird nur gelesen.
  • Bei 4 Bereichen laufen 4 photorec-Prozesse pro Platte. Das ist bei HDDs etwas langsamer als ein Scan am Stück, erfasst dafür aber mehr der Platte.!

Hier das Script:

#!/usr/bin/env bash
# smart-check.sh – prüft alle Festplatten (außer der Systemplatte):
#   1. SMART-Werte (Laufzeit, defekte Sektoren, Auffälligkeiten)
#   2. Datenreste: photorec-Scan (Standard 15 Min.), um zu sehen, ob die Platte
#      noch wiederherstellbare Dateien enthält, also NICHT sicher gelöscht wurde
# Ergebnisse werden an ein Textfile angehängt. Es wird nichts auf die Platten
# geschrieben (nur lesender Zugriff).
#
# Benutzung:  sudo ./smart-check.sh [-m MINUTEN] [-t] [-n BEREICHE] [-x MAX_MB]
#   -m MIN   Dauer des Datenrest-Scans in Minuten (Standard 15, 0 = überspringen)
#   -t       zusätzlich SMART-Kurztest (short self-test) ausführen
#   -n N     Scan verteilt auf N Bereiche der Platte (Standard 4, 1 = nur Anfang)
#   -x MB    Abbruch, wenn pro Platte mehr als MB wiederhergestellt wurden (Standard 1024)
#
# Logfile: standardmäßig smart-log.txt neben dem Skript
#          anderes Ziel:   sudo LOGFILE=/pfad/log.txt ./smart-check.sh
# Zusätzlich ausschließen:  sudo EXTRA_EXCLUDE="sdc sdd" ./smart-check.sh
# Voraussetzung: sudo apt install smartmontools testdisk

MIN=15; SELFTEST=0; SLICES=4; MAXMB=1024
LOGFILE="${LOGFILE:-$(dirname "$(readlink -f "$0")")/smart-log.txt}"
# Dateitypen mit zuverlässigen Signaturen (wenige Fehltreffer)
TYPES="jpg png gif pdf zip doc 7z rar mov riff mp3 sqlite gz"

usage() { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; }
while getopts "m:tn:x:h" o; do
    case $o in
        m) MIN=$OPTARG ;;
        t) SELFTEST=1 ;;
        n) SLICES=$OPTARG ;;
        x) MAXMB=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
for v in MIN SLICES MAXMB; do
    [[ ${!v} =~ ^[0-9]+$ ]] || { echo "Ungültiger Wert für $v: ${!v}"; exit 1; }
done
(( SLICES < 1 )) && SLICES=1

if [[ $EUID -ne 0 ]]; then echo "Bitte als root starten: sudo $0"; exit 1; fi
command -v smartctl >/dev/null || { echo "smartctl fehlt: sudo apt install smartmontools"; exit 1; }
if (( MIN > 0 )) && ! command -v photorec >/dev/null; then
    echo "photorec fehlt: sudo apt install testdisk   (oder mit -m 0 ohne Datenrest-Scan starten)"
    exit 1
fi

# --- Systemplatte(n) ermitteln (/, /boot, /boot/efi inkl. LVM/LUKS) -----------
sys_disks=" ${EXTRA_EXCLUDE} "
for mp in / /boot /boot/efi; do
    src=$(findmnt -no SOURCE "$mp" 2>/dev/null | head -n1 | sed 's/\[.*//')
    [[ -b $src ]] || continue
    for d in $(lsblk -rnso NAME,TYPE "$src" | awk '$2=="disk"{print $1}'); do
        sys_disks+="$d "
    done
done

targets=()
while read -r n; do
    [[ $sys_disks == *" $n "* ]] && continue
    targets+=("$n")
done < <(lsblk -dnro NAME,TYPE | awk '$2=="disk"{print $1}' | grep -Ev '^(loop|zram|ram|nbd|md)')

if (( ${#targets[@]} == 0 )); then
    echo "Keine Platte außer der Systemplatte gefunden." | tee -a "$LOGFILE"
    exit 0
fi

WORK=$(mktemp -d /var/tmp/smartcheck.XXXXXX)
: > "$WORK/loops.txt"

cleanup() {
    trap - EXIT INT TERM
    local p
    for p in $(jobs -p); do kill "$p" 2>/dev/null; done
    pkill -P $$ 2>/dev/null
    sleep 1
    if [[ -f $WORK/loops.txt ]]; then
        while read -r lp; do [[ -n $lp ]] && losetup -d "$lp" 2>/dev/null; done < "$WORK/loops.txt"
    fi
    rm -rf "$WORK"
}
trap cleanup EXIT
trap 'echo; echo "Abgebrochen."; exit 130' INT TERM

# --- SMART-Hilfsfunktionen ------------------------------------------------------
declare -A DT
detect_dt() {   # USB-Docks brauchen oft "-d sat"
    local o
    o=$(smartctl -i "/dev/$1" 2>&1)
    if grep -qi "serial number" <<<"$o"; then DT[$1]=""; return; fi
    o=$(smartctl -i -d sat "/dev/$1" 2>&1)
    if grep -qi "serial number" <<<"$o"; then DT[$1]="-d sat"; else DT[$1]=""; fi
}
gt0() { [[ -n $1 && $1 =~ ^[0-9]+$ && $1 -gt 0 ]]; }
attr() {
    awk -v id="$1" '/^ID#/{f=1;next} f&&NF==0{f=0} f&&$1==id{print $10;exit}' <<<"$out" \
        | sed 's/[^0-9].*//'
}
field() { grep -m1 -iE "^($1):" <<<"$out" | sed 's/^[^:]*:[[:space:]]*//'; }
nvme()  { field "$1" | sed 's/,//g; s/[^0-9].*//'; }

# --- Datenrest-Scan (photorec) für eine Platte ---------------------------------
PR_CMD="partition_none,fileopt,everything,disable"
for t in $TYPES; do PR_CMD+=",$t,enable"; done
PR_CMD+=",wholespace,search"

any_alive() { local p; for p in "$@"; do kill -0 "$p" 2>/dev/null && return 0; done; return 1; }

scan_disk() {
    local name=$1 dev="/dev/$1" base="$WORK/$1"
    local size mib=1048576 slice i off lp used limit=0
    local -a loops=() pids=()
    mkdir -p "$base"
    size=$(blockdev --getsize64 "$dev" 2>/dev/null || echo 0)

    # Platte in N Bereiche teilen (nur lesende Loop-Geräte), damit nicht nur der
    # Plattenanfang geprüft wird
    if (( SLICES > 1 && size > SLICES * 1024 * mib )); then
        slice=$(( size / SLICES / mib * mib ))
        for ((i = 0; i < SLICES; i++)); do
            off=$(( i * slice ))
            lp=$(losetup -f --show -r -o "$off" --sizelimit "$slice" "$dev" 2>/dev/null)
            if [[ -z $lp ]]; then
                for lp in "${loops[@]}"; do losetup -d "$lp" 2>/dev/null; done
                loops=()
                break
            fi
            echo "$lp" >> "$WORK/loops.txt"
            loops+=("$lp")
        done
    fi
    (( ${#loops[@]} == 0 )) && loops=("$dev")   # Fallback: nur Plattenanfang

    i=0
    for lp in "${loops[@]}"; do
        mkdir -p "$base/s$i"
        TERM=vt100 timeout "$((MIN * 60))" photorec /d "$base/s$i/" /cmd "$lp" "$PR_CMD" >/dev/null 2>&1 &
        pids+=($!)
        i=$((i + 1))
    done

    # Platz auf der Systemplatte schützen
    while any_alive "${pids[@]}"; do
        used=$(du -sm "$base" 2>/dev/null | cut -f1)
        if [[ -n $used ]] && (( used > MAXMB )); then
            limit=1
            kill "${pids[@]}" 2>/dev/null
            break
        fi
        sleep 5
    done
    wait "${pids[@]}" 2>/dev/null

    for lp in "${loops[@]}"; do
        [[ $lp == /dev/loop* ]] && losetup -d "$lp" 2>/dev/null
    done

    local n mb types
    n=$(find "$base" -type f ! -name report.xml 2>/dev/null | wc -l)
    mb=$(du -sm "$base" 2>/dev/null | cut -f1)
    types=$(find "$base" -type f ! -name report.xml 2>/dev/null | sed 's/.*\.//' \
            | sort | uniq -c | sort -rn | head -5 | awk '{printf "%s:%s ", $2, $1}')
    printf 'R_FILES=%s\nR_MB=%s\nR_LIMIT=%s\nR_SLICES=%s\nR_TYPES="%s"\n' \
        "$n" "${mb:-0}" "$limit" "${#loops[@]}" "$types" > "$base.res"
    rm -rf "$base"   # wiederhergestellte Dateien sofort wieder löschen
}

# --- Ablauf ---------------------------------------------------------------------
{
    echo "=================================================================="
    echo "Lauf: $(date '+%Y-%m-%d %H:%M:%S')   (Systemplatte ausgenommen:${sys_disks})"
    echo "=================================================================="
} | tee -a "$LOGFILE"

echo "Gefundene Platten: ${targets[*]}"
for n in "${targets[@]}"; do detect_dt "$n"; done

if (( SELFTEST )); then
    echo "Starte SMART-Kurztests ..."
    for n in "${targets[@]}"; do smartctl ${DT[$n]} -t short "/dev/$n" >/dev/null 2>&1; done
fi

if (( MIN > 0 )); then
    echo "Starte Datenrest-Scan auf allen Platten parallel (${MIN} Min., ${SLICES} Bereiche je Platte) ..."
    for n in "${targets[@]}"; do scan_disk "$n" & done
    start=$SECONDS
    while [[ -n $(jobs -r) ]]; do
        sleep 30
        left=$(( (MIN * 60 - (SECONDS - start)) / 60 ))
        (( left >= 0 )) && echo "  ... läuft, noch ca. ${left} Min."
    done
    wait
fi

if (( SELFTEST )); then
    echo "Warte auf Ende der SMART-Kurztests (max. 15 Min.) ..."
    for ((w = 0; w < 90; w++)); do
        busy=0
        for n in "${targets[@]}"; do
            smartctl ${DT[$n]} -c "/dev/$n" 2>&1 | grep -qi "in progress" && busy=1
        done
        (( busy )) || break
        sleep 10
    done
fi

# --- Berichte -------------------------------------------------------------------
SUMMARY=()
for name in "${targets[@]}"; do
    dev="/dev/$name"
    out=$(smartctl ${DT[$name]} -a "$dev" 2>&1); rc=$?
    now=$(date '+%Y-%m-%d %H:%M:%S')
    serial=$(field "Serial Number")

    if [[ -z $serial ]]; then
        block="[$now] $dev – SMART NICHT LESBAR (Dock/Controller unterstützt es evtl. nicht, oder Platte defekt)"$'\n'
        block+="$(echo "$out" | head -n 5 | sed 's/^/    /')"$'\n'
        status="UNBEKANNT"; serial="unbekannt"
    else
        model=$(field "Device Model|Model Number|Product")
        cap=$(field "User Capacity|Total NVM Capacity" | sed -n 's/.*\[\(.*\)\].*/\1/p')
        health=$(grep -m1 -iE "overall-health|SMART Health Status" <<<"$out" | sed 's/.*: *//')

        if grep -q "NVMe" <<<"$out" && grep -q "Power On Hours" <<<"$out"; then
            hours=$(nvme "Power On Hours"); realloc="-"; pending="-"; offunc="-"; crc="-"
            temp=$(nvme "Temperature"); wear=$(nvme "Percentage Used")
            errcount=$(nvme "Media and Data Integrity Errors")
        else
            hours=$(attr 9); realloc=$(attr 5); pending=$(attr 197)
            offunc=$(attr 198); crc=$(attr 199); temp=$(attr 194); wear=""
            errcount=$(grep -m1 "^ATA Error Count" <<<"$out" | sed 's/[^0-9]*\([0-9]*\).*/\1/')
        fi

        status="OK"; notes=()
        if [[ $health =~ FAIL ]]; then status="DEFEKT"; notes+=("SMART-Gesamtstatus: FAILED"); fi
        if gt0 "$realloc"; then [[ $status == OK ]] && status="WARNUNG"; notes+=("Reallocated Sectors: $realloc"); fi
        if gt0 "$pending"; then [[ $status == OK ]] && status="WARNUNG"; notes+=("Pending Sectors: $pending"); fi
        if gt0 "$offunc";  then [[ $status == OK ]] && status="WARNUNG"; notes+=("Offline Uncorrectable: $offunc"); fi
        if gt0 "$crc";     then notes+=("UDMA-CRC-Fehler: $crc (oft nur Kabel/Dock)"); fi
        if gt0 "$errcount"; then [[ $status == OK ]] && status="WARNUNG"; notes+=("Fehler im Error-Log: $errcount"); fi
        if grep -E "^# *[0-9]+" <<<"$out" | grep -qiE "failure|failed|error"; then
            [[ $status == OK ]] && status="WARNUNG"; notes+=("Fehlgeschlagener Selbsttest im Log")
        fi
        if gt0 "$wear" && [[ $wear -ge 80 ]]; then notes+=("SSD-Verschleiß: ${wear}%"); fi
        if (( rc & 8 )); then status="DEFEKT"; notes+=("smartctl: Platte meldet bevorstehenden Ausfall"); fi
        if (( rc & 16 )); then [[ $status == OK ]] && status="WARNUNG"; notes+=("smartctl: Pre-Fail-Attribut unter Schwellwert"); fi

        if [[ -n $hours ]]; then
            laufzeit="${hours} h ($((hours / 8760)) J $((hours % 8760 / 24)) T)"
        else
            laufzeit="unbekannt"
        fi

        block="[$now] $dev   Seriennummer: $serial"$'\n'
        block+="    Modell:            $model  ${cap:+($cap)}"$'\n'
        block+="    SMART-Status:      ${health:-unbekannt}"$'\n'
        block+="    Laufzeit:          $laufzeit"$'\n'
        block+="    Defekte Sektoren:  realloc=${realloc:-?}  pending=${pending:-?}  offline-uncorr=${offunc:-?}"$'\n'
        block+="    Sonstiges:         CRC-Fehler=${crc:-?}  Fehlerlog=${errcount:-0}  Temp=${temp:-?}°C"$'\n'
        if ((${#notes[@]})); then
            block+="    Auffälligkeiten:   $(IFS=';'; echo "${notes[*]}" | sed 's/;/; /g')"$'\n'
        else
            block+="    Auffälligkeiten:   keine"$'\n'
        fi
        block+="    >>> SMART-FAZIT:   $status"$'\n'
    fi

    # Datenreste
    sigs=$( { wipefs "$dev" 2>/dev/null | awk 'NR>1{print $3}'; lsblk -rno FSTYPE "$dev" 2>/dev/null; } \
            | grep . | sort -u | paste -sd, -)
    block+="    Signaturen:        ${sigs:-keine Partitionstabelle/Dateisystem erkannt}"$'\n'
    datenrest="nicht geprüft"
    if (( MIN > 0 )) && [[ -f $WORK/$name.res ]]; then
        R_FILES=0; R_MB=0; R_LIMIT=0; R_SLICES=1; R_TYPES=""
        # shellcheck disable=SC1090
        source "$WORK/$name.res"
        if (( R_FILES > 0 )); then
            datenrest="NICHT SICHER GELÖSCHT"
            block+="    Datenrest-Scan:    ${R_FILES} Dateien wiederherstellbar (~${R_MB} MB; ${R_TYPES% })"$'\n'
            (( R_LIMIT )) && block+="                       (Scan wegen ${MAXMB}-MB-Limit vorzeitig beendet)"$'\n'
        else
            datenrest="nichts gefunden"
            block+="    Datenrest-Scan:    keine Dateien gefunden (Stichprobe: ${MIN} Min., ${R_SLICES} Bereich(e); kein Beweis für vollständiges Löschen)"$'\n'
        fi
    elif (( MIN > 0 )); then
        block+="    Datenrest-Scan:    kein Ergebnis (Scan fehlgeschlagen?)"$'\n'
    fi
    block+="    >>> DATEN-FAZIT:   $datenrest"$'\n'
    block+="------------------------------------------------------------------"

    printf '%s\n' "$block" | tee -a "$LOGFILE"
    SUMMARY+=("$dev | $serial | SMART: $status | Daten: $datenrest")
done

{
    echo "ZUSAMMENFASSUNG $(date '+%Y-%m-%d %H:%M:%S')"
    printf '  %s\n' "${SUMMARY[@]}"
    echo
} | tee -a "$LOGFILE"
echo "Log: $LOGFILE"