#!/bin/sh # Entware/Keenetic storage speedtest with device selection and multi-size runs set -u # ---------- helpers ---------- # Print to stderr err() { printf "%s\n" "$*" >&2; } # Safe integer division with 1 decimal using awk: NUM/DEN -> "X.Y" div1() { # usage: div1 NUM DEN awk 'BEGIN{n='"$1"'; d='"$2"'; if (d<1) d=1; printf "%.1f", n/d }' } # Humanize MB/s (one decimal) mbps() { # usage: mbps SIZE_MB DUR_S SIZE_MB=$1; DUR=$2 # avoid zero without if: add 1 if DUR==0 DUR=$(( DUR + (DUR==0) )) div1 "$SIZE_MB" "$DUR" } # Get epoch seconds now_s() { date +%s } # Measure dd op: args: MODE(write|read) SIZE_MB FILE measure_dd() { MODE="$1" SZ="$2" FILE="$3" case "$MODE" in write) START=$(now_s) # conv=fsync у BusyBox dd — валидно; гарантируем запись на диск dd if=/dev/zero of="$FILE" bs=1M count="$SZ" conv=fsync >/dev/null 2>&1 sync END=$(now_s) ;; read) START=$(now_s) dd if="$FILE" of=/dev/null bs=1M count="$SZ" >/dev/null 2>&1 END=$(now_s) ;; *) err "Unknown mode: $MODE"; return 2 ;; esac DUR=$(( END - START )) # защита от деления на ноль без if DUR=$(( DUR + (DUR==0) )) printf "%s" "$DUR" } # Summaries (track min/max/avg via sums) init_stats() { SUM_W=0 SUM_R=0 CNT=0 MIN_W=0 MAX_W=0 MIN_R=0 MAX_R=0 } update_stats() { # args: w_speed_str r_speed_str WS="$1"; RS="$2" CNT=$((CNT+1)) # суммируем в сотых, чтобы избегать плавающей точки в оболочке WS100=$(printf "%s" "$WS" | awk '{printf "%d",$1*100}') RS100=$(printf "%s" "$RS" | awk '{printf "%d",$1*100}') SUM_W=$((SUM_W + WS100)) SUM_R=$((SUM_R + RS100)) # min/max (строковые в десятичной — используем awk для сравнения) if [ "$CNT" -eq 1 ]; then MIN_W="$WS"; MAX_W="$WS"; MIN_R="$RS"; MAX_R="$RS" else MIN_W=$(awk 'BEGIN{a='"$WS"';b='"$MIN_W"';print (ab)?a:b}') MIN_R=$(awk 'BEGIN{a='"$RS"';b='"$MIN_R"';print (ab)?a:b}') fi } print_summary() { AVG_W=$(awk 'BEGIN{s='"$SUM_W"';c='"$CNT"';printf "%.1f", (c? s/100.0/c : 0)}') AVG_R=$(awk 'BEGIN{s='"$SUM_R"';c='"$CNT"';printf "%.1f", (c? s/100.0/c : 0)}') echo echo "=== Итоги (${CNT} теста) ===" printf "Средняя запись: %s MB/s | Мин: %s | Макс: %s\n" "$AVG_W" "$MIN_W" "$MAX_W" printf "Среднее чтение: %s MB/s | Мин: %s | Макс: %s\n" "$AVG_R" "$MIN_R" "$MAX_R" } # ---------- detect & pick device ---------- # Составляем список только реальных блок-устройств /dev/* (исключая tmpfs/overlay/loop и т.п.) # Используем df -P для стабильного парсинга (POSIX), -k чтобы всё в KiB. LIST_FILE="/tmp/stor_list.$$" trap 'rm -f "$LIST_FILE"' EXIT HUP INT TERM i=0 df -Pk | awk 'NR>1 && $1 ~ "^/dev/" {print $1, $6, $4}' | while read -r DEV MNT AVAIL_KB; do # Фильтры: игнорируем корень, если нужно, и очевидные системные точки монтирования case "$MNT" in /proc|/sys|/dev|/run) continue ;; esac i=$((i+1)) AVAIL_H=$(df -hP "$MNT" | awk 'NR==2{print $4}') printf "%d\t%s\t%s\t%s\t%s\n" "$i" "$DEV" "$MNT" "$AVAIL_KB" "$AVAIL_H" done > "$LIST_FILE" if ! [ -s "$LIST_FILE" ]; then err "Не найдены подходящие накопители (/dev/*)." exit 1 fi echo "Доступные накопители:" awk -F '\t' '{printf " %d) %s на %s (свободно %s)\n", $1, $2, $3, $5}' "$LIST_FILE" printf "Выберите номер: " read -r CHOICE # Проверка ввода: цифра и присутствует в списке case "$CHOICE" in ''|*[!0-9]*) err "Неверный выбор." exit 1 ;; esac SEL_LINE=$(awk -F '\t' '$1=='"$CHOICE"'{print; found=1} END{if(!found) exit 1}' "$LIST_FILE") || { err "Выбранный номер отсутствует." exit 1 } DEV=$(printf "%s" "$SEL_LINE" | awk -F '\t' '{print $2}') MNT=$(printf "%s" "$SEL_LINE" | awk -F '\t' '{print $3}') AVAIL_KB=$(printf "%s" "$SEL_LINE" | awk -F '\t' '{print $4}') AVAIL_MB=$(awk 'BEGIN{printf "%d", '"$AVAIL_KB"'/1024}') echo echo "Вы выбрали: $DEV, точка монтирования: $MNT, свободно: ${AVAIL_MB} MB" # ---------- decide base size ---------- BASE_MB=200 if [ "$AVAIL_MB" -lt 50 ]; then err "Недостаточно свободного места (< 50 MB). Отмена." exit 1 elif [ "$AVAIL_MB" -lt 100 ]; then BASE_MB=50 elif [ "$AVAIL_MB" -lt 200 ]; then BASE_MB=100 else BASE_MB=200 fi # ---------- test sizes ---------- SIZE1=$BASE_MB SIZE2=$(( BASE_MB / 2 )) SIZE3=$(( BASE_MB / 4 )) # гарантируем минимум 1 MB [ "$SIZE2" -lt 1 ] && SIZE2=1 [ "$SIZE3" -lt 1 ] && SIZE3=1 TESTFILE="$MNT/entware_speedtest.bin" echo echo "=== Speedtest на $MNT (база: ${BASE_MB} MB) ===" printf "План тестов (MB): %d, %d, %d\n" "$SIZE1" "$SIZE2" "$SIZE3" init_stats run_case() { SZ="$1" echo echo "[*] Тест записи: ${SZ} MB" DUR_W=$(measure_dd write "$SZ" "$TESTFILE") SPEED_W=$(mbps "$SZ" "$DUR_W") printf " Время записи: %ss | Скорость записи: %s MB/s\n" "$DUR_W" "$SPEED_W" echo "[*] Тест чтения: ${SZ} MB" DUR_R=$(measure_dd read "$SZ" "$TESTFILE") SPEED_R=$(mbps "$SZ" "$DUR_R") printf " Время чтения: %ss | Скорость чтения: %s MB/s\n" "$DUR_R" "$SPEED_R" update_stats "$SPEED_W" "$SPEED_R" # очищаем файл после каждого кейса, чтобы гарантировать место под следующий rm -f "$TESTFILE" 2>/dev/null sync } run_case "$SIZE1" run_case "$SIZE2" run_case "$SIZE3" print_summary echo echo "[*] Готово."