Update storage docs and sync homelab scripts
Align homelab docs repo with local homelab changes, including updated storage layout and the Steam games presence checker script. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
86
homelab/docs/migrate-nextcloud-to-hdd.md
Normal file
86
homelab/docs/migrate-nextcloud-to-hdd.md
Normal file
@@ -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.
|
||||
33
homelab/docs/nextcloud-quota-fix.md
Normal file
33
homelab/docs/nextcloud-quota-fix.md
Normal file
@@ -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` при следующем пересчёте цифра должна стать адекватной.
|
||||
@@ -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.
|
||||
212
homelab/scripts/check_missing_steam_games.py
Normal file
212
homelab/scripts/check_missing_steam_games.py
Normal file
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user