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
+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)."