diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d2eac58
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.gocache/
+java/
+gliffy2drawio
+sample*.drawio
+sample*.gliffy
+.DS_Store
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..882f8a6
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+# Build stage
+FROM golang:1.25-alpine AS builder
+
+WORKDIR /app
+COPY . .
+RUN go build -o server ./cmd/server
+
+# Runtime stage
+FROM alpine:3.20
+
+WORKDIR /app
+COPY --from=builder /app/server /usr/local/bin/server
+EXPOSE 8080
+
+ENTRYPOINT ["/usr/local/bin/server"]
diff --git a/cmd/server/main.go b/cmd/server/main.go
new file mode 100644
index 0000000..1713032
--- /dev/null
+++ b/cmd/server/main.go
@@ -0,0 +1,258 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+
+ gliffy2drawio "gliffy2drawio"
+)
+
+const (
+ maxUploadSize = 10 << 20 // 10MB
+)
+
+var uploadTpl = template.Must(template.New("upload").Parse(`
+
+
+
+
+ Gliffy → draw.io Converter
+
+
+
+
+
Gliffy → draw.io Converter
+
+
API docs: Swagger UI
+
+
+
+`))
+
+type apiRequest struct {
+ Data string `json:"data"` // raw Gliffy JSON
+}
+
+type apiResponse struct {
+ XML string `json:"xml"`
+ Warn string `json:"warning,omitempty"`
+}
+
+func main() {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", uploadHandler)
+ mux.HandleFunc("/convert", uploadConvertHandler)
+ mux.HandleFunc("/api/convert", apiConvertHandler)
+ mux.HandleFunc("/openapi.json", openAPISpecHandler)
+ mux.HandleFunc("/swagger", swaggerUIHandler)
+
+ addr := ":8080"
+ log.Printf("Server listening on %s", addr)
+ log.Fatal(http.ListenAndServe(addr, mux))
+}
+
+func uploadHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ if err := uploadTpl.Execute(w, nil); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func uploadConvertHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ log.Printf("[upload] %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
+
+ r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
+ if err := r.ParseMultipartForm(maxUploadSize); err != nil {
+ http.Error(w, "failed to parse form: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ file, header, err := r.FormFile("file")
+ if err != nil {
+ http.Error(w, "missing file: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+ data, err := io.ReadAll(file)
+ if err != nil {
+ http.Error(w, "failed to read file: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ xml, warn, err := convert(string(data))
+ if err != nil {
+ log.Printf("[upload] conversion failed: %v", err)
+ http.Error(w, "conversion failed: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.drawio"`, safeName(header.Filename)))
+ w.Header().Set("Content-Type", "application/xml")
+ if warn != "" {
+ w.Header().Set("X-Conversion-Warning", warn)
+ }
+ log.Printf("[upload] conversion succeeded, warn: %q, bytes out: %d", warn, len(xml))
+ _, _ = w.Write([]byte(xml))
+}
+
+func apiConvertHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ log.Printf("[api] %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
+ defer r.Body.Close()
+ var req apiRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ xml, warn, err := convert(req.Data)
+ if err != nil {
+ log.Printf("[api] conversion failed: %v", err)
+ http.Error(w, "conversion failed: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ resp := apiResponse{XML: xml, Warn: warn}
+ w.Header().Set("Content-Type", "application/json")
+ log.Printf("[api] conversion succeeded, warn: %q, bytes out: %d", warn, len(xml))
+ _ = json.NewEncoder(w).Encode(resp)
+}
+
+func convert(gliffyJSON string) (string, string, error) {
+ log.Printf("[convert] starting, input bytes: %d", len(gliffyJSON))
+ converter, err := gliffy2drawio.NewGliffyDiagramConverter(gliffyJSON)
+ if err != nil {
+ log.Printf("[convert] failed to initialize converter: %v", err)
+ return "", "", err
+ }
+ xml, err := converter.GraphXML()
+ if err != nil {
+ log.Printf("[convert] failed to generate XML: %v", err)
+ return "", "", err
+ }
+ warn := ""
+ log.Printf("[convert] done, output bytes: %d", len(xml))
+ return xml, warn, nil
+}
+
+func safeName(name string) string {
+ if name == "" {
+ return "diagram"
+ }
+ dot := strings.LastIndex(name, ".")
+ if dot > 0 {
+ name = name[:dot]
+ }
+ name = strings.ReplaceAll(name, "\"", "")
+ return name
+}
+
+func openAPISpecHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(openAPISpec))
+}
+
+func swaggerUIHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ swaggerHTML := `
+
+
+ Gliffy → draw.io API
+
+
+
+
+
+
+
+`
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write([]byte(swaggerHTML))
+}
+
+var openAPISpec = `
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Gliffy to draw.io Converter",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/api/convert": {
+ "post": {
+ "summary": "Convert Gliffy JSON to draw.io XML",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "string",
+ "description": "Raw Gliffy JSON content"
+ }
+ },
+ "required": ["data"]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Conversion succeeded",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "xml": { "type": "string" },
+ "warning": { "type": "string" }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Conversion failed"
+ }
+ }
+ }
+ }
+ }
+}
+`
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..6178ee9
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3.9"
+
+services:
+ gliffy2drawio:
+ build: .
+ ports:
+ - "8080:8080"
+ restart: unless-stopped
diff --git a/server b/server
new file mode 100755
index 0000000..46c2553
Binary files /dev/null and b/server differ