125 lines
8.1 KiB
Markdown
Executable File
125 lines
8.1 KiB
Markdown
Executable File
---
|
|
title: skill — truenas-administration
|
|
namespace: personal
|
|
type: tech
|
|
created: '2026-09-26'
|
|
updated: '2026-09-27'
|
|
tags:
|
|
- truenas
|
|
- skill
|
|
- docker
|
|
- acl
|
|
- nfc
|
|
- navidrome
|
|
- ndignore
|
|
related:
|
|
- '[[personal/plans/tagwriter-container]]'
|
|
- '[[personal/tech/music-tags-repair-truenas]]'
|
|
- '[[family/how-to/navidrome-truenas-android]]'
|
|
---
|
|
|
|
# Зеркало навыка `truenas-administration`
|
|
|
|
Навык: `~/.hermes/hermes-whale/skills/devops/truenas-administration/SKILL.md`
|
|
(umbrella: ACL/PUID, ZFS panic recovery, tree comparison, named-volumes→bind-mount).
|
|
|
|
Ниже — дельта. **2026-09-26:** волна 38 + сборка `tagwriter`. **2026-09-27:** добавился RULE 0
|
|
(Area 3) про `.ndignore`.
|
|
|
|
## 0. 🔴 RULE 0 (2026-09-27): любой новый `backup*` → в `.ndignore` В ТОМ ЖЕ ПРОГОНЕ
|
|
|
|
Область: `/mnt/RED_2TB/storage/Music/` — корень библиотеки Navidrome (монтируется как `/music`).
|
|
Navidrome игнорирует **только то, что перечислено в `.ndignore`**. Бэкап, которого там нет,
|
|
**сканируется как обычная музыка** и пачкает КАЖДЫЙ запрос к БД: расхождения счётчиков,
|
|
фантомные находки («альбом со скобками до сих пор в базе», хотя правка отработала),
|
|
дифф, который выглядит сломанным, хотя он верный.
|
|
|
|
**Проверенный провал 2026-09-27:** `.ndignore` создан **03:49** и содержал 4 папки
|
|
(`backup`, `backup_conv40`, `backup_fix`, `backup_raw`). Бэкапы, созданные **позже**, в файл
|
|
не попали: `backup_before_306` (**05:17**), `backup_album_root_20260927-album-root-229` (**06:19**).
|
|
Итог: БД **8167** против плана **7861**, разница **306** = неотфильтрованный бэкап.
|
|
Альбомы `(zvezda)`, `(Авария)`, `(Несчастный Случай)` «остались в базе» — они существовали
|
|
**только внутри бэкапа**.
|
|
|
|
**Фикс (боевой конфиг → порядок: бэкап ДО → правка → проверка → коммит ПОСЛЕ):**
|
|
```bash
|
|
# 1. бэкап файла
|
|
ssh truenas_admin@mallexxx.duckdns.org \
|
|
"cp /mnt/RED_2TB/storage/Music/.ndignore /mnt/RED_2TB/storage/Music/.ndignore.bak-\$(date +%Y%m%d-%H%M%S)"
|
|
# 2. перечислить ВСЕ бэкап-папки и сверить со списком
|
|
ssh truenas_admin@mallexxx.duckdns.org "cd /mnt/RED_2TB/storage/Music && ls -d */ | grep -i backup"
|
|
# 3. полный файл собрать ЛОКАЛЬНО (write_file) → scp → cp НА МЕСТО (НЕ sed на NAS!)
|
|
scp /tmp/ndignore-new truenas_admin@mallexxx.duckdns.org:/tmp/ndignore-new
|
|
ssh truenas_admin@mallexxx.duckdns.org "cp /tmp/ndignore-new /mnt/RED_2TB/storage/Music/.ndignore"
|
|
```
|
|
Затем **`docker restart navidrome`** (в контейнере `curl /api/scanner/status` → `404`, эндпоинта
|
|
нет; подхват идёт по рестарту + watcher). После скана проверить, что игнор сработал —
|
|
игнорируемые получают **`missing=1`, их НЕ удаляют**:
|
|
```bash
|
|
sqlite3 /mnt/RED_2TB/docker/navidrome/data/navidrome.db \
|
|
"SELECT COUNT(*) FROM media_file; -- всего
|
|
SELECT COUNT(*) FROM media_file WHERE missing=1; -- игнор/отсутствуют
|
|
SELECT COUNT(*) FROM media_file WHERE missing=0 AND path NOT LIKE 'backup%';" -- живая музыка
|
|
```
|
|
**Контрольное число:** живая музыка = числу строк плана (**7861** на 2026-09-27).
|
|
Факт после фикса: всего **8396**, `missing=1` **535**, живая **7861** ✅ (до фикса было 8167/306).
|
|
|
|
> 🔴 **ОБЯЗАТЕЛЬНЫЙ ФИЛЬТР в ЛЮБОМ запросе к `media_file`:**
|
|
> `missing=0 AND path NOT LIKE 'backup%'`
|
|
> Без него читаешь бэкап-копии и делаешь ЛОЖНЫЕ выводы. Эта ошибка дала неверный отчёт
|
|
> «скобки не исправлены» — бэкап был выдан за живое дерево.
|
|
> 🔑 **Правило на будущее (Alex: «Да»):** любой новый `backup*` под библиотекой — в `.ndignore`
|
|
> **в том же прогоне, что его создал**. Файл создан 03:49, бэкапы — 05:17 и 06:19; список
|
|
> просто не обновили. Скрипты бэкапа обязаны дописывать себя сами.
|
|
|
|
## 1. Создание НОВОГО каталога под `/mnt/<POOL>/docker/` — из контейнера
|
|
|
|
`/mnt/RED_2TB/docker/` = `root:root`. `truenas_admin` (uid 950) **не пишет** туда;
|
|
`sudo` требует пароль (`sudo -n` → password required). Но `docker` доступен (юзер в группе `docker`).
|
|
|
|
```bash
|
|
docker run --rm -u 0 -v /mnt/RED_2TB/docker:/docker alpine:latest sh -c \
|
|
'mkdir -p /docker/tagwriter/data && chown -R 950:950 /docker/tagwriter && chmod 755 /docker/tagwriter'
|
|
```
|
|
|
|
Затем `scp` проекта обычным путём (владелец 950).
|
|
**Правило: любой новый каталог под `docker/` создавать ИЗ КОНТЕЙНЕРА (`-u 0`), не через ssh под 950.**
|
|
|
|
## 2. 🔴 Права файлов при `scp` → `COPY` в Dockerfile
|
|
|
|
`scp` кладёт файлы как `-rw-------` (600). `COPY` **сохраняет режим** → внутри образа
|
|
`/app/<script>.py` = root:root 600 → пользователь образа (uid 950) получает
|
|
`python3: can't open file '/app/x.py': [Errno 13] Permission denied`, хотя на хосте права 644.
|
|
|
|
**Фикс:** в Dockerfile `RUN chmod 755 /app/<script>.py && chmod 755 /app` + `chmod 644` файлов проекта на хосте.
|
|
**Проверка:** `docker run --rm --entrypoint sh -u 950:950 <img> -c 'id; ls -la /app'`.
|
|
|
|
## 3. 🔴🔴 NFD на диске vs NFC в данных — путь молча НЕ находится
|
|
|
|
Батч-задача сверяла манифест (CSV) с реальными файлами → **47 «отсутствующих» файлов**, которые
|
|
лежат на диске. Причина: имена на диске в **NFD** (разложенная форма: `й` = `и` + combining breve
|
|
`U+0306`, то же для `ё`), манифест — в **NFC**. `os.path.exists()` → `False`; `ls` ничего не показывает;
|
|
выглядит как «файл удалён».
|
|
|
|
Поражённые: `Али-Баба и сорок разбойников`, `Принц и нищий (Диск 2)`, `Пеппи Длинный Чулок`,
|
|
`Летучий корабль`, `Голубой щенок` — **любое имя с `й`/`ё`**.
|
|
|
|
```python
|
|
# ✅ индекс диска: NFC(путь) -> реальный путь
|
|
idx = {}
|
|
for root, dirs, files in os.walk(BASE):
|
|
for n in files:
|
|
real = os.path.join(root, n)
|
|
idx[unicodedata.normalize("NFC", real)] = real
|
|
real = idx.get(unicodedata.normalize("NFC", wanted)) # None ⇒ действительно нет
|
|
```
|
|
|
|
**Правило: не звать `os.path.exists()` на не-ASCII путь из манифеста — строить NFC-индекс и искать по нему.**
|
|
Тот же класс, что кейс `combining breve` для `Принц и нищий (Диск 2)`; применимо к **любой**
|
|
батч-задаче с кириллическими путями, не только к музыке. Приёмка: MISSING `47 → 0`.
|
|
|
|
## Применение
|
|
|
|
Оба питфолла воспроизвелись при сборке контейнера [[personal/plans/tagwriter-container]]
|
|
(применение тегов MP3, 7041 файл). Оттуда же — команды `docker compose --profile tools run …`.
|