mxdrw expects HTTP Basic Auth (BASIC_AUTH_USER + BASIC_AUTH_PASS on the server side). Replace MEXDRAW_TOKEN with MEXDRAW_USER + MEXDRAW_PASS, use req.SetBasicAuth on the export PUT. Updated docker-compose.yml comment and README env table to match. Roundtrip verified locally against mxdrw.msbls.de.
123 lines
3.4 KiB
Go
123 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"mgit.msbls.de/m/mcables/internal/db"
|
|
"mgit.msbls.de/m/mcables/internal/exporter"
|
|
)
|
|
|
|
// syncExport runs the project's snapshot through the exporter, persists
|
|
// the assigned excalidraw_ids, then PUTs the scene to mxdrw.msbls.de.
|
|
func (h *handlers) syncExport(w http.ResponseWriter, r *http.Request) {
|
|
pid, ok := parseInt64Path(r, "pid")
|
|
if !ok {
|
|
writeError(w, db.ErrInvalidInput, "pid must be a positive integer")
|
|
return
|
|
}
|
|
|
|
base := os.Getenv("MEXDRAW_BASE_URL")
|
|
if base == "" {
|
|
base = "https://mxdrw.msbls.de"
|
|
}
|
|
user := os.Getenv("MEXDRAW_USER")
|
|
pass := os.Getenv("MEXDRAW_PASS")
|
|
if user == "" || pass == "" {
|
|
writeJSON(w, http.StatusBadRequest, errorBody{
|
|
Error: "MEXDRAW_USER / MEXDRAW_PASS not set",
|
|
Details: "Add MEXDRAW_USER and MEXDRAW_PASS to /home/m/secrets/mcables/.env on mDock and restart the container — mxdrw expects HTTP Basic Auth",
|
|
})
|
|
return
|
|
}
|
|
|
|
snap, err := h.store.Snapshot(pid)
|
|
if err != nil {
|
|
writeError(w, err, nil)
|
|
return
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
scene, ids := exporter.BuildScene(snap, now, exporter.Generate21)
|
|
|
|
// Persist the freshly-assigned ids so the next export reuses them.
|
|
// We pass in the full maps; PersistExcalidrawIDs is idempotent (it
|
|
// only updates rows whose excalidraw_id is still NULL).
|
|
if err := h.store.PersistExcalidrawIDs(pid, ids.Frames, ids.Devices, ids.Ports, ids.IOMarkers, ids.Cables); err != nil {
|
|
writeError(w, fmt.Errorf("persist excalidraw_ids: %w", err), nil)
|
|
return
|
|
}
|
|
|
|
payload, err := exporter.MarshalScene(scene)
|
|
if err != nil {
|
|
writeError(w, fmt.Errorf("marshal scene: %w", err), nil)
|
|
return
|
|
}
|
|
|
|
drawingName := snap.Project.DrawingName
|
|
if !strings.HasSuffix(drawingName, ".excalidraw") {
|
|
drawingName += ".excalidraw"
|
|
}
|
|
url := strings.TrimSuffix(base, "/") + "/api/drawings/" + drawingName
|
|
|
|
// Sane network timeout; mxdrw is on the LAN so this should be quick.
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(payload))
|
|
if err != nil {
|
|
writeError(w, fmt.Errorf("build PUT: %w", err), nil)
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.SetBasicAuth(user, pass)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadGateway, errorBody{
|
|
Error: "mxdrw unreachable",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 400 {
|
|
writeJSON(w, http.StatusBadGateway, errorBody{
|
|
Error: fmt.Sprintf("mxdrw rejected upload (%d)", resp.StatusCode),
|
|
Details: map[string]any{
|
|
"status": resp.StatusCode,
|
|
"body": string(body),
|
|
"url": url,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Best-effort parse — mxdrw returns whatever it returns; we surface
|
|
// the public viewer URL no matter what.
|
|
var serverEcho any
|
|
_ = json.Unmarshal(body, &serverEcho)
|
|
|
|
viewerURL := strings.TrimSuffix(base, "/") + "/draw/" + strings.TrimSuffix(drawingName, ".excalidraw") + ".excalidraw"
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
"drawing_name": drawingName,
|
|
"url": viewerURL,
|
|
"element_count": len(scene.Elements),
|
|
"mxdrw_response": serverEcho,
|
|
})
|
|
}
|
|
|
|
// noLeak prevents unused-import errors if errors-pkg ever becomes unused
|
|
// after a refactor — keeps the import light.
|
|
var _ = errors.New
|