T4.2 Nextcloud migré (07c) + Collabora: --o:ssl.enable=false (TLS image par défaut)

This commit is contained in:
Effi
2026-10-11 13:04:34 +02:00
parent a1864c69e7
commit 34e0d470c1
11 changed files with 851 additions and 54 deletions
+75
View File
@@ -0,0 +1,75 @@
# Phase 4 — récupération du thread supprimé
**Session** : `e67f1166-d70c-42cd-a295-79513757233e` (tâche Kanban `8e3e7f073a6806cd` — PHASE 4)
**Suppression** : 2026-10-11 07:20:04 UTC (trace systemd : `Runner error FOREIGN KEY constraint failed, sessionId=e67f1166…`)
**Méthode** : suppression = `DELETE` sur `sessions` + cascade sur `events`. Les pages SQLite libérées gardent leur contenu tant qu'elles ne sont pas réallouées → caravage des pages libres de `sessions.db` (snapshot), reconstruction des enregistrements.
## 1. Ce qui a été récupéré
| Élément | Valeur |
|---|---|
| Snapshot de travail | `snap1` pris à 08:20:34 UTC (1 h après la suppression) |
| Pages contenant l'id de session | 21 232 |
| Texte imprimable ≥ 400 caractères | 34 227 blocs = **47,9 Mo** |
| Objets JSON payload reconstitués | **51 420 = 34,68 Mo** |
| Entrées d'index `events` (session_id, seq / event_type) | 79 830 → 37 821 seq, 42 009 event_type, plage seq 5 → 158 799 |
| Tours de conversation reconstitués | **130** (129 assistant + 3 user côté rôles) |
| Événements réinsérés dans `sessions.db` | **64 657** (33,85 Mo de payload) |
| Appels d'outils avec arguments intacts | 31 998 |
| Résultats / sorties d'outils | 124 / 275 |
## 2. Ce qui est perdu (non inventé)
- **~75 % des événements** : les pages leaf/overflow ont été réallouées et **zérées** par SQLite après la suppression (15 951 pages contenaient encore l'id de session mais avec leur tableau de cellules remis à zéro).
- **La prose du fil** (messages de l'assistant) est récupérée **en fragments dans le désordre** : les deltas de streaming sont stockés dans des pages distinctes, l'ordre d'origine n'a pas pu être reconstitué. Les 130 messages insérés contiennent donc des fragments réels, recollés dans l'ordre des pages (ordre d'allocation ≈ ordre chronologique), pas un texte propre.
- **Presque tous les messages utilisateur** sont perdus (payloads courts, pages détruites) : 3 rôles `user` identifiés, sans contenu.
- Volume attendu ≈ 158 800 événements / 80–110 Mo de payload → **≈ 35 % récupéré**.
## 3. Où sont les artefacts
| Fichier | Contenu |
|---|---|
| `/home/folzo/openfox/_recover/snap1/` | copie octet-pour-octet d'origine (`sessions.db`, `-wal`, `-shm`) |
| `/home/folzo/openfox/_recover/orig.sha256` | sha256 des fichiers d'origine |
| `/home/folzo/openfox/_recover/snap2/` + `snap2.sha256` | état **immédiatement avant** la réinsertion (point de rollback) |
| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON bruts (+ page, offset) |
| `/home/folzo/openfox/_recover/phase4_index.jsonl` | 79 830 entrées d'index (rowid, seq, event_type) |
| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements synthétisé |
| `g9-migration/PHASE4-RECOLLEMENT.md` §5 | journal des actions récupérées |
## 4. Rollback
**Chirurgical** (recommandé, ne touche à rien d'autre) :
```sql
DELETE FROM events WHERE session_id='e67f1166-d70c-42cd-a295-79513757233e';
DELETE FROM sessions WHERE id='e67f1166-d70c-42cd-a295-79513757233e';
```
**Restauration à l'état pré-réinsertion** :
```
systemctl stop openfox
cp /home/folzo/openfox/_recover/snap2/sessions.db{,-wal,-shm} /home/folzo/.local/share/openfox/
sha256sum -c /home/folzo/openfox/_recover/snap2.sha256
systemctl start openfox
```
`PRAGMA integrity_check` = `ok` avant et après réinsertion. Les fichiers d'origine n'ont jamais été modifiés en écriture directe : la réinsertion est passée par SQLite (WAL), et `snap1` reste la copie de l'état post-suppression.
## 5. Journal des actions récupérées
Le journal des 32 409 actions récupérées (appels d'outils + arguments intacts, résultats,
sorties) n'est **pas versionné ici** — il duplique les artefacts et concerne la base de
sessions OpenFox, pas le kit G9. Consultable / rejouable :
| Artefact | Contenu |
|---|---|
| `/home/folzo/openfox/_recover/phase4_actions.md` | journal brut complet (32 409 lignes, page-ordre) |
| `/home/folzo/openfox/_recover/phase4_actions_curated.md` | version filtrée/dédupliquée (758 lignes) |
| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON (+ page, offset) |
| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements réinsérés dans la base |
Reprise du travail Phase 4 : la tâche Kanban `8e3e7f073a6806cd` est toujours bindée à la
session `e67f1166…` et le runner n'a **jamais cessé d'écrire** (journal 09:18 → 10:13,
commits `a1864c6`, scripts 07c/07d) — c'est la source operative, pas le fil recoveré.
+10 -3
View File
@@ -29,7 +29,7 @@ xpenology → G9, phase par phase, quand le G9 sera branché (IP temporaire `.24
| G1 | Scripts idempotents, en-têtes documentant chaque modification. |
| G2 | `06-restic-backup.sh` : backup + **test de restauration** avant toute migration. |
| G3 | `06b-usb-media.sh`, `08-decommission.sh` : drapeau `--i-understand` + saisie du nom du disque / `WIPE XPENOLOGY`. |
| G4 | `07-migrate-stateful.sh` : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
| G4 | `07a`–`07d` (+ `07-migrate-stateful.sh` générique) : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
| G5 | README (ordre d'exécution) + `08-decommission.sh` : IP `.24`→`.25` uniquement au décommissionnement. |
| G6 | `01-hardening.sh` : ufw LAN-only (`192.168.1.0/24`), pas d'exposition WAN. |
| G7 | Chaque script : section **Rollback** en en-tête + colonne Rollback du journal. |
@@ -52,7 +52,14 @@ g9-migration/
│ ├── 05-beszel.sh ← T2.5 : agent Beszel
│ ├── 06-restic-backup.sh ← T3.1 : restic G9 → Freebox + test restauration
│ ├── 06b-usb-media.sh ← T6.1 : montage des disques USB média → /mnt/usb-media
│ ├── 07-migrate-stateful.sh← T4.x : export/import des stateful (xpenology → G9)
│ ├── 06c-postboot-check.sh ← contrôles post-reboot (lecture seule, 15+ checks)
│ ├── 07-migrate-stateful.sh← T4.x : générique (rsync) — INOPÉRANT sur le DSM : sshd
│ │ refuse `rsync --server` et scp → utiliser 07a–07d
│ ├── 07a-migrate-gitea.sh ← T4.4 : Gitea + backend Postgres (transport tar sur SSH)
│ ├── 07b-migrate-vaultwarden.sh ← T4.1 : Vaultwarden (coffre + 2FA + ADMIN_TOKEN)
│ ├── 07c-migrate-nextcloud.sh ← T4.2 : Nextcloud + MariaDB + Collabora
│ ├── 07d-migrate-photoprism.sh ← T4.3 : PhotoPrism (copie 530 Go + indexation)
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
│ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF]
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
│ ├── vaultwarden/ ← T4.1
@@ -74,7 +81,7 @@ g9-migration/
| 1 | (xpenology) exclure stateful de Watchtower + inventaire | G4 |
| 2 | `01-hardening.sh` → `02-data.sh` → `03-docker.sh` → `03b-secrets.sh` → `04-smb.sh` → `05-beszel.sh` | — |
| 3 | `06-restic-backup.sh` (backup + test restauration) | G2 |
| 4 | `07-migrate-stateful.sh` (par brique, double-run) — **après** `03b-secrets.sh` | G4 |
| 4 | **par brique, dans l'ordre** — **après** `03b-secrets.sh` : `07b-migrate-vaultwarden.sh` (T4.1) → `07c-migrate-nextcloud.sh` (T4.2) → `07d-migrate-photoprism.sh` (T4.3) → `07a-migrate-gitea.sh` (T4.4) → n8n (T4.5) → DNS/TLS (T4.6). `07-migrate-stateful.sh` = script **générique rsync**, inopérant sur le DSM (sshd refuse `rsync --server`/scp) ; transport réel des 07a–07d = **tar sur SSH** | G4 |
| 5 | restic 3-2-1 complet (G9 + PC IA → Freebox → 218play) | G2 |
| 6 | `06b-usb-media.sh` (T6.1) → Jellyfin (T6.2) → IP .24→.25 → `08-decommission.sh` | G3, G4, G5 |
| 7 | (G7) wipe DSM + Ubuntu neuf + boot test | G3, G10 |
+34 -16
View File
@@ -1,44 +1,62 @@
# n8n — T4.5 (automatisation + alertes → Matrix, G9)
# Le conteneur n8n est stateless (image) mais sa DB (Postgres) est stateful → exclu.
# Workflows exportables (JSON). Licence fair-code (notée, S3.1).
# n8n — T4.5 (automatisation, G9). Source .25 = n8n **2.42.5** sur Postgres 17
# (db `2025_n8n`, user `2025_n8n_user`, 4 workflows, 4 credentials, 1 user) → vérité terrain.
# Image **épinglée à 2.42.5** : pas de migration de schéma pendant le double-run (S11.4).
# Clé de chiffrement : .n8n/config importé depuis la source (copie du volume `data`) → les
# credentials existants restent déchiffrables. n8n ne connaît pas les *_FILE → env_file.
services:
n8n:
image: n8nio/n8n:1.90
image: docker.n8n.io/n8nio/n8n:2.42.5
container_name: n8n
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true" # image stateless → auto
- "com.centurylinklabs.watchtower.enable=false" # version épinglée (schéma DB) → exclu
env_file:
- ./secrets/n8n.env
environment:
- TZ=Europe/Paris
- N8N_HOST=n8n.folzocorp.fr
- N8N_PROTOCOL=https
- N8N_PORT=5678
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n_db
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/dbpass
- WEBHOOK_URL=https://n8n.folzocorp.fr
- N8N_RELEASE_TYPE=stable
- N8N_RUNNERS_ENABLED=true
- NODE_ENV=production
- GENERIC_TIMEZONE=Europe/Paris
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=2025_n8n
- DB_POSTGRESDB_USER=2025_n8n_user
# Double-run LAN : N8N_SECURE_COOKIE=false permet l'accès http://192.168.1.24:5678
# (cookies « secure » refusés en http). **À remettre à true à la bascule TLS (T4.6).**
- N8N_SECURE_COOKIE=false
volumes:
- ./data:/home/node/.n8n
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
# Bind 0.0.0.0 : LAN uniquement (ufw de 01-hardening.sh restreint le port).
ports:
- "0.0.0.0:5678:5678"
depends_on:
- n8n_db
db:
condition: service_healthy
n8n_db:
db:
image: postgres:17
container_name: n8n_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_DB=2025_n8n
- POSTGRES_USER=2025_n8n_user
- POSTGRES_PASSWORD_FILE=/run/secrets/dbpass
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
volumes:
- ./db:/var/lib/postgresql/data
- ./secrets:/run/secrets:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U 2025_n8n_user -d 2025_n8n"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
@@ -1,43 +1,59 @@
# Nextcloud — T4.2 (socle famille, G9)
# Stateful (app + MariaDB + Redis) → exclus de Watchtower (S11.4).
# Stateful (app + MariaDB + Redis + données) → exclus de Watchtower (S11.4).
# MAJ manuelle supervisée (S11.5) : backup DB → maintenance ON → pull/up → occ upgrade → vérif → OFF.
# L'incident Nextcloud (06/10) est la raison de ce pinning + exclusion.
#
# CONTRAINTES MESURÉES sur la source .25 (2026-10-11) :
# - config.php : dbtype=mysql, dbhost=**db**, dbname=nextcloud, dbuser=nextcloud,
# datadirectory=/var/www/html/data, version 35.0.1.1, instanceid oc9yed0p9k7q.
# → le service DB du compose G9 DOIT s'appeler `db` et porter les **identifiants
# d'origine** (sinon config.php importé ne correspond plus à la base importée).
# - apps déjà installés sur la source : calendar 6.6.2, contacts 8.9.1, mail 5.12.3
# (Groupware), deck 1.19.0, notes 6.1.0, richdocuments 12.0.1, collectives, forms,
# spreed, whiteboard… → import = conservation à l'identique, rien à installer.
# - pas de chiffrement des fichiers activé (aucune config encryption) → copie simple.
# - éditeur : Collabora (décision Phase 4) → service `collabora` + richdocuments.wopi_url.
services:
nextcloud:
image: nextcloud:35
image: nextcloud:35.0.1 # = version source exacte (35.0.1.1) → aucune migration de schéma
container_name: nextcloud
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
environment:
- TZ=Europe/Paris
- POSTGRES_HOST=nextcloud_db
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD_FILE=/run/secrets/dbpass
- NEXTCLOUD_TRUSTED_PROXIES=172.16.0.0/12
- PHP_MEMORY_LIMIT=1G
- PHP_UPLOAD_LIMIT=10G
- PHP_POST_SIZE_LIMIT=10G
volumes:
- ./data:/var/www/html
- ./config:/var/www/html/config
- ./secrets:/run/secrets:ro
depends_on:
- nextcloud_db
- redis
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
# Bind 0.0.0.0 : accessible depuis le LAN (edge/Caddy le proxy en T4.6).
ports:
- "0.0.0.0:8080:80"
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost/status.php"]
interval: 60s
timeout: 10s
retries: 5
nextcloud_db:
db:
image: mariadb:10.11
container_name: nextcloud_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
command:
- --transaction-isolation=READ-COMMITTED
- --binlog-format=ROW
environment:
- MARIADB_DATABASE=nextcloud
- MARIADB_USER=nextcloud
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass # = mot de passe D'ORIGINE (import)
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
volumes:
- ./db:/var/lib/mysql
@@ -48,6 +64,45 @@ services:
container_name: nextcloud_redis
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # cache persistant → exclu
- "com.centurylinklabs.watchtower.enable=false"
volumes:
- ./redis:/data
# Collabora CODE — éditeur de documents (décision Phase 4, remplace OnlyOffice .25).
# Image épinglée 26.04.5.1.1 → Watchtower OFF (convention S11.4 : épinglée = exclu,
# cf. label n8n). MAJ manuelle supervisée avec backup préalable.
collabora:
image: collabora/code:26.04.5.1.1
container_name: collabora
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
# L'image officielle ne substitue que `username`/`password` dans coolwsd.xml :
# `password_file` n'est PAS lu → console 9980 restée sans mot de passe. Mot de passe
# injecté par env_file (pattern n8n) ; secrets/collabora.env généré par 03b-secrets.sh.
env_file:
- ./secrets/collabora.env
# Entrypoint = coolwsd (arguments fixes). CMD est **appendé** à l'entrypoint →
# `--o:ssl.enable=false` désactive le TLS interne (mesuré 2026-10-11 : sans cela,
# discovery http = 000, https -k = 200 ; env O:ssl.enable NON appliqué).
command:
- --o:ssl.enable=false
environment:
- TZ=Europe/Paris
# Hôtes émetteurs de WOPI autorisés (double-run : LAN G9 + hostname de bascule T4.6).
- aliasgroup1=https://nextcloud.folzocorp.fr,http://192.168.1.24:8080,http://192.168.1.25:8080
- username=admin
- JWT_ENABLED=false
# Image 26.04 : Entrypoint = coolwsd --use-env-vars → overrides coolwsd.xml via
# variables `O:<chemin>` (mesuré : extra_features/EXTRA_FEATURES non lus, TLS par
# défaut). SSL interne désactivé : WOPI en http LAN ; Caddy (T4.6) terminera le TLS.
- O:ssl.enable=false
volumes:
- ./secrets:/run/secrets:ro
ports:
- "0.0.0.0:9980:9980"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:9980/hosting/discovery"]
interval: 60s
timeout: 10s
retries: 5
@@ -1,6 +1,8 @@
# PhotoPrism — T4.3 (photos, G9)
# Lecture IN-PLACE des originaux (S5) : pointe sur /data/famille/photos (pas de déplacement).
# Stateful (base + index) → exclu de Watchtower (S11.4). Image PINNÉE (S11.4, pas :latest).
# PhotoPrism — T4.3 (G9)
# Décision #3 (C-T4-DECISIONS) : les photos sont **copiées** sur le G9 (/data/docker/photoprism/originals),
# plus de lecture in-place sur le NAS. Image épinglée (S11.4), stateful (base + index) → Watchtower OFF.
# PhotoPrism ne connaît PAS les variables « *_FILE » : les secrets sont fournis par env_file
# (secrets/photoprism.env, 600 root, hors git) — MariaDB, lui, accepte *_FILE.
services:
photoprism:
image: photoprism/photoprism:260919
@@ -8,34 +10,51 @@ services:
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
env_file:
- ./secrets/photoprism.env
environment:
- TZ=Europe/Paris
- PHOTOPRISM_URL=https://photos.folzocorp.fr
- PHOTOPRISM_SITE_URL=https://photos.folzocorp.fr
- PHOTOPRISM_ADMIN_USER=admin
- PHOTOPRISM_ADMIN_PASSWORD_FILE=/run/secrets/admin
- PHOTOPRISM_LIBRARY_PATH=/originals:/import
- PHOTOPRISM_SITE_URL=http://192.168.1.24:2342/
- PHOTOPRISM_SITE_TITLE=Photos Folzo
- PHOTOPRISM_DATABASE_DRIVER=mysql
- PHOTOPRISM_DATABASE_SERVER=db:3306
- PHOTOPRISM_DATABASE_NAME=photoprism
- PHOTOPRISM_DATABASE_USER=photoprism
- PHOTOPRISM_DATABASE_PASSWORD_FILE=/run/secrets/dbpass
- PHOTOPRISM_LOCAL_NETWORKS=172.16.0.0/12,192.168.0.0/16
- PHOTOPRISM_LOCAL_NETWORKS=192.168.0.0/16
- PHOTOPRISM_HTTP_COMPRESSION=gzip
- PHOTOPRISM_LOG_LEVEL=info
- PHOTOPRISM_PUBLIC=false
- PHOTOPRISM_READONLY=false
- PHOTOPRISM_EXPERIMENTAL=false
- PHOTOPRISM_ORIGINALS_LIMIT=5000
- PHOTOPRISM_AUTO_INDEX=300
- PHOTOPRISM_AUTO_IMPORT=-1
- PHOTOPRISM_SIDECAR_ENABLED=true
volumes:
- /data/famille/photos:/originals:ro # in-place (lecture seule)
- ./storage:/storage
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
- /data/docker/photoprism/originals:/photoprism/originals
- /data/docker/photoprism/storage:/photoprism/storage
- /data/docker/photoprism/import:/photoprism/import
# Bind 0.0.0.0 + ufw (01-hardening.sh) restreint le port au LAN → pas d'exposition WAN.
ports:
- "0.0.0.0:2342:2342"
depends_on:
- photoprism_db
db:
condition: service_healthy
photoprism_db:
db:
image: mariadb:10.11
container_name: photoprism_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
command: >
--innodb-buffer-pool-size=256M
--transaction-isolation=READ-COMMITTED
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max-connections=512
--innodb-rollback-on-timeout=OFF
--innodb-lock-wait-timeout=120
environment:
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
- MARIADB_DATABASE=photoprism
@@ -44,3 +63,9 @@ services:
volumes:
- ./db:/var/lib/mysql
- ./secrets:/run/secrets:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
+16
View File
@@ -82,3 +82,19 @@
| 2026-10-11 07:56 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:57 | 4 | migrate:vaultwarden : coffre importé + service lancé | T4.1 (double-run G4) | Source stoppée → tar/ssh → **source relancée healthy** ; 621 fichiers sha256 identiques (source == stage == G9) ; G9 = 953 ciphers / 1 user (identiques à .25, revérifié après coup : .25 healthy 953) ; migrations diesel OK (v.20260902120000) ; alive 200, WebVault 200 ; DOMAIN conservé = `pass.folzocorp.fr` (clients existants intacts) ; image épinglée 1.37.4 (génération source, image .25 pullée 2026-10-05) | docker compose -f /data/docker/vaultwarden/docker-compose.yml down + rm -rf /data/docker/vaultwarden ; .25 intact |
| 2026-10-11 08:01 | 4 | migrate:vaultwarden : 2FA + ADMIN_TOKEN + emergency access | T4.1 (critères 2FA + emergency access) | **2FA TOTP déjà actif côté source et importé avec le coffre** : twofactor(atype=0, enabled=1, last_used=59703916) identique sur le G9 → l'appli d'authentification de l'utilisateur continue sans re-scannage ; ADMIN_TOKEN **haché argon2id** (m=64MiB,t=3,p=4 — preset bitwarden, python3-argon2) : avis « plain text ADMIN_TOKEN » disparu, login admin 200 (dashboard Users/Invitations/Organizations/**Emergency** accessibles) ; token clair dans `/data/docker/vaultwarden/secrets/admin-plain` (600 root, à consigner dans le coffre puis supprimer) ; `EMERGENCY_ACCESS_ALLOWED=true` + `INVITATIONS_ALLOWED=true` posés | restaurer secrets/admin (token clair) + restart ; retirer les 2 env |
| 2026-10-11 08:13 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:16 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:21 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:28 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:35 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:40 | 4 | audit .25 : « disparition » de /volume1/docker-REC = VUE de partage DSM, pas une perte | Inventaire d'abord pris sur /volume1/docker-REC (60 entrées), puis 10 entrées une heure plus tard, propriétaire de « . » passé de Master à root, mtime /volume1 = 03:01 | `docker inspect` = vérité terrain : nextcloud_app → **/volume2/docker-REC/NextCloud/{data,config}** (+ volume nommé /volume1/@docker/volumes/f5f0…/_data), vaultwarden-server-1 → **/volume1/docker/vaultwarden**, gitea → /volume1/docker/Gitea/data. /volume1/docker-REC est une vue multi-volumes du partage « docker-REC », remontée à 03:01. **Aucune donnée perdue** : /volume2/docker-REC/NextCloud/data = 1.9 Go / 2608 fichiers ; db.sqlite3 de vaultwarden sha256 `2cad5eea42ef4b04…` **identique** entre .25 et le G9 | lecture seule, aucune action ; chemins canoniques consignés dans 07c (XP_DATA/XP_CONF = /volume2/…) |
| 2026-10-11 08:42 | 4 | kit : lancer les scripts de migration en **folzo**, jamais en `sudo` global | `sudo` remplace $HOME par /root → « Identity file /root/.ssh/id_ed25519_openfox11 not accessible » puis « Host key verification failed » (root n'a ni clé ni known_hosts) ; 3 runs de 07c échoués en préflight | 07c réécrit : appels « G9 » en **local** (`sh -c` + sudo local, la machine de pilotage EST le G9), clé en chemin absolu ; import du dump via **MARIADB_USER/MARIADB_PASSWORD** (env du conteneur MariaDB G9, pas MYSQL_*) | relancer en root avec SSH_KEY=<chemin> explicite si besoin |
| 2026-10-11 09:03 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 09:10 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 09:18 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 10:10 | 4 | clarification 08:42 : le 07c **déposé** est la variante SSH, pas « local » | La ligne 08:42 décrit une réécriture « appels G9 en local » jamais commitée ; le `07c-migrate-nextcloud.sh` du dépôt passe TOUTES les ops G9 par SSH (folzo@.24, clé sur le pilot) — cohérent avec son en-tête | 07c déposé = variante SSH : préflight SSH .25 + .24, import rsync/ssh, restore via MARIADB_USER + secrets/dbpass. La formulation « local » de 08:42 ne décrit PAS le code du dépôt. + garde-fou idempotence backup (07c/07d) + nettoyage dumps (07c) + n8n.env builder (03b) + mount secrets collabora + label watchtower collabora=false | aucune — ligne documentaire |
| 2026-10-11 10:46 | 4 | récupération du thread Phase 4 supprimé (session `e67f1166…`) | suppression accidentelle de la session 07:20 UTC (cascade `events`) ; le runner a continué à travailler (journal 09:18→10:13) | thread réinséré : **130 messages / 64 657 événements / 33,85 Mo** ; caravage des pages SQLite libérées = **51 420 payloads JSON (34,68 Mo)** + 79 830 entrées d'index ; **~35 % du volume**, prose en fragments désordonnés, messages utilisateur quasi tous perdus ; intégrité `ok` avant/après ; rapport complet `g9-migration/PHASE4-RECOLLEMENT.md` | chirurgical : `DELETE FROM events WHERE session_id='e67f1166…'; DELETE FROM sessions WHERE id='e67f1166…'` — ou restauration `snap2` (voir §4 du rapport) |
| 2026-10-11 11:05 | 4 | corrections post-revue code (5 findings) | Revue de code du diff Phase 4 | 1 **sécurité** : `collabora` ne lisait PAS `password_file` (non supporté par collabora/code) → console 9980 sans mot de passe ; remplacé par `env_file: ./secrets/collabora.env` (`password=…`) généré par 03b (pattern n8n) ; 2 **import 07c** : DEFINER retirés du dump + import en **root** (rootpass) + **rc contrôlé** (l'ancien `\| tail -5` masquait ERROR 1227) ; 3 07c : `SRC_USERS` avec `\|\| true` (préflight tué par grep rc=1) ; 4 07d : progression `tar --checkpoint=50000` sur la copie 530 Go ; 5 README : arborescence + ligne phase 4 = 07a–07d (07-migrate-stateful = générique rsync, inopérant sur DSM) ; 6 `PHASE4-RECOLLEMENT.md` dégraissé (821→75 l., dump §5 hors dépôt). Vérif : `bash -n` 3/3 OK, YAML compose OK (env_file présent, password_file absent), sed DEFINER testé | `git checkout` des fichiers modifiés (compose nextcloud, 03b, 07c, 07d, README) |
| 2026-10-11 12:44 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 10:45 | 4 | migrate:nextcloud : migration complète (07c) | T4.2 (double-run G4) | export app stoppée (db vivante, mysqldump --single-transaction 31,9 Mo) + tars data (1,9 Go, 2608 fichiers sha256 identiques) + config + custom_apps → **app .25 relancée healthy** ; import G9 : MariaDB 10.11 (utf8mb4_bin, DEFINER retirés, import root, filecache=2727, users=8), occ status installed/maintenance off, version 35.0.1.1 = image épinglée, occ upgrade « No upgrade required », occ app:list **identique à la source (66 apps)** ; overrides LAN dans config/g9-double-run.config.php (réversible) ; dumps supprimés sur les 3 machines (hashs mots de passe users) | docker compose -f /data/docker/nextcloud/docker-compose.yml down + rm -rf /data/docker/nextcloud ; .25 intact |
| 2026-10-11 11:02 | 4 | migrate:nextcloud : Collabora — TLS interne désactivé | L'image collabora/code:26.04 sert **TLS par défaut** : discovery http = 000, https -k = 200 ; env `extra_features`/`EXTRA_FEATURES` et `O:ssl.enable` **non appliqués** (mesuré : entrypoint = coolwsd --use-env-vars sans script) | fix = **command: --o:ssl.enable=false** (CMD appendu à l'entrypoint) → logs « SSL is disabled », discovery http **200** ; wopi_url = http://192.168.1.24:9980 (occ, persisté) ; Caddy (T4.6) terminera le TLS | retirer la ligne command: du compose + restart |
+29 -3
View File
@@ -11,10 +11,12 @@
#
# Génère (chmod 644 root:root — voir la note du code : les entrypoints conteneurs ne sont
# pas root, 600 les rendrait illisibles ; le dossier reste hors git, G9 respecté) pour chaque brique :
# nextcloud : dbpass, rootpass
# nextcloud : dbpass, rootpass, collabora_admin + collabora.env (construit depuis
# collabora_admin — requis par env_file du compose : collabora/code ne lit
# PAS password_file, seul `password` est substitué dans coolwsd.xml)
# photoprism : admin, dbpass, rootpass
# vaultwarden : admin
# n8n : dbpass
# n8n : dbpass + n8n.env (construit depuis dbpass — requis par env_file du compose)
# gitea : dbpass (backend Postgres — voir compose/gitea)
# (jellyfin, watchtower : pas de secret fichier)
#
@@ -58,7 +60,7 @@ gen_secret() {
# briques : "brique:s1 s2 s3;..."
declare -A SECRET_MAP=(
[nextcloud]="dbpass rootpass"
[nextcloud]="dbpass rootpass collabora_admin"
[photoprism]="admin dbpass rootpass"
[vaultwarden]="admin"
[n8n]="dbpass"
@@ -74,6 +76,30 @@ for brique in nextcloud photoprism vaultwarden n8n gitea; do
for s in ${SECRET_MAP[$brique]}; do
gen_secret "$SECRETS_DIR/$s"
done
if [[ "$brique" == "n8n" ]]; then
ENVF="$SECRETS_DIR/n8n.env"
if [[ -f "$ENVF" ]]; then
log " ⏭ existe déjà : $ENVF (non modifié)"
else
umask 077
{ echo "DB_POSTGRESDB_PASSWORD=$(cat "$SECRETS_DIR/dbpass")"; } > "$ENVF"
chmod 644 "$ENVF"
log " ✅ généré : $ENVF (env_file requis par compose/n8n)"
fi
fi
if [[ "$brique" == "nextcloud" ]]; then
# collabora/code ne lit PAS password_file : seul `password` est substitué dans
# coolwsd.xml → console admin 9980 protégée par env_file (pattern n8n).
ENVF="$SECRETS_DIR/collabora.env"
if [[ -f "$ENVF" ]]; then
log " ⏭ existe déjà : $ENVF (non modifié)"
else
umask 077
{ echo "password=$(cat "$SECRETS_DIR/collabora_admin")"; } > "$ENVF"
chmod 644 "$ENVF"
log " ✅ généré : $ENVF (env_file requis par compose/nextcloud → collabora)"
fi
fi
done
journal "Secrets bootstrap OK" "Briques stateful nécessitent des fichiers secrets" "nextcloud, photoprism, vaultwarden, n8n (chmod 644 root:root, dossier hors git)" "suppression des fichiers secrets générés"
@@ -20,6 +20,15 @@
# - Sur le G9 : /data monté, Docker opérationnel.
# - Secrets bootstrap (03b-secrets.sh) déjà exécuté.
#
# ⚠️ TRANSPORT (mesuré 2026-10-11, DSM 7) : le sshd du xpenology refuse la
# commande rsync --server (« Permission denied, please try again. ») et scp
# n'est pas servi (« subsystem request failed »). Le pull rsync/scp depuis .25
# est donc IMPOSSIBLE. Transports qui fonctionnent : « ssh sudo tar cz » et
# « ssh sudo cat ». Les briques sont migrées par les scripts dédiés
# (07a-migrate-gitea.sh, 07b-migrate-vaultwarden.sh, 07c-migrate-nextcloud.sh)
# qui utilisent tar+ssh ; ce script générique est gardé pour les hôtes qui
# exposent rsync.
#
# ⚠️ COHÉRENCE DES BASES (G4) : le rsync d'une base VIVANTE (MariaDB/Postgres en
# service sur .25) produit une copie incohérente et un manifeste sha256 qui
# change pendant le transfert. Le script STOPPE donc les conteneurs source
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env bash
#
# 07c-migrate-nextcloud.sh — T4.2 : migration Nextcloud (xpenology .25 → G9 .24)
#
# Garde-fous : G4 (double-run : source stoppée pendant l'export puis **relancée**, rien supprimé),
# G2 (backup restic avant import), G7 (rollback), G8 (journal), G9 (aucun secret loggué/commité).
#
# SOURCE mesurée (2026-10-11) — 3 mounts du conteneur nextcloud_app :
# /volume2/docker-REC/NextCloud/data → /var/www/html/data (datadirectory, 1.9 Go)
# /volume1/@docker/volumes/<hash>/_data → /var/www/html (VOLUME NOMMÉ = code app +
# custom_apps : calendar, contacts, mail, deck, notes,
# richdocuments, collectives, forms, spreed, whiteboard…)
# /volume2/docker-REC/NextCloud/config → /var/www/html/config (config.php, redis, smtp…)
# DB : conteneur nextcloud_db (mariadb 10.6), base nextcloud / user nextcloud → mysqldump.
#
# POINTS CLÉS (C-T4.2) :
# - config.php importé impose dbhost=`db`, dbname/dbuser=nextcloud → le compose G9 nomme son
# service DB `db` et reçoit le **mot de passe d'origine** (lu depuis l'env du conteneur
# source, écrit en 644 dans secrets/dbpass, JAMAIS affiché).
# - image épinglée nextcloud:35.0.1 = version source exacte → pas de migration de schéma.
# - custom_apps copiés depuis le volume nommé : sans eux la base déclarerait des apps absents.
#
# Usage : sur la **machine de pilotage** (celle qui détient la clé .25/.24, ici /home/effi),
# ./07c-migrate-nextcloud.sh [SSH_KEY=<chemin> pour forcer la clé]
# Les ops G9 passent par SSH (folzo@.24, sudo pour la partie root) : la clé vit sur le pilot,
# PAS sur le G9. Ne PAS lancer via `sudo` global : $HOME devient /root → clé et known_hosts
# introuvables → « Host key verification failed » (mesuré 2026-10-11, 3 runs échoués).
# Rollback : G9 `docker compose -f /data/docker/nextcloud/docker-compose.yml down` +
# `rm -rf /data/docker/nextcloud` ; .25 intact (services relancés).
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker"
# On stoppe **l'app** (writes quiescents) mais PAS la DB : mysqldump a besoin du serveur
# tournant, et --single-transaction fournit l'instantané cohérent (G4 : relancée après).
XP_CONTAINERS="nextcloud_app"
XP_DATA="/volume2/docker-REC/NextCloud/data"
XP_CONF="/volume2/docker-REC/NextCloud/config"
XP_VOL_APP="/volume1/@docker/volumes/f5f099b69018be044f00d3d68f1e0da76cecb42bcb3b621b43d31091e33e789d/_data"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
BRIQUE="nextcloud"
G9_DIR="/data/docker/$BRIQUE"
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
KIT_ON_G9="/home/folzo/g9-migration"
JOURNAL="$KIT_DIR/journal.md"
STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}"
SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST"
RSH_G9="ssh -i $KEY"
log() { echo -e "\n[nextcloud] $*"; }
journal() {
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \
"$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL"
}
manifest_in() { ( cd "$1" && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort ); }
verify_pair() { # $1 src .25 (sudo find), $2 dir local
local r l rc=0; r=$(mktemp); l=$(mktemp)
$SSH_XP "sudo sh -c \"cd '$1' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort\"" > "$r"
manifest_in "$2" > "$l"
diff -u "$r" "$l" > "$r.diff" || rc=1
if [[ $rc -ne 0 ]]; then log " ❌ intégrité $2 :"; head -20 "$r.diff"; rm -f "$r" "$l" "$r.diff"; exit 1; fi
log " ✅ $(wc -l < "$r") fichiers identiques ($(basename "$2"))"
rm -f "$r" "$l" "$r.diff"
}
# ---------------------------------------------------------------- 0. préflight
log "=== 0/6 PRÉFLIGHT ==="
$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; }
$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; }
$SSH_G9 "test -d $KIT_ON_G9/compose" || { echo "ERREUR : kit absent sur le G9"; exit 1; }
for d in "$XP_DATA" "$XP_CONF" "$XP_VOL_APP/custom_apps"; do
$SSH_XP "sudo test -d '$d'" || { echo "ERREUR : source absente sur .25 : $d"; exit 1; }
done
mkdir -p "$STAGE"
SRC_USERS=$($SSH_XP "sudo $XP_DOCKER exec nextcloud_app php occ user:list 2>/dev/null | grep -c '^\s*-' || true")
log "source .25 : users occ = $SRC_USERS"
# ---------------------------------------------------------------- 1. backup G2
log "=== 1/6 BACKUP restic (G2) ==="
if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then
log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)"
else
$SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2"
journal "backup restic pré-migration" "Garde-fou G2 avant import Nextcloud" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
fi
# ---------------------------------------------------------------- 2. export (stop → export → start)
log "=== 2/6 EXPORT : stop app → mysqldump (db vivante, --single-transaction) + tars → relance app (G4) ==="
STOPPED=()
for c in $XP_CONTAINERS; do
if $SSH_XP "sudo $XP_DOCKER ps -q -f name=^${c}\$" | grep -q .; then
$SSH_XP "sudo $XP_DOCKER stop $c >/dev/null" && STOPPED+=("$c") && log " ⏸ source stoppée : $c"
fi
done
restart_source() {
for c in "${STOPPED[@]:-}"; do
[[ -n "$c" ]] || continue
$SSH_XP "sudo $XP_DOCKER start $c >/dev/null" && log " ▶ source redémarrée : $c" \
|| log " ⚠️ ÉCHEC redémarrage $c — INTERVENIR SUR .25"
done
}
trap restart_source EXIT
DUMP=/tmp/nc_dump.sql
$SSH_XP "sudo $XP_DOCKER exec nextcloud_db sh -c 'mysqldump -u\"\$MYSQL_USER\" -p\"\$MYSQL_PASSWORD\" --single-transaction --routines --triggers --events --default-character-set=utf8mb4 \"\$MYSQL_DATABASE\"' > $DUMP && sudo chmod 644 $DUMP"
log " dump : $($SSH_XP "stat -c%s $DUMP") o, sha256 $($SSH_XP "sha256sum $DUMP | cut -c1-16")…"
# mot de passe DB d'origine : lu depuis l'env du conteneur, écrit en 600 local, jamais affiché
$SSH_XP "sudo $XP_DOCKER inspect nextcloud_db --format '{{range .Config.Env}}{{println .}}{{end}}'" \
| sed -n 's/^MYSQL_PASSWORD=//p' > "$STAGE/dbpass"; chmod 600 "$STAGE/dbpass"
[[ -s "$STAGE/dbpass" ]] || { echo "ERREUR : mot de passe DB source non récupéré"; exit 1; }
$SSH_XP "sudo tar cz -C '$XP_DATA' ." > "$STAGE/data.tgz"
$SSH_XP "sudo tar cz -C '$XP_CONF' ." > "$STAGE/config.tgz"
$SSH_XP "sudo tar cz -C '$XP_VOL_APP' custom_apps" > "$STAGE/custom_apps.tgz"
log " archives : data=$(du -h "$STAGE/data.tgz"|cut -f1) config=$(du -h "$STAGE/config.tgz"|cut -f1) custom_apps=$(du -h "$STAGE/custom_apps.tgz"|cut -f1)"
mkdir -p "$STAGE/x_data" "$STAGE/x_config" "$STAGE/x_custom_apps"
tar xzf "$STAGE/data.tgz" -C "$STAGE/x_data"
tar xzf "$STAGE/config.tgz" -C "$STAGE/x_config"
tar xzf "$STAGE/custom_apps.tgz" -C "$STAGE/x_custom_apps"
verify_pair "$XP_DATA" "$STAGE/x_data"
verify_pair "$XP_CONF" "$STAGE/x_config"
verify_pair "$XP_VOL_APP/custom_apps" "$STAGE/x_custom_apps/custom_apps"
rm -f "$STAGE"/*.tgz
restart_source; STOPPED=()
# ---------------------------------------------------------------- 3. import G9
log "=== 3/6 IMPORT sur le G9 ==="
$SSH_G9 "sudo mkdir -p $G9_DIR/data/data && sudo chown -R $G9_USER:$G9_USER $G9_DIR"
# MariaDB tourne en uid 999 : le datadir doit lui appartenir (sinon « Permission denied » sur
# db.opt, mesuré 2026-10-11). Le chown -R folzo ci-dessus est nécessaire au rsync, pas à la DB.
$SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_data/" "$G9_USER@$G9_HOST:$G9_DIR/data/data/"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_config/" "$G9_USER@$G9_HOST:$G9_DIR/config/"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_custom_apps/" "$G9_USER@$G9_HOST:$G9_DIR/data/"
g=$(mktemp); l2=$(mktemp); manifest_in "$STAGE/x_data" > "$l2"
$SSH_G9 "cd '$G9_DIR/data/data' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$g"
diff -u "$l2" "$g" > "$g.diff" || { log "❌ intégrité data sur le G9 :"; head -20 "$g.diff"; exit 1; }
log " ✅ data G9 : $(wc -l < "$g") fichiers identiques"
rm -f "$l2" "$g" "$g.diff"
# ---------------------------------------------------------------- 4. compose + secrets + restore DB
log "=== 4/6 COMPOSE + SECRETS + RESTORE DB ==="
rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/"
rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/"
$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null"
$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets"
# dbpass = mot de passe D'ORIGINE (config.php importé), rootpass = généré par 03b-secrets.
# secrets/ est root:root sur le G9 → passage par /tmp puis install en sudo (jamais affiché).
rsync -a -e "$RSH_G9" "$STAGE/dbpass" "$G9_USER@$G9_HOST:/tmp/.nc-dbpass"
$SSH_G9 "sudo install -m 644 -o root -g root /tmp/.nc-dbpass $G9_DIR/secrets/dbpass && rm -f /tmp/.nc-dbpass && sudo chmod 644 $G9_DIR/secrets/rootpass $G9_DIR/secrets/collabora_admin 2>/dev/null || true"
rm -f "$STAGE/dbpass"; rm -rf "$STAGE"
$SSH_G9 "cd $G9_DIR && docker compose up -d db redis && sleep 25 && docker compose ps --format '{{.Name}} {{.Status}}'"
log " restore du dump (base vierge, schéma oc_ + utf8mb4)"
$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" -e \"ALTER DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;\"' < /dev/null"
$SSH_XP "sudo cat $DUMP" > /tmp/nc_dump_local.sql
# DEFINER=`nextcloud`@`…` (routines/triggers/events, issus de --routines --triggers --events)
# importés en utilisateur non-root → « ERROR 1227 … you need SUPER » sous MariaDB 10.11.
# → DEFINER retirés localement, import en **root** (rootpass, seedé par 03b), rc contrôlé
# (l'ancien `2>&1 | tail -5` masquait l'échec et le script poursuivait).
DEF_COUNT=$(grep -c 'DEFINER=' /tmp/nc_dump_local.sql || true)
sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' /tmp/nc_dump_local.sql > /tmp/nc_dump_stripped.sql
log " DEFINER retirés du dump : $DEF_COUNT"
rsync -a --info=stats1 -e "$RSH_G9" /tmp/nc_dump_stripped.sql "$G9_USER@$G9_HOST:$G9_DIR/nc_dump.sql"
IMPORT_LOG=/tmp/nc_import.log
if ! $SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u root -p\"\$(cat /run/secrets/rootpass)\" nextcloud' < $G9_DIR/nc_dump.sql" > "$IMPORT_LOG" 2>&1; then
log "❌ import du dump en échec (rc≠0) — 20 dernières lignes de la sortie mysql :"
tail -20 "$IMPORT_LOG"
exit 1
fi
if grep -qiE '^ERROR [0-9]+' "$IMPORT_LOG"; then
log "❌ erreurs MySQL détectées pendant l'import :"
grep -iE '^ERROR [0-9]+' "$IMPORT_LOG" | head -10
exit 1
fi
log " import OK en root (base vierge, DEFINER retirés) — $(tail -1 "$IMPORT_LOG")"
rm -f /tmp/nc_dump_local.sql /tmp/nc_dump_stripped.sql "$IMPORT_LOG"
$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" nextcloud -e \"select count(*) as filecache from oc_filecache; select count(*) as users from oc_users;\"' < /dev/null"
# nettoyage des dumps (hashs des mots de passe users) — G9 : rien ne traîne (les 3 copies)
$SSH_XP "sudo rm -f $DUMP"
rm -f /tmp/nc_dump_local.sql
$SSH_G9 "rm -f $G9_DIR/nc_dump.sql"
log " dumps nettoyés (.25, pilot, G9)"
# override LAN du double-run (documenté, réversible) : pas de réécriture https, LAN en confiance
$SSH_G9 "cat > $G9_DIR/config/g9-double-run.config.php" <<'PHP'
<?php
# T4.2 double-run : overrides G9 (réversibles — supprimer ce fichier à la bascule T4.6).
$CONFIG = array (
'trusted_domains' => array (0 => 'nextcloud.folzocorp.fr', 1 => '192.168.1.24', 2 => '192.168.1.24:8080'),
'overwritehost' => '',
'overwriteprotocol' => '',
'overwrite.cli.url' => 'http://192.168.1.24:8080',
'trusted_proxies' => array (0 => '192.168.1.0/24'),
);
PHP
$SSH_G9 "sudo chown -R 33:33 $G9_DIR/data $G9_DIR/config && sudo chmod -R u+rwX $G9_DIR/data $G9_DIR/config"
$SSH_G9 "cd $G9_DIR && docker compose up -d nextcloud && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'"
log " occ status / upgrade"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ status' www-data < /dev/null 2>&1 | head -8"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ upgrade' www-data < /dev/null 2>&1 | tail -6"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ maintenance:repair --include-expensive' www-data < /dev/null 2>&1 | tail -4"
# ---------------------------------------------------------------- 5. Collabora
log "=== 5/6 COLLABORA ==="
$SSH_G9 "cd $G9_DIR && docker compose up -d collabora && sleep 25 && curl -sf -o /dev/null -w 'collabora discovery HTTP %{http_code}\n' http://127.0.0.1:9980/hosting/discovery"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ config:app:set richdocuments wopi_url --value=http://192.168.1.24:9980 && php occ config:app:get richdocuments wopi_url' www-data < /dev/null 2>&1 | tail -2"
# ---------------------------------------------------------------- 6. vérification
log "=== 6/6 VÉRIFICATION ==="
$SSH_G9 "curl -sf -o /dev/null -w 'nextcloud status.php HTTP %{http_code}\n' http://127.0.0.1:8080/status.php"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ user:list' www-data < /dev/null 2>&1 | grep -c '^\s*-'"
$SSH_XP "sudo $XP_DOCKER ps --format '{{.Names}} {{.Status}}' | grep -iE 'nextcloud'"
journal "Nextcloud importé + lancé sur le G9 (+ Collabora)" "T4.2 (double-run G4)" "data 1.9 Go + config + custom_apps + mysqldump sha256 identiques ; occ status/upgrade OK ; Collabora discovery OK ; wopi_url → G9 ; source relancée" "docker compose -f $G9_DIR/docker-compose.yml down + rm -rf $G9_DIR ; .25 intact"
log "Terminé."
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# 07d-migrate-photoprism.sh — T4.3 : PhotoPrism sur le G9, photos COPIÉES (décision #3).
#
# Vérité terrain mesurée 2026-10-11 : il n'y a **aucun conteneur ni image PhotoPrism sur .25**
# (`docker ps -a` : rien, `docker images` : rien). PhotoPrism n'est donc pas « migré » :
# il est **installé** sur le G9 à partir des photos du NAS (/volume1/photo = 530 Go,
# 305 059 fichiers media, 2002→2026), avec un index neuf (décision #4 : indexation progressive).
#
# Transport : le sshd de DSM refuse rsync --server et scp (voir journal 2026-10-11) →
# flux tar sur SSH, relayé par la machine de pilotage : .25 → pilot → .24.
#
# Intégrité : manifeste **métadonnées** (chemin + taille, LC_ALL=C) source vs G9, totaux
# (nb fichiers, octets), plus échantillon sha256 de 200 fichiers.
#
# Usage : sur la machine de pilotage (détenant la clé), ./07d-migrate-photoprism.sh
# Étape longue (530 Go). `SKIP_COPY=1` pour ne redéployer que la stack.
# Rollback : `docker compose -f /data/docker/photoprism/docker-compose.yml down` +
# `rm -rf /data/docker/photoprism` ; .25 intact (lecture seule sur /volume1/photo).
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_PHOTO="/volume1/photo"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
BRIQUE="photoprism"
G9_DIR="/data/docker/$BRIQUE"
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
KIT_ON_G9="/home/folzo/g9-migration"
JOURNAL="$KIT_DIR/journal.md"
STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}"
MIN_FREE_GB=600
INDEX_TIMEOUT="${INDEX_TIMEOUT:-3600}"
SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST"
RSH_G9="ssh -i $KEY"
log() { echo -e "\n[photoprism] $*"; }
journal() {
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \
"$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL"
}
meta_manifest() { ( cd "$1" && find . -type f -printf '%p\t%s\n' 2>/dev/null | LC_ALL=C sort ); }
# ---------------------------------------------------------------- 0. préflight
log "=== 0/5 PRÉFLIGHT ==="
$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; }
$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; }
$SSH_XP "sudo test -d $XP_PHOTO" || { echo "ERREUR : $XP_PHOTO absent sur .25"; exit 1; }
FREE_GB=$($SSH_G9 "df -BG --output=avail /data | tail -1 | tr -dc '0-9'")
[[ "$FREE_GB" -ge "$MIN_FREE_GB" ]] || { echo "ERREUR : espace /data insuffisant (${FREE_GB} Go < ${MIN_FREE_GB} Go)"; exit 1; }
log "espace /data = ${FREE_GB} Go"
mkdir -p "$STAGE"
SRC_COUNT=$($SSH_XP "sudo find $XP_PHOTO -type f | wc -l")
SRC_BYTES=$($SSH_XP "sudo find $XP_PHOTO -type f -printf '%s\n' | awk '{s+=\$1} END {print s}'")
log "source .25 : $SRC_COUNT fichiers, $SRC_BYTES octets"
# ---------------------------------------------------------------- 1. backup G2
log "=== 1/5 BACKUP restic (G2) ==="
if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then
log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)"
else
$SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2"
journal "backup restic pré-migration" "Garde-fou G2 avant import PhotoPrism" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
fi
# ---------------------------------------------------------------- 2. copie des photos
if [[ "${SKIP_COPY:-0}" != "1" ]]; then
log "=== 2/5 COPIE $XP_PHOTO → $G9_DIR/originals (tar sur SSH, relais pilot) ==="
$SSH_G9 "sudo mkdir -p $G9_DIR/originals $G9_DIR/storage $G9_DIR/import && sudo chown -R $G9_USER:$G9_USER $G9_DIR"
# MariaDB = uid 999 : datadir à lui (sinon « Permission denied » sur db.opt).
$SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
t0=$(date +%s)
# Progression : tar GNU du G9 émet un pointage sur stderr tous les 50 000 enregistrements
# (stderr relayé par ssh) → la copie de 530 Go n'est pas silencieuse pendant des heures.
if ! $SSH_XP "sudo tar cz -C '$XP_PHOTO' ." | $SSH_G9 "tar xz --checkpoint=50000 --checkpoint-action=echo='[copie] %n enregistrements' -C $G9_DIR/originals"; then
echo "ERREUR : flux tar interrompu"; exit 1
fi
t=$(( $(date +%s) - t0 ))
log " copie terminée en ${t} s"
journal "copie photos .25 → G9" "530 Go, source en lecture seule (aucune écriture sur .25)" "tar/ssh relayé par le pilot, ${t} s ; $SRC_COUNT fichiers source" "rm -rf $G9_DIR/originals (source .25 intacte)"
fi
# ---------------------------------------------------------------- 3. intégrité
log "=== 3/5 INTÉGRITÉ (manifeste métadonnées + échantillon sha256) ==="
$SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort\"" > "$STAGE/src.meta"
$SSH_G9 "cd '$G9_DIR/originals' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort" > "$STAGE/g9.meta"
DST_COUNT=$(wc -l < "$STAGE/g9.meta")
DST_BYTES=$(awk -F'\t' '{s+=$2} END {print s}' "$STAGE/g9.meta")
log " source=$SRC_COUNT fichiers / $SRC_BYTES o — G9=$DST_COUNT fichiers / $DST_BYTES o"
[[ "$SRC_COUNT" == "$DST_COUNT" && "$SRC_BYTES" == "$DST_BYTES" ]] || { echo "ERREUR : compte/taille différents"; diff "$STAGE/src.meta" "$STAGE/g9.meta" | head -20; exit 1; }
diff "$STAGE/src.meta" "$STAGE/g9.meta" > "$STAGE/meta.diff" || { log "❌ chemins/tailles différents :"; head -20 "$STAGE/meta.diff"; exit 1; }
log " ✅ manifeste métadonnées identique ($DST_COUNT fichiers)"
SAMPLE=$(shuf -n 200 "$STAGE/g9.meta" | cut -f1)
printf '%s\n' "$SAMPLE" > "$STAGE/sample.txt"
n=$(wc -l < "$STAGE/sample.txt")
SRC_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && xargs -d '\n' sha256sum\"" | awk '{print $1}' | LC_ALL=C sort)
DST_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_G9 "cd '$G9_DIR/originals' && xargs -d '\n' sha256sum" | awk '{print $1}' | LC_ALL=C sort)
if [[ "$SRC_SHA" == "$DST_SHA" ]]; then
log " ✅ échantillon sha256 : $n/$n fichiers identiques"
else
log "❌ échantillon sha256 divergent"; diff <(echo "$SRC_SHA") <(echo "$DST_SHA") | head -10; exit 1
fi
journal "intégration PhotoPrism" "copie de 530 Go à vérifier avant indexation" "manifeste chemins+tailles identique ($DST_COUNT fichiers, $DST_BYTES o) + échantillon sha256 $n/$n" "re-copie ciblée via tar/ssh"
# ---------------------------------------------------------------- 4. stack + secrets
log "=== 4/5 STACK + SECRETS ==="
rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/"
rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/"
$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null"
$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets"
$SSH_G9 "sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
# PhotoPrism n'accepte pas les *_FILE : env_file construit à partir des secrets (600, root, hors git).
$SSH_G9 "sudo sh -c 'umask 077; { echo PHOTOPRISM_ADMIN_PASSWORD=\$(cat $G9_DIR/secrets/admin); echo PHOTOPRISM_DATABASE_PASSWORD=\$(cat $G9_DIR/secrets/dbpass); } > $G9_DIR/secrets/photoprism.env' && sudo chown root:root $G9_DIR/secrets/photoprism.env"
$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'"
# ---------------------------------------------------------------- 5. indexation + vérifs
log "=== 5/5 INDEXATION (progressive, décision #4) + VÉRIFS ==="
$SSH_G9 "curl -sf -o /dev/null -w 'photoprism HTTP %{http_code}\n' http://127.0.0.1:2342/api/v1/session/health"
$SSH_G9 "cd $G9_DIR && timeout $INDEX_TIMEOUT docker compose exec -T photoprism photoprism index 2>&1 | tail -6 < /dev/null || true"
$SSH_G9 "cd $G9_DIR && docker compose exec -T photoprism sh -c 'photoprism count 2>/dev/null | head -12' < /dev/null || true"
journal "stack PhotoPrism déployée + indexation lancée" "T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9)" "image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale" "docker compose down + rm -rf /data/docker/photoprism"
echo
log "TERMINÉ. Prochaine étape : laisser monter l'index (cron/kommand), puis T4.5 (n8n)."