Initial homelab docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-23 16:47:17 +03:00
commit ce731c28da
53 changed files with 6943 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env bash
#
# Упорядочивание папок игр в common/ (HDD 4 TB): GameName/GameName/ (файлы) + appmanifest в GameName/,
# затем перенос всех папок GameName из common/ на уровень выше (HDD 4 TB). Итог: HDD 4 TB/GameName/GameName/, HDD 4 TB/GameName/appmanifest_*.acf, common/ пустая.
# AppId берётся по имени папки из steam_library_with_sizes.json (резолвер resolve_steam_appid.py).
# Ручные сопоставления (CM3, Darksiders 3, DOOMEternal и др.) — в resolve_steam_appid.py MANUAL_MAP.
#
# Запуск в CT 101. Корневая директория: /mnt/nextcloud-extra/common/ (PARENT = /mnt/nextcloud-extra = HDD 4 TB).
#
# Подготовка: скопировать в контейнер скрипты и JSON (в одну папку, например /opt/scripts/):
# pct push 101 homelab/scripts/reorganize-games-common.sh /opt/scripts/
# pct push 101 homelab/scripts/resolve_steam_appid.py /opt/scripts/
# pct push 101 homelab/scripts/steam_library_with_sizes.json /opt/scripts/
#
# Dry-run (ничего не меняет, только вывод; в конце — папки без совпадений):
# ssh root@192.168.1.150 'pct exec 101 -- bash /opt/scripts/reorganize-games-common.sh /mnt/nextcloud-extra/common 1 /opt/scripts/resolve_steam_appid.py /opt/scripts/steam_library_with_sizes.json'
#
# Реальное выполнение (второй аргумент 0):
# ssh root@192.168.1.150 'pct exec 101 -- bash /opt/scripts/reorganize-games-common.sh /mnt/nextcloud-extra/common 0 /opt/scripts/resolve_steam_appid.py /opt/scripts/steam_library_with_sizes.json'
#
set -euo pipefail
ROOT="${1:?Usage: $0 <ROOT_DIR> [DRY_RUN=1] [RESOLVER_SCRIPT] [LIBRARY_JSON]}"
DRY_RUN="${2:-1}"
RESOLVER_SCRIPT="${3:-}"
LIBRARY_JSON="${4:-}"
[[ -z "$RESOLVER_SCRIPT" ]] && [[ -f ./resolve_steam_appid.py ]] && RESOLVER_SCRIPT="./resolve_steam_appid.py"
[[ -n "$RESOLVER_SCRIPT" ]] && [[ -z "$LIBRARY_JSON" ]] && LIBRARY_JSON="$(dirname "$RESOLVER_SCRIPT")/steam_library_with_sizes.json"
[[ -z "$RESOLVER_SCRIPT" ]] && {
echo "ERROR: resolve_steam_appid.py not found. Pass path as 3rd arg or run from script dir."
exit 1
}
[[ ! -f "$RESOLVER_SCRIPT" ]] && {
echo "ERROR: resolver script not found: $RESOLVER_SCRIPT"
exit 1
}
[[ -z "$LIBRARY_JSON" ]] || [[ ! -f "$LIBRARY_JSON" ]] && {
echo "ERROR: steam_library_with_sizes.json not found. Pass path as 4th arg or put next to resolver."
exit 1
}
echo "=== ROOT: $ROOT | DRY_RUN: $DRY_RUN | Resolver: $RESOLVER_SCRIPT | Library: $LIBRARY_JSON ==="
echo ""
if [[ ! -d "$ROOT" ]]; then
echo "ERROR: not a directory: $ROOT"
exit 1
fi
PARENT_DIR="$(dirname "$ROOT")"
echo "Папки после обработки будут перенесены: $ROOT -> $PARENT_DIR (уровень выше)"
echo ""
MISSING_APPID=()
MISSING_MANIFEST=()
RESOLVED=()
PROCESSED_DIRS=()
while IFS= read -r -d '' dir; do
base=$(basename "$dir")
[[ "$base" == "lost+found" ]] && continue
appid=""
appid=$(python3 "$RESOLVER_SCRIPT" "$LIBRARY_JSON" "$base" 2>/dev/null || true)
if [[ -z "$appid" ]]; then
MISSING_APPID+=("$base")
echo "[$base] appId не найден по имени — пропуск"
echo ""
continue
fi
manifest="$ROOT/appmanifest_${appid}.acf"
# Манифест может лежать в корне common/ или на уровень выше (steamapps/)
if [[ ! -f "$manifest" ]]; then
manifest_parent="$(dirname "$ROOT")/appmanifest_${appid}.acf"
if [[ -f "$manifest_parent" ]]; then
manifest="$manifest_parent"
else
MISSING_MANIFEST+=("$base -> appmanifest_${appid}.acf")
fi
fi
inner="$dir/$base"
if [[ -d "$inner" ]]; then
echo "[$base] (уже есть вложенная $base/) appId=$appid"
for item in "$dir"/*; do
[[ -e "$item" ]] || continue
iname=$(basename "$item")
[[ "$iname" == "$base" ]] && continue
[[ "$iname" == appmanifest_*.acf ]] && continue
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] mv '$item' -> '$inner/'"
else
mv "$item" "$inner/"
echo " mv $iname -> $base/"
fi
done
if [[ -f "$manifest" ]]; then
dest_manifest="$dir/appmanifest_${appid}.acf"
if [[ "$manifest" != "$dest_manifest" ]]; then
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] cp/mv '$manifest' -> '$dest_manifest'"
else
cp "$manifest" "$dest_manifest" 2>/dev/null || mv "$manifest" "$dest_manifest"
echo " appmanifest_${appid}.acf -> $dir/"
fi
fi
fi
else
echo "[$base] (создать $base/$base/ и перенести файлы) appId=$appid"
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] mkdir -p '$inner'"
else
mkdir -p "$inner"
fi
for item in "$dir"/*; do
[[ -e "$item" ]] || continue
iname=$(basename "$item")
[[ "$iname" == "$base" ]] && continue
[[ "$iname" == appmanifest_*.acf ]] && continue
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] mv '$item' -> '$inner/'"
else
mv "$item" "$inner/"
echo " mv $iname -> $base/"
fi
done
if [[ -f "$manifest" ]]; then
dest_manifest="$dir/appmanifest_${appid}.acf"
if [[ "$manifest" != "$dest_manifest" ]]; then
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] cp/mv '$manifest' -> '$dest_manifest'"
else
cp "$manifest" "$dest_manifest" 2>/dev/null || mv "$manifest" "$dest_manifest"
echo " appmanifest_${appid}.acf -> $dir/"
fi
fi
fi
fi
RESOLVED+=("$base -> $appid")
PROCESSED_DIRS+=("$base")
echo ""
done < <(find "$ROOT" -maxdepth 1 -type d ! -path "$ROOT" -print0 | sort -z)
echo "=== Перенос папок из common/ на уровень выше (HDD 4 TB) ==="
for base in "${PROCESSED_DIRS[@]}"; do
src="$ROOT/$base"
dst="$PARENT_DIR/$base"
if [[ $DRY_RUN -eq 1 ]]; then
echo " [DRY-RUN] mv '$src' -> '$PARENT_DIR/'"
else
if mv "$src" "$PARENT_DIR/" 2>/dev/null; then
echo " mv $base -> $PARENT_DIR/"
else
# Цель уже существует (например, дубликат игры) — сливаем содержимое и удаляем источник
if [[ -d "$dst" ]]; then
inner="$src/$base"
if [[ -d "$inner" ]]; then
mkdir -p "$dst/$base"
for item in "$inner"/*; do [[ -e "$item" ]] && mv "$item" "$dst/$base/"; done
rmdir "$inner" 2>/dev/null || true
fi
for f in "$src"/appmanifest_*.acf; do [[ -f "$f" ]] && cp -n "$f" "$dst/" 2>/dev/null || true; done
rm -rf "$src"
echo " merge $base -> $PARENT_DIR/ (target existed)"
else
echo " ERROR: could not move $base"
fi
fi
fi
done
echo ""
echo "=== Папки, для которых не найден appId ==="
for x in "${MISSING_APPID[@]}"; do echo " $x"; done
echo ""
echo "=== Итог: игры в $PARENT_DIR/GameName/GameName/, манифесты в $PARENT_DIR/GameName/, common/ пустая ==="
echo ""
echo "=== Нет файла appmanifest (игра найдена по имени) ==="
for x in "${MISSING_MANIFEST[@]}"; do echo " $x"; done
echo ""
echo "=== Сопоставления имя -> appId ==="
for x in "${RESOLVED[@]}"; do echo " $x"; done
echo ""
echo "=== Конец (DRY_RUN=$DRY_RUN) ==="