Le problème de départ #
QFieldCloud auto-hébergé [E05] : l'instance de base, prête pour la production
Le contexte : une instance QFieldCloud a été remontée à neuf, à partir d’un dossier vide. L’idée était de resynchroniser le même projet QGIS dessus et de retrouver ses photos. Ça paraît raisonnable. C’est faux, et comprendre pourquoi est tout l’objet de cette annexe.
Les fichiers qu’on touche #
Aucun. Cette annexe n’écrit pas une ligne dans le dépôt de déploiement : elle lit une base, un .gpkg et un bucket ressuscité. Les deux fichiers qu’elle ouvre — le projet QGIS et le géopackage — appartiennent à la donnée, pas à l’instance, et chaque bloc de code plus bas le rappelle en première ligne.
Ce que QFieldCloud sait, et ce qu’il ignore #
Premier réflexe, et première erreur : chercher les photos dans QFieldCloud. La base de l’ancienne instance connaît onze fichiers pour ce projet :
$ psql -d qfieldcloud_db -tAc "SELECT name FROM filestorage_file ORDER BY name"
38160_38232_acca_entente.gpkg
38160_38232_acca_entente_lizmap_cloud.qgs
38160_38232_acca_entente_lizmap_cloud_attachments.zip
auth/minio-s3.xml
DCIM/9c9e9d77-…-20251014171549468.jpg
fonts/Lato-Black.ttf
…
Une seule photo, dans DCIM/ — celle-là est bien gérée par QFieldCloud. Les quarante-cinq autres n’apparaissent nulle part. Elles vivent dans un bucket séparé, qfieldcloud-attachments, que la base ne mentionne jamais.
Lire un stockage objet dont le serveur est mort #
Le volume de l’ancien stockage est toujours sur le disque. Mais on ne lit pas un bucket avec cat :
$ find …/qfieldcloud-local/…/38160_38232_acca_entente.gpkg/v20260901145800-08094c12 -type f
…/xl.meta
…/5db6150a-…/part.1
…/5db6150a-…/part.2
…
Le format interne découpe chaque objet en parties précédées de métadonnées. Concaténer les part.N produit un fichier de la bonne taille à mille octets près — et une empreinte fausse, donc un GeoPackage illisible. La leçon est simple : on ne lit pas un stockage objet à la main, on lui redonne un serveur.
$ docker run -d --name oldrustfs -v /copie/du/volume:/data \
-e RUSTFS_ACCESS_KEY=… -e RUSTFS_SECRET_KEY=… rustfs/rustfs:1.0.0-beta.11
$ mc alias set old http://127.0.0.1:9000 "$RUSTFS_ACCESS_KEY" "$RUSTFS_SECRET_KEY" --api S3v4
$ mc ls old
[2026-09-01 14:26:19 UTC] 0B qfieldcloud-attachments/
[2026-09-01 14:26:17 UTC] 0B qfieldcloud-local/
$ mc du old/qfieldcloud-attachments
217MiB 43 objects qfieldcloud-attachments
Deux précautions valent d’être notées. On travaille sur une copie du volume, jamais sur l’original — un serveur objet écrit dès qu’il démarre. Et l’image utilisée doit être celle qui a écrit les données : un format interne n’est pas un format d’échange.
Une fois le serveur debout, le GeoPackage se récupère normalement, et l’empreinte le confirme :
$ mc cp old/qfieldcloud-local/projects/1ae2804d-…/files/38160_38232_acca_entente.gpkg/v20260901145800-08094c12 acca.gpkg
$ md5sum acca.gpkg
d2355267b83713e929fe6dea8fa30527 # identique à celle enregistrée en base
Ce qui relie une photo à son objet #
Le projet QGIS, extrait du même bucket, contient la réponse. Le champ photo de la table de liaison est configuré ainsi :
<!-- 38160_38232_acca_entente_lizmap_cloud.qgs — le projet QGIS, pas un fichier de la stack -->
<field name="photo">
<editWidget type="ExternalResource">
<config>
<Option name="StorageType" type="QString" value="AWSS3"/>
<Option name="StorageUrl" type="QString"
value="https://s3-api.example.org/qfieldcloud-attachments/entente_acca_chasse/photos/"/>
<Option name="StorageAuthConfigId" type="QString" value="6lie345"/>
<Option name="RelativeStorage" type="int" value="0"/>
</config>
</editWidget>
</field>RelativeStorage=0 est le détail qui commande tout : la valeur écrite dans la donnée est l’URL absolue. Vérification dans le GeoPackage :
$ sqlite3 acca.gpkg "SELECT * FROM tj_acca_battue_poste_photo LIMIT 2"
1|110e925d-…|https://s3-api.example.org/qfieldcloud-attachments/entente_acca_chasse/photos/IMG_20251010_155429991.jpg
2|d8b4bf3b-…|https://s3-api.example.org/qfieldcloud-attachments/entente_acca_chasse/photos/IMG_20251010_155351187.jpg
Le lien entre un poste de chasse et sa photo, c’est une chaîne de caractères dans une table du GeoPackage. Pas une référence QFieldCloud, pas un chemin relatif, pas une pièce jointe : une URL, gravée dans la donnée au moment de la prise de vue.
L’inventaire, et ce qu’il révèle #
lignes en base : 46
fichiers distincts : 45 # une photo référencée deux fois, normal
objets dans le bucket : 43
hôtes distincts : {'s3-api.example.org'}
Trois références n’ont pas d’objet correspondant :
=== références SANS objet ===
IMG_20251102_103748356.jpg
IMG_20251102_103811015.jpg
IMG_20251102_103955553.jpg
Toutes du 2 novembre 2025, à quelques minutes d’intervalle. Elles ne sont dans aucune des deux copies du stockage — ni le volume MinIO d’origine, ni le miroir fait lors de la migration. Elles n’ont jamais atteint le serveur.
Attention : c’est la faiblesse structurelle du stockage externe, et elle mérite d’être connue avant de le choisir. QGIS écrit la référence dans l’attribut, puis envoie le fichier. Si l’envoi échoue — terrain hors réseau, serveur momentanément absent, session fermée trop tôt — la référence survit sans le fichier. Rien ne le signale : ni la couche, ni le projet, ni un journal. On le découvre en ouvrant la fiche, des mois plus tard.
Le contrôle tient en une requête sur le GeoPackage comparée à la liste des objets du bucket, et il devrait être périodique. Concrètement :
# script d'enquête, à lancer sur une COPIE du .gpkg — rien à installer
# les noms de fichiers référencés par la donnée
sqlite3 acca.gpkg \
"SELECT photo FROM tj_acca_battue_poste_photo WHERE photo IS NOT NULL" \
| xargs -n1 basename | sort -u > /tmp/references
# les objets réellement présents dans le bucket
mc ls --recursive old/qfieldcloud-attachments \
| awk '{print $NF}' | xargs -n1 basename | sort -u > /tmp/objets
# ce que la donnée réclame et que le stockage n'a pas
comm -23 /tmp/references /tmp/objetscomm -23 ne garde que les lignes de gauche : les références orphelines. Une sortie vide est le seul résultat acceptable. L’inverse, comm -13, liste les objets que plus aucune donnée ne désigne — moins grave, mais utile avant de facturer du stockage.
Dernier point relevé : le bucket était privé.
$ curl -o /dev/null -w '%{http_code}' http://…/qfieldcloud-attachments/…/IMG_20251010_155429991.jpg
403
$ mc anonymous get old/qfieldcloud-attachments
Access permission for `old/qfieldcloud-attachments` is `private`
Or les URL stockées sont nues, sans signature. L’accès reposait donc entièrement sur la configuration d’authentification QGIS 6lie345, présente sur chaque poste et sur chaque client QField. C’est un point à trancher avant de remettre le service en route, pas après.
Le plan de remise en service #
Deux routes, et elles ne se valent pas.
| Ce qu’il faut faire | Donnée touchée | |
|---|---|---|
| A. Reconstruire l’endpoint | Un bucket au même nom sur le stockage objet actuel, le miroir des 43 objets, et le nom d’hôte historique repointé dessus | aucune |
| B. Réécrire les URL | Modifier les 46 valeurs du GeoPackage et le StorageUrl du projet, puis reverser le projet |
46 lignes et le .qgs |
La route A est la bonne, pour une raison qui dépasse le confort : elle ne touche pas à la donnée de terrain. Une réécriture d’URL en masse sur un GeoPackage synchronisé avec des clients QField, c’est un conflit de synchronisation en puissance sur chaque appareil. Reconstruire un endpoint, c’est une ligne de configuration sur un frontal.
Elle répare aussi le chemin d’écriture : le StorageUrl du projet pointe au même endroit, donc les prochaines photos repartent dans le bon bucket sans qu’on rediffuse quoi que ce soit.
La décision restante porte sur l’authentification :
- reconduire l’authentification — nouvelle clé sur le stockage actuel, mise à jour de la configuration QGIS, rediffusion à tous les clients ;
- passer le bucket en lecture publique — les URL fonctionnent partout sans configuration, au prix d’être lisibles par quiconque connaît le nom exact d’un fichier.
Pour des photos de postes de chasse derrière un nom d’hôte non indexé, la seconde est défendable et bien plus simple à maintenir dans la durée. Mais c’est un choix à faire avant de rediffuser quoi que ce soit, pas après.
Ce qu’il faut retenir #
- Un stockage externe QGIS n’a rien à voir avec QFieldCloud. Resynchroniser un projet ne rattache aucune photo : le lien est une URL absolue écrite dans la donnée.
-
RelativeStorage=0est le réglage qui décide de tout. Il grave l’URL dans l’attribut, donc l’adresse du serveur devient une donnée métier. - On ne lit pas un stockage objet à la main. On remonte un serveur de la bonne version sur une copie du volume.
- Une référence peut survivre à son fichier. L’attribut est écrit avant l’envoi ; si l’envoi échoue, plus rien ne le signale. Comparez périodiquement la base et le bucket.
- Réparez l’adresse, pas la donnée. Reconstruire l’endpoint ne touche rien ; réécrire les URL crée un conflit de synchronisation par client.
Image de couverture : Scott Blake sur Unsplash.