excalidraw-go
Excalidraw auto-hébergé avec un gestionnaire de fichiers intégré : arborescence de dossiers, création, renommage, déplacement (glisser-déposer ou menu), 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
└─ .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.
- ⋯ sur une ligne : renommer, déplacer, dupliquer, télécharger, supprimer.
- Glisser-déposer (souris) d'un élément sur un dossier, sur
..ou sur le fil d'Ariane pour le déplacer. Sur écran tactile : menu ⋯ → Déplacer… - 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.
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/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
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. - Miniatures dans l'explorateur : générables côté client via
exportToSvget stockées à côté du fichier. - 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.