diff --git a/homelab/docs/migrate-nextcloud-to-hdd.md b/homelab/docs/migrate-nextcloud-to-hdd.md new file mode 100644 index 0000000..2a5e5cd --- /dev/null +++ b/homelab/docs/migrate-nextcloud-to-hdd.md @@ -0,0 +1,86 @@ +# Перенос Nextcloud с SSD на HDD (освобождение SSD 1.9 TB) + +## Цель +Перенести все данные Nextcloud (БД, приложение, файлы пользователей, ~73 GB) с SSD (sdb 1.9 TB) на HDD (sdd 6.8 TB) и отмонтировать SSD для использования в других проектах. + +## Текущая схема +- **mp0:** `/mnt/ssd-storage/nextcloud-101` → `/mnt/nextcloud-data` (SSD, ~73 GB) +- **mp1:** `/mnt/nextcloud-hdd` → `/mnt/nextcloud-extra` (HDD, игры в корне) + +После переноса: +- **mp0:** `/mnt/nextcloud-hdd/nextcloud-101` → `/mnt/nextcloud-data` (на HDD) +- **mp1:** `/mnt/nextcloud-hdd` → `/mnt/nextcloud-extra` (как сейчас) +- Внешнее хранилище «Игры» будет указывать на `/mnt/nextcloud-extra/games` (подпапка), чтобы в списке не было каталога `nextcloud-101`. + +## Шаги + +### 1. Остановить Nextcloud и контейнер +На хосте: +```bash +ssh root@192.168.1.150 +pct exec 101 -- bash -c 'cd /opt/nextcloud && docker compose down' +pct stop 101 +``` + +### 2. На хосте: подготовить структуру на HDD +Создать папку `games` и перенести в неё все текущие папки из корня HDD (игры + common): +```bash +# На хосте 192.168.1.150 +mkdir -p /mnt/nextcloud-hdd/games +cd /mnt/nextcloud-hdd +for d in */ ; do + [ "$d" = "games/" ] && continue + mv "$d" games/ +done +``` +Проверка: в корне HDD остаются только `games/`, в нём — все 154 папки. + +### 3. Копировать nextcloud-101 с SSD на HDD +```bash +rsync -av --progress /mnt/ssd-storage/nextcloud-101/ /mnt/nextcloud-hdd/nextcloud-101/ +``` +Проверить объём: `du -sh /mnt/nextcloud-hdd/nextcloud-101` (~73 GB). + +### 4. Изменить конфиг контейнера 101 +```bash +# Заменить mp0 в /etc/pve/lxc/101.conf +# Было: mp0: /mnt/ssd-storage/nextcloud-101,mp=/mnt/nextcloud-data +# Стало: mp0: /mnt/nextcloud-hdd/nextcloud-101,mp=/mnt/nextcloud-data +sed -i 's|mp0: /mnt/ssd-storage/nextcloud-101,mp=/mnt/nextcloud-data|mp0: /mnt/nextcloud-hdd/nextcloud-101,mp=/mnt/nextcloud-data|' /etc/pve/lxc/101.conf +``` + +### 5. Запустить контейнер и Nextcloud +```bash +pct start 101 +# Подождать загрузки, затем: +pct exec 101 -- bash -c 'cd /opt/nextcloud && docker compose up -d' +``` + +### 6. Обновить путь внешнего хранилища «Игры» +Сейчас хранилище указывает на `/mnt/nextcloud-extra`. Нужно изменить на `/mnt/nextcloud-extra/games`, чтобы в «Игры» отображались только игры, без папки `nextcloud-101`: +```bash +pct exec 101 -- docker exec nextcloud-nextcloud-1 php occ files_external:config 1 datadir /mnt/nextcloud-extra/games +pct exec 101 -- docker exec nextcloud-nextcloud-1 php occ files_external:scan 1 +``` + +### 7. Проверить работу +- Открыть Nextcloud в браузере, зайти в «Игры» — должны быть все папки игр. +- Проверить «Мои файлы» и приложение. + +### 8. Отмонтировать SSD и убрать из fstab +Когда всё проверено и SSD больше не нужен: +```bash +umount /mnt/ssd-storage +# Удалить или закомментировать строку с /mnt/ssd-storage в /etc/fstab +sed -i.bak '/\/mnt\/ssd-storage/d' /etc/fstab +``` +После этого SSD можно физически отключить или использовать под другие разделы. + +--- + +## Сократить SSD до 200 GB (альтернатива) +Если SSD нужно оставить в сервере, но выделить под Nextcloud только 200 GB, а остальное под другие проекты: +- Текущее использование на SSD ~73 GB — в 200 GB поместится. +- Нужно: уменьшить раздел sdb1 и файловую систему до 200 GB (опасно, только с резервной копией), затем создать второй раздел на свободном месте. Либо сделать полный бэкап, переразбить диск (например, sdb1=200G, sdb2=остальное), отформатировать, восстановить данные. Это сложнее и рискованнее, чем перенос на HDD. + +Рекомендация: перенос на HDD проще и освобождает весь SSD. diff --git a/homelab/docs/nextcloud-quota-fix.md b/homelab/docs/nextcloud-quota-fix.md new file mode 100644 index 0000000..d3e7819 --- /dev/null +++ b/homelab/docs/nextcloud-quota-fix.md @@ -0,0 +1,33 @@ +# Исправление отображения квоты и размера «Игры» в Nextcloud + +## Проблемы +1. **«Использовано 928,4 GB»** — устаревшее значение (остаток от папки «Игры» на SSD до переноса). +2. **«Игры» → «Ожидается»** — размер внешней папки ещё не посчитан или считается в фоне. + +## Что сделано на сервере + +### 1. Сброс кэша квоты +- Удалена запись `lastSeenQuotaUsage` для пользователя kerrad в `oc_preferences`. При следующем заходе Nextcloud пересчитает использование заново. + +### 2. Очистка устаревшего кэша хранилища +- В БД оставался старый «storage» для пути `/mnt/nextcloud-extra/` (корень до переноса игр в `games/`). В нём было ~844 тыс. записей и ~29 TB в кэше — это могло влиять на расчёт квоты. +- Удалены все записи этого хранилища из `oc_filecache` и строка из `oc_storages`. Сейчас используется только хранилище `local::/mnt/nextcloud-extra/games/`. + +### 3. Пересканирование файлов +- Выполнен `occ files:scan kerrad --all`: обновлён кэш домашнего хранилища (удалено 161 устаревшая запись). + +## Что сделать тебе + +1. **Жёсткое обновление страницы** + Обнови страницу Nextcloud с очисткой кэша браузера: **Ctrl+Shift+R** (или Cmd+Shift+R на Mac). Либо открой Nextcloud в режиме инкогнито. + После этого «Использовано» должно пересчитаться (ожидаемо порядка **~8–9 GB** для «домашних» файлов без учёта внешнего хранилища, в зависимости от настроек квоты). + +2. **«Игры» → «Ожидается»** + Размер больших внешних каталогов Nextcloud часто считает в фоне. Подожди несколько минут или зайди в «Игры» и открой папку — расчёт может запуститься/обновиться. Если через 10–15 минут по-прежнему «Ожидается», можно запустить на сервере повторное сканирование внешнего хранилища: + ```bash + ssh root@192.168.1.150 'pct exec 101 -- docker exec nextcloud-nextcloud-1 php occ files_external:scan 1' + ``` + +## Если 928 GB снова появится + +Проверь в настройках пользователя (Администрирование → Пользователи → kerrad), включено ли **«Учитывать внешние хранилища в квоте»**. Если да и в кэше внешнего хранилища были старые данные — значение могло быть завышено. После очистки старого storage и сброса `lastSeenQuotaUsage` при следующем пересчёте цифра должна стать адекватной. diff --git a/homelab/docs/storage-mount-layout.md b/homelab/docs/storage-mount-layout.md index e9aa33f..a368c3e 100644 --- a/homelab/docs/storage-mount-layout.md +++ b/homelab/docs/storage-mount-layout.md @@ -6,44 +6,34 @@ | Устройство | Размер | Раздел | Точка монтирования | Использовано | |------------|--------|--------|--------------------|--------------| -| **sdd** (WDC WD80EFPX 8 TB) | 7.28 TiB | sdd1 **4 TB** ext4 | `/mnt/nextcloud-hdd` | 3.4 TB | -| **sdb** (SATA SSD) | 1.9 TB | sdb1 ext4 | `/mnt/ssd-storage` | 932 GB | +| **sdd** (WDC WD80EFPX 8 TB) | 7.28 TiB | sdd1 **6.8 TB** ext4 | `/mnt/nextcloud-hdd` | ~5 TB (Nextcloud + Игры) | -- На диске **sdd** занята только часть: раздел sdd1 = 4 TB. Свободно ~3.3 TB неразмеченного места. +- **SSD (sdb) отмонтирован** и убран из fstab — используется в других проектах. - **LVM не используется** для этих дисков — обычные разделы ext4. ### Контейнер 101 (Nextcloud) | Хост (источник) | В контейнере (mp) | |------------------|-------------------| -| `/mnt/ssd-storage/nextcloud-101` | `/mnt/nextcloud-data` | +| `/mnt/nextcloud-hdd/nextcloud-101` | `/mnt/nextcloud-data` (БД, приложение, файлы пользователей) | | `/mnt/nextcloud-hdd` | `/mnt/nextcloud-extra` | -- **«HDD 4 TB»** в Nextcloud = `/mnt/nextcloud-extra` в CT = хост `/mnt/nextcloud-hdd` (диск sdd, 4 TB). -- **«Игры»** = папка внутри данных Nextcloud на SSD: - - В CT: `/mnt/nextcloud-data/html/data/kerrad/files/Игры` (≈928 GB) - - На хосте: `/mnt/ssd-storage/nextcloud-101/html/data/kerrad/files/Игры` +- **«Игры»** в Nextcloud = внешнее хранилище, путь в CT: `/mnt/nextcloud-extra/games` (подпапка на HDD). Все игры в `games/`. +- Nextcloud (pgdata, html, данные пользователей) перенесён с SSD на HDD в `/mnt/nextcloud-hdd/nextcloud-101`. ### Раздел sdd (8 TB диск) -``` -Disk /dev/sdd: 7.28 TiB -Disklabel type: gpt -/dev/sdd1 Start 2048 End 8589936639 Size 4T (Linux filesystem) - ↑ свободно до конца диска ~3.3 TB -``` - -Расширение до 6.5 TB: увеличить конец раздела sdd1, затем `resize2fs`. Это не LVM, а рост обычного раздела + файловой системы. +Раздел sdd1 расширен до 6.8 TB, используется под Nextcloud (nextcloud-101) и игры (games/). --- ## План: один том «Игры» 7.5 TB + том «Прочее» 200 GB 1. **Расширить раздел и ФС на sdd до 7.5 TB** (на хосте, при остановленном использовании тома). ✅ Сделано. -2. **Перенести содержимое «Игры»** (928 GB) в `/mnt/nextcloud-hdd` (то есть в текущий nextcloud-extra) и удалить том Игры. -3. **Размонтировать/удалить** только папку «Игры» из файлов (данные Игры убираем с SSD; место освобождается). -4. **В Nextcloud:** убрать отображение папки «Игры», оставить одно хранилище и переименовать его в Игры. -5. **Смонтировать новый раздел** с SSD диска на 200 гб, назвать его Прочее и подключить как внешнюю папку +2. **Перенести содержимое «Игры»** (928 GB) в `/mnt/nextcloud-hdd` (nextcloud-extra) и перенести игры из common в корень. ✅ Сделано. +3. **Удалить содержимое папки «Игры»** на SSD (освобождение ~928 GB). ✅ В процессе (find -delete). +4. **В Nextcloud:** внешнее хранилище переименовано в «Игры», переиндексация (files_external:scan) запущена. ✅ Сделано. +5. **Смонтировать новый раздел** с SSD диска на 200 гб, назвать его Прочее и подключить как внешнюю папку — в планах. Итого: одна сетевая папка Игры = смонтированный расширенный том **7.5 TB** на sdd. Вторая сетевая папка Прочее = смонтированный как внешняя папка том 200 GB на SSD. \ No newline at end of file diff --git a/homelab/scripts/check_missing_steam_games.py b/homelab/scripts/check_missing_steam_games.py new file mode 100644 index 0000000..ab3ccd3 --- /dev/null +++ b/homelab/scripts/check_missing_steam_games.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import json +import os +import sys +import unicodedata +from typing import Dict, List, Tuple, Any + + +DEFAULT_GAMES_JSON = os.path.join( + os.path.dirname(__file__), "steam_library_with_sizes.json" +) +DEFAULT_GAMES_ROOT = "/mnt/nextcloud-hdd/games" +IGNORE_DIRS = {".", "..", "common", "lost+found"} + + +def normalize(text: str) -> str: + """Normalize game / directory names for comparison.""" + text = unicodedata.normalize("NFKD", text) + text = "".join(ch for ch in text if not unicodedata.combining(ch)) + text = text.lower() + return "".join(ch for ch in text if ch.isalnum()) + + +def load_games(json_path: str) -> List[Dict[str, Any]]: + with open(json_path, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, list): + raise ValueError(f"Unexpected JSON structure in {json_path}, expected list") + return data + + +def list_game_dirs(root: str) -> List[str]: + entries: List[str] = [] + for name in os.listdir(root): + if name in IGNORE_DIRS or name.startswith("."): + continue + full = os.path.join(root, name) + if os.path.isdir(full): + entries.append(name) + return sorted(entries) + + +def build_dir_index(dirs: List[str]) -> Dict[str, List[str]]: + index: Dict[str, List[str]] = {} + for d in dirs: + nd = normalize(d) + index.setdefault(nd, []).append(d) + return index + + +def similar_candidates( + norm_name: str, dirs: List[str] +) -> Tuple[float, List[str]]: + from difflib import SequenceMatcher + + best_ratio = 0.0 + best: List[str] = [] + for d in dirs: + nd = normalize(d) + ratio = SequenceMatcher(None, norm_name, nd).ratio() + if ratio > best_ratio: + best_ratio = ratio + best = [d] + elif ratio == best_ratio and ratio > 0: + best.append(d) + return best_ratio, best + + +def classify_games( + games: List[Dict[str, Any]], server_dirs: List[str] +) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, int]]: + norm_dir_map = build_dir_index(server_dirs) + + missing: List[Dict[str, Any]] = [] + doubtful: List[Dict[str, Any]] = [] + + for g in games: + appid = str(g.get("appid", "")) + name = g.get("name", "") + norm_name = normalize(name) + + candidates = norm_dir_map.get(norm_name, []) + if len(candidates) == 1: + # Confident уникальное соответствие: игра считается присутствующей + continue + + record = { + "appid": appid, + "name": name, + "attempted_match": False, + "reason": "", + } + + if len(candidates) > 1: + record["attempted_match"] = True + record[ + "reason" + ] = f"Найдено несколько директорий с одинаковым нормализованным именем: {candidates}" + missing.append(record) + doubtful.append( + { + "appid": appid, + "name": name, + "candidates": candidates, + "confidence": "низкая уверенность (несколько совпадений по нормализованному имени)", + } + ) + continue + + # Нет точного совпадения по нормализованному имени: ищем похожие директории, + # но не считаем их найденными. + best_ratio, best_matches = similar_candidates(norm_name, server_dirs) + + if best_ratio >= 0.85: + record["attempted_match"] = True + record["reason"] = ( + "Найден(ы) похожие директории, но совпадение не однозначное " + f"(коэффициент схожести {best_ratio:.2f}, кандидаты: {best_matches})" + ) + if best_ratio >= 0.92: + conf = "вероятно совпадение" + else: + conf = "низкая уверенность" + doubtful.append( + { + "appid": appid, + "name": name, + "candidates": best_matches, + "confidence": f"{conf} (similarity={best_ratio:.2f})", + } + ) + else: + record[ + "reason" + ] = "Похожих директорий не найдено (по нормализованному имени)" + + missing.append(record) + + stats = { + "total_games_in_json": len(games), + "total_server_dirs": len(server_dirs), + "missing_count": len(missing), + "doubtful_count": len(doubtful), + } + return missing, doubtful, stats + + +def print_markdown( + missing: List[Dict[str, Any]], doubtful: List[Dict[str, Any]], stats: Dict[str, int] +) -> None: + print("### Статистика") + print() + print(f"- **Всего игр в JSON**: {stats['total_games_in_json']}") + print(f"- **Всего директорий на сервере**: {stats['total_server_dirs']}") + print(f"- **Отсутствующие игры**: {stats['missing_count']}") + print(f"- **Сомнительные сопоставления**: {stats['doubtful_count']}") + print() + + print("### Игры, отсутствующие на сервере") + print() + if not missing: + print("Все игры из JSON найдены на сервере по строгому сопоставлению.") + else: + print("| appid | name | попытка сопоставления | комментарий |") + print("| --- | --- | --- | --- |") + for r in missing: + attempted = "да" if r["attempted_match"] else "нет" + reason = r.get("reason", "").replace("\n", " ") + print(f"| {r['appid']} | {r['name']} | {attempted} | {reason} |") + print() + + print("### Сомнительные/неоднозначные сопоставления") + print() + if not doubtful: + print("Сомнительных сопоставлений не обнаружено.") + return + + print("| appid | name | директории-кандидаты | степень уверенности |") + print("| --- | --- | --- | --- |") + for d in doubtful: + candidates = ", ".join(d.get("candidates") or []) + conf = d.get("confidence", "") + print(f"| {d['appid']} | {d['name']} | {candidates} | {conf} |") + + +def main(argv: List[str]) -> int: + games_json = DEFAULT_GAMES_JSON + games_root = DEFAULT_GAMES_ROOT + + if len(argv) > 1: + games_json = argv[1] + if len(argv) > 2: + games_root = argv[2] + + if not os.path.isfile(games_json): + print(f"ERROR: JSON file not found: {games_json}", file=sys.stderr) + return 1 + if not os.path.isdir(games_root): + print(f"ERROR: Games root not found: {games_root}", file=sys.stderr) + return 1 + + games = load_games(games_json) + server_dirs = list_game_dirs(games_root) + missing, doubtful, stats = classify_games(games, server_dirs) + print_markdown(missing, doubtful, stats) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) +