+++
title = "Migrer Nextcloud d’un stockage local à S3"
date = 2023-01-15
[taxonomies]
tags = [ "système", "s3", "garage", "stockage", "nextcloud" ]
+++

> Ah bah ça, ça tombe bien alors !

*La foule en délire*

<!-- more -->

**Avertissement**

Même si je suis certain de mon coup et que cette procédure a été testé et validé avec succès sur quelques instances Nextcloud, elle doit tout de même se faire à vos risques et périls : je ne peux offrir aucune garantie quant à sa réussite et je ne l’ai jamais testée sur des instances de grande ampleur (plusieurs dizaines, voire centaines de gigaoctets, avec des partages dans tous les sens). Voilà, t’es prévenu, prends tes précautions.

# Introduction

L’idée de cette présente est de permettre à tout un chacun de migrer les données hébergées en local sur son instance Nextcloud vers du S3 (dans un [garage](https://blog.libertus.eu/tags/garage/), pourquoi pas). Cette procédure n’étant absolument pas prévu par Nextcloud (aucun outil officiel, même pas une doc), elle a été faite à la main avec sueur et amour ❤️.

# Fais une maudite sauvegarde ostie de marde !

Alors, avant de commencer à rentrer dans le dur, deux ou trois précautions à prendre, quoiqu’il arrive :
* **Éjecte tous tes utilisateurs de l’instance**. Tous. Mémé, son chien, ton petit frère, faut virer tout le monde. C’est pas des blagues hein, tu le fais, je t’attends (Note : le plus simple est d’activer le mode maintenance en ligne de commande via `occ maintenance:mode --on`). Pour les clients de synchro et les clients Web, cela peut prendre jusqu’à une demi-heure pour que toutes les sessions soient bien mortes côté serveur, on y reviendra.
* **Fais une putain de sauvegarde de absolument tout**. Là, c’est pareil, vas-y, je t’attends. On se fout que ça prenne 10 minutes ou 2h, mais il faut impérativement que tu ais une sauvegarde de tout : base de données **et** données utilisateurs.
* Arme-toi de patience (et d’une bière).

# La structure S3 sous Nextcloud

Les données ne vont pas être structurées de la même manière sur S3 et sur ton disque local. En fait, quand tu navigues dans ton Nextcloud, tu navigues essentiellement dans la structure de la base de données. En gros, tout est virtuel.

Quand tu passes en S3, cette structure, cette hiérarchie de fichiers qui était si familière, va être complètement explosée : pour des questions d’optimisations, le greffon S3 stocke tous les fichiers à plat à la racine du seau S3 en les identifiant avec leur `fileid` (extrait directement de la base de données).

Il va donc falloir **transformer** la structure de tes dossiers et fichiers en une structure mangeable par Nextcloud S3. Pour cela et pour éviter de faire des copies dans tous les sens, on va **recréer** la structure en question avec des liens symboliques dans un dossier de transfert et ensuite, on va faire une synchro complète vers S3.

# Première étape : nettoyer la base de données

C’est loin d’être une obligation mais malheureusement, Nextcloud est extrêmement mauvais pour faire le ménage dans ses propres tables de fichiers et de cache. On va s’intéresser à deux tables essentiellement : `oc_filecache` (qui contient en fait le cache de tous les fichiers, leurs propriétés de base, etc…) et `oc_storages` (qui contient en fait les différents stockages des différents utilisateurs et le stockage de l’instance elle-même).

La première chose à vérifier est assez simple : **il faut impérativement s’assurer qu’aucune entrée dans `oc_filecache` ne fasse référence un stockage inexistant dans `oc_storages`**. Les deux tables sont liés par l’attribut `oc_filecache.storage = oc_storages.numeric_id`.

On peut facilement vérifier le nombre d’entrées avec la commande SQL suivante :

```sql
SELECT storage, count(storage)
FROM oc_filecache
GROUP BY storage;

SELECT * FROM oc_storages;
```

Logiquement, la colonne `storage` de la première requête devrait correspondre exactement à la sortie de la seconde requête. Si ce n’est pas le cas, **vérifie qu’il n’y a pas une cagade entre les deux**. Sur ma vieille instance, mes fichiers les plus anciens étaient en double dans pratiquement toute la table `oc_filecache` (je suppose que c’est une mise à jour qui s’est mal passée).

En toute logique, la table `oc_storages` ne devrait contenir que :
* une entrée `local::<chemin vers le stockage de ton nextcloud>`
* plusieurs entrées `home::<utilisateur>`

Si tu vois des utilisateurs en double, des `oc_filecache.storage` qui ne correspondent plus à rien, il est temps de faire le ménage.

*Note : il est très certainement possible de faire une requête SQL des familles pour accomplir cela en une fois, je n’ai pas creusé, un truc de plus à faire si besoin…*

Toujours dans le même état d’esprit, cela peut être intéressant de purger les corbeilles et les versions de fichiers (ça fera toujours ça de moins à transférer). À noter qu’il est nécessaire pour cela de sortir du mode maintenance. Donc, si tu as prévu une migration un jour donné, ça peut être intéressant de commencer à faire le ménage *in vivo* quelques jours ou quelques semaines avant.

# Deuxième étape : préparer les données

Bon voilà, maintenant que tu as mis tout en maintenance, que tu as fait une grosse sauvegarde de tout et que ta base de données est nettoyée (autant que faire se peut en tout cas), on va pouvoir attaquer le vif du sujet.

Donc, on va commencer par établir une liste exhaustive de tous les fichiers des utilisateurs et les mettre dans un fichier assez simple avec une association `urn:oid:<numéro de fichiers>` vers le chemin actuel.

Pour cela, un petit coup de shell/sql :

```bash
mysql -B --disable-column-names -D <ta base de données> << EOF > user_file_list
    SELECT CONCAT('urn:oid:', fileid, ' ', '<chemin vers les données Nextcloud>', SUBSTRING(id from 7), '/', path)
    FROM oc_filecache JOIN oc_storages
    ON storage = numeric_id
    WHERE id LIKE 'home::%'
    ORDER BY id;
EOF
```

Tu prendras évidemment soin de remplacer le nom de la base de données ainsi que le chemin où se trouve les données. Il s’agit bien du répertoire de données ; par défaut, il s’agit du répertoire `data` à la racine de l’installation, mais techniquement, ça peut être n’importe où.

Logiquement, tu devrais obtenir un fichier dont les lignes ressemblent à ça(c’est, au passage, le bon moment de vérifier que les chemins en question sont bons) :

```
urn:oid:120795 /srv/nextcloud/user1/files/Documents/Welcome to Nextcloud Hub.docx
```

De la même manière, on va établir la liste des métadonnées (les données exploitées par Nextcloud lui-même et qui sont stockées d’ordinaire dans le répertoire `appdata`) :

```bash
mysql -B --disable-column-names -D <ta base de données> << EOF > meta_file_list
    SELECT CONCAT('urn:oid:', fileid, ' ', SUBSTRING(id from 8), path)
    FROM oc_filecache JOIN oc_storages
    ON storage = numeric_id
    WHERE id LIKE 'local::%'
    ORDER BY id;
EOF
```

# Troisième étape : transférer les données

L’idée générale est de créer des liens symboliques vers les « vraies » données pour éviter de faire une copie en local avant de balancer une copie vers le S3. Comme on vient d’établir la liste des données pour les métadonnées et les données utilisateurs, ce n’est pas bien compliqué :

```bash
mkdir s3_files
cd s3_files
while read target source ; do
    if [ -f "$source" ] ;
    then
        ln -sv "$source" "$target"
    fi
done < ../user_file_list
while read target source ; do
    if [ -f "$source" ] ;
    then
        ln -sv "$source" "$target"
    fi
done < ../meta_file_list
```

En toute logique, tu devrais te retrouver avec un répertoire contenant quelques dizaines/centaines/milliers de liens symboliques.

Une bonne idée pour vérifier que rien n’a merdé, c’est de compter les liens en question :

```bash
ls -l | wc -l
find <chemin vers les données Nextcloud> -type f | wc -l
```

Tu peux aussi vérifier la taille de l’ensemble :

```bash
du -Lhsc *
```

Si tu es confort avec les chiffres que tu vois (qu’au moins ils ont le bon ordre de grandeur), tu peux passer à la suite.

*Note : à ce stade, je pars du principe que tu as créé le bucket S3 et la clé correspondante qui va bien dans `garage`.*

Pour la partie transfert en elle-même, j’ai essayé [le client MinIO](https://min.io/download) sans grand succès à chaque fois. Je ne sais pas pourquoi, mais ça a toujours merdé avec ce client.

Du coup, je me suis décidé à passer par `awscli` pour éviter les emmerdes. Peu importe comment tu l’installes (via le gestionnaire de paquets ou depuis `pip`), l’important c’est qu’il soit configuré correctement : utilise la même clé et le même secret dans `~/.aws/credentials` et n’oublie pas de mettre la région `garage` dans `~/.aws/config`. Logiquement, c’est tout ce dont tu auras besoin.

On se place ensuite dans le répertoire `s3_files` contenant l’ensemble des liens symboliques et on s’arme d’une autre bière et de beaucoup de patience :

```bash
aws --endpoint-url http://<adresse de garage>:3900 s3 sync . s3://nextcloud
```

Évidemment, la commande est à adapter à la situation. Si un transfert merdouille, le simple fait de la relancer doit suffire à retransférer les fichiers qui ont merdé. On peut également surveiller ce qu’il se passe côté `garage` pour être certain que rien ne foire de manière hyper évidente.

**En toute logique, si tu relances la commande et qu’il ne se passe plus rien, c’est que le transfert est intégralement terminé.**

On va donc pouvoir passer au nettoyage/adaptation de la base.

# Quatrième étape : adaptation de la base de données

Les données sont transférées mais la base de données à toujours les anciennes références, il faut donc les adapter. Première chose donc, modifier les `storages` utilisateur :

```sql
UPDATE oc_storages
SET id = CONCAT('object::user:', SUBSTRING(id from 7))
WHERE id LIKE 'home::%';
```

Tu transformes ici les références `home::<utilisateur>` en référence `object::user:<utilisateur>` (note que le `:` seul à la fin est normal). Il faut ensuite transformer le stockage de Nextcloud lui-même :

```sql
UPDATE oc_storages
SET id = 'object::store:amazon::<nom du bucket S3>'
WHERE id LIKE 'local::%';
```

**Normalement**, tu ne devais avoir qu’un seul `local::*` dans cette table. Si ce n’est pas le cas, c’est peut-être qu’un truc est resté allumé malgré la maintenance ou qu’il s’est passé autre chose. Quoiqu’il arrive, ça vaut le coup d’être vérifié avant de continuer.

**Normalement**, à partir de cette étape, on ne touchera plus à la table `oc_storages` et elle ne devrait donc contenir que deux types d’entrées :
* une entrée `object::user:<utilisateur>` pour chaque utilisateur
* une entrée `object::storage:amazon::<nom du bucket S3>`

La casse, les `:` ou `::`, le nom des utilisateurs, le nom du bucket S3, **tout cela a une importance**, donc il vaut mieux vérifier 3 fois pour être sûr.

Dernière adaptation, il est nécessaire de faire des changements dans la table `oc_mounts` (qui gère notamment comment les points de montage comme les partages vont être gérés par Nextcloud) :

```sql
UPDATE oc_mounts
SET mount_provider_class = 'OC\\Files\\Mount\\ObjectHomeMountProvider';
```

Cette table devrait normalement contenir :
* une entrée par utilisateur
* une entrée par partage

La colonne `mount_provider_class` devrait maintenant être uniforme avec la valeur `OC\Files\Mount\ObjectHomeMountProvider` (comme d’habitude avec les caractères d’échappement de type `\`, se méfier du résultat).

# Dernière étape : configurer Nextcloud et vérifier l’ensemble

Bon, on a bien transpiré, il est temps de finir la configuration. Dans le fichiers `config/config.php` de Nextcloud, tu peux maintenant ajouter ton instance `garage` avec les mêmes informations que tu avais configuré dans `awscli` directement dans l’objet `$CONFIG` :

```php
'objectstore' => [
    'class' => '\\OC\\Files\\ObjectStore\\S3',
    'arguments' => [
        'bucket' => '<nom du bucket S3>',
        'autocreate' => false,
        'key'    => '<clé>',
        'secret' => '<secret>',
        'hostname' => '<adresse de garage>'
        'port' => <port de garage>,
        'use_ssl' => false, // à adapter si tu as un reverse proxy avec du TLS
        'region' => 'garage',
        'use_path_style' => true
    ],
],
```

Tu peux maintenant virer la maintenance, te reconnecter sur l’interface Web et vérifier que tout fonctionne correctement. Mon conseil : passer dans quelques partages, ouvrir des fichiers que tu n’a pas accédé depuis un petit moment. Ça peut être le bon moment aussi pour relancer une synchro complète sur un client vierge histoire de vérifier que tous les accès sont bons.

Dès que tu es suffisamment confiant dans ta migration, tu peux virer les répertoires de données d’origine et virer le répertoire contenant les liens symboliques.

# Conclusation

Et ben, on va pouvoir prendre des vacances bien méritées avec ça…
