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