excalidraw-go
Excalidraw auto-hébergé avec un gestionnaire de fichiers intégré : arborescence de dossiers, vignettes des schémas, création, renommage, sélection et déplacement multiples (glisser-déposer ou dialogue « Déplacer vers… »), duplication, suppression (corbeille), recherche, import/export de fichiers .excalidraw, sauvegarde automatique côté serveur. Interface web unique, responsive (l'explorateur devient un tiroir sur mobile).
- Backend : Go 1.24, bibliothèque standard uniquement (aucune dépendance externe), API REST/JSON, stockage en fichiers
.excalidrawsur un volume. - Frontend : React 19 + composant officiel
@excalidraw/excalidraw0.18.1 (licence MIT), build Vite embarqué dans le binaire Go (embed). - Image : multi-stage, finale en
distroless/static:nonroot(~53 Mo), un seul conteneur, aucun accès Internet requis à l'exécution (polices Excalidraw servies localement).
Architecture
Navigateur ──HTTP──▶ conteneur excalidraw-go (:8080, uid 65532, rootfs read-only)
├─ / SPA React + Excalidraw (embarquée dans le binaire)
├─ /api/... API fichiers (Go net/http)
└─ /healthz sonde
│
▼
volume /data
├─ projets/réseau/sdn.excalidraw ← fichiers JSON Excalidraw standards
├─ .library.excalidrawlib ← bibliothèque partagée
├─ .thumbs/projets/réseau/sdn.excalidraw.png ← miniatures (régénérables)
└─ .trash/<horodatage>/... ← éléments supprimés
Les schémas restent des fichiers .excalidraw standards : ils s'ouvrent tels quels dans excalidraw.com, VS Code (extension Excalidraw) ou Obsidian, et se sauvegardent/versionnent comme n'importe quel répertoire (rsync, restic, git...).
Démarrage
cp .env.example .env # optionnel : port, authentification Basic
docker compose up -d --build
# http://localhost:8080
| Variable | Défaut | Rôle |
|---|---|---|
LISTEN_ADDR |
:8080 |
Adresse d'écoute |
DATA_DIR |
/data |
Racine des schémas |
MAX_BODY_MB |
50 |
Taille max d'un schéma (images embarquées en base64 comprises) |
AUTH_USER / AUTH_PASSWORD |
vide | Active une authentification HTTP Basic si les deux sont définis |
Bind mount au lieu du volume nommé : - ./data:/data puis chown -R 65532:65532 ./data.
Utilisation
- + Schéma / + Dossier : création dans le dossier courant.
- Vignettes / liste : bouton ▦ / ☰ à droite de la recherche (choix mémorisé par navigateur).
- ⋯ sur un élément : renommer, déplacer, dupliquer, télécharger, supprimer.
- Sélection multiple : case à cocher (au survol, toujours visible sur écran tactile),
Ctrl/Cmd+clic,Maj+clicpour une plage,Ctrl+Apour tout. Tant qu'une sélection est en cours, un simple clic/tap ajoute ou retire un élément. La barre de sélection propose Déplacer… et la mise à la corbeille ;Échapannule,Supprsupprime. - Déplacer : glisser-déposer (souris) d'un élément — ou de toute la sélection — sur un dossier, sur
..ou sur le fil d'Ariane ; ou Déplacer… qui ouvre un sélecteur de dossier (navigation, création de dossier), utilisable au doigt. En cas de conflit de nom dans la destination, les autres éléments sont tout de même déplacés et les échecs listés. - Importer : envoie un ou plusieurs
.excalidrawlocaux dans le dossier courant. - Sauvegarde : automatique 1,5 s après la dernière modification, immédiate avec
Ctrl/Cmd+S. Le statut (Modifié / Enregistrement… / Enregistré) est affiché dans la barre du haut. - Lien direct : l'URL
http://hôte:8080/#/dossier/schema.excalidrawouvre le schéma. - Bibliothèque Excalidraw : persistée côté serveur, partagée entre tous les schémas.
Miniatures
Rendues par le navigateur avec exportToBlob d'Excalidraw (PNG 320 px), puis stockées dans DATA_DIR/.thumbs/ :
- à chaque sauvegarde du schéma ouvert ;
- à la demande pour les schémas sans miniature ou modifiés hors de l'application (import, copie de fichiers sur le volume, script) : l'explorateur charge le schéma, génère la vignette (2 en parallèle, seulement pour les éléments visibles) et l'envoie au serveur, une fois pour toutes.
Une miniature plus ancienne que son schéma est ignorée (404) puis régénérée. Elles suivent les renommages, déplacements, copies et suppressions ; .thumbs/ peut être supprimé sans risque. En thème sombre, elles sont inversées en CSS comme le canevas Excalidraw.
Concurrence
Chaque lecture renvoie un ETag (SHA-256 tronqué du contenu) ; chaque sauvegarde l'envoie en If-Match. Si le fichier a été modifié ailleurs (autre onglet, autre poste), le serveur répond 412 et l'interface propose d'écraser ou de recharger. Il n'y a pas d'édition collaborative temps réel (voir Évolutions).
API
Tous les chemins sont relatifs à DATA_DIR, en notation /. Les composants commençant par . sont refusés, et les liens symboliques sortant de la racine sont bloqués.
| Méthode | Route | Corps / paramètres | Réponse |
|---|---|---|---|
| GET | /api/list?path=dir |
[Entry] (dossiers puis schémas) |
|
| GET | /api/search?q=texte |
[Entry] (200 max) |
|
| GET | /api/file?path=x.excalidraw |
JSON du schéma + ETag |
|
| PUT | /api/file?path=x.excalidraw |
JSON ; If-Match: <etag> ou If-None-Match: * (création) |
Entry + ETag ; 409 existe, 412 conflit |
| POST | /api/folder |
{"path":"a/b"} |
Entry |
| POST | /api/move |
{"from":"a.excalidraw","to":"d/b.excalidraw"} |
Entry (renommage / déplacement, fichier ou dossier) |
| POST | /api/move-many |
{"paths":["a.excalidraw","dossier"],"dir":"d"} |
[{"from","to"}|{"from","error"}] : déplacement multiple, résultat par élément |
| GET | /api/thumb?path=x.excalidraw&v=<modTime> |
PNG ; 404 si absente ou périmée |
|
| PUT | /api/thumb?path=x.excalidraw |
PNG (1 Mo max) | 204 |
| POST | /api/copy |
{"from":"…","to":"…"} |
Entry |
| DELETE | /api/entry?path=x[&recursive=true] |
204 (déplacé dans .trash/) |
|
| GET/PUT | /api/library |
bibliothèque excalidrawlib |
|
| GET | /healthz |
ok (hors authentification) |
Entry = {"name","path","isDir","size","modTime"}.
Exemple d'automatisation (génération de schémas par script) :
curl -X PUT 'http://localhost:8080/api/file?path=auto/inventaire.excalidraw' \
-H 'If-None-Match: *' --data-binary @inventaire.excalidraw
Développement
# Terminal 1 : API
go run ./cmd/server # DATA_DIR=./data par défaut
# Terminal 2 : frontend avec rechargement à chaud (proxy /api -> :8080)
cd frontend && npm install && npm run dev
# Tests et build complet hors Docker
go test ./...
cd frontend && npm run build && cd .. && go build -o server ./cmd/server
Arborescence :
cmd/server/main.go serveur HTTP, SPA embarquée, auth Basic, healthcheck
internal/storage/ accès disque : sécurité des chemins, écriture atomique, ETag, corbeille
internal/api/ routes REST
web/embed.go embarque web/dist (produit par le build Vite)
frontend/src/App.tsx éditeur Excalidraw, sauvegarde auto, conflits, bibliothèque
frontend/src/Explorer.tsx gestionnaire de fichiers (vignettes/liste, sélection multiple, glisser-déposer)
frontend/src/MoveDialog.tsx sélecteur de dossier « Déplacer vers… »
frontend/src/Thumb.tsx vignette chargée à l'affichage, générée si absente
frontend/src/thumbs.ts rendu PNG des miniatures et file de génération
Sécurité
- Conteneur non-root, rootfs en lecture seule,
cap_drop: ALL,no-new-privileges. - L'authentification Basic intégrée suffit pour un usage local ; en exposition réseau, placer le service derrière un reverse proxy TLS (Traefik, Caddy, nginx) avec SSO (Authelia, oauth2-proxy, Authentik) et laisser
AUTH_*vides. - Écritures atomiques (fichier temporaire +
fsync+rename) : pas de schéma tronqué en cas d'arrêt brutal. - Aucune suppression physique : la purge de
.trash/est à la charge de l'exploitant (ex.find /data/.trash -mindepth 1 -maxdepth 1 -mtime +30 -exec rm -rf {} +).
Limites et évolutions possibles
- Collaboration temps réel : nécessiterait le serveur excalidraw-room (WebSocket) et la logique de réconciliation (
reconcileElements) côté client. - Historique de versions : commit git automatique dans
DATA_DIRà chaque sauvegarde, ou snapshots du volume (ZFS/Btrfs). - Multi-utilisateurs avec espaces privés : identité transmise par le reverse proxy (
X-Forwarded-User) et racine par utilisateur. - Les schémas avec beaucoup d'images embarquées sont volumineux (base64 dans le JSON) ; ajuster
MAX_BODY_MB.