/
serve.go
205 lines (179 loc) · 5.23 KB
/
serve.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package main
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path"
"strings"
"sync"
"time"
"goa.design/model/mdl"
)
type (
// Server implements a HTTP server with 4 endpoints:
//
// * GET requests to "/" return the diagram editor single page app implemented in the "webapp" directory.
// * GET requests to "/data/model.json" return the JSON representation of the architecture model.
// * GET requests to "/data/layout.json" return the view element positions indexed by view id.
// * POST requests to "/data/save?id=<ID>" saves the SVG representation for the view with the given id.
// The request body must be a JSON representation of a SavedView data structure.
//
// Server is intended to provide the backend for the model single page app diagram editor.
Server struct {
design []byte
lock sync.Mutex
}
// Layout is position info saved for one view (diagram)
Layout = map[string]any
)
// NewServer created a server that serves the given design.
func NewServer(d *mdl.Design) *Server {
var s Server
s.SetDesign(d)
return &s
}
//go:embed webapp/dist/*
var distFS embed.FS
// Serve starts the HTTP server on localhost with the given port. outDir
// indicates where the view data structures are located. If devmode is true then
// the single page app is served directly from the source under the "webapp"
// directory. Otherwise, it is served from the code embedded in the Go executable.
func (s *Server) Serve(outDir string, devmode bool, port int) error {
if devmode {
// in devmode (go run), serve the webapp from filesystem
fs := http.FileSystem(http.Dir("./cmd/mdl/webapp/dist"))
http.Handle("/", http.FileServer(fs))
} else {
sub, _ := fs.Sub(distFS, "webapp/dist")
http.Handle("/", http.FileServer(http.FS(sub)))
}
http.HandleFunc("/data/model.json", func(w http.ResponseWriter, _ *http.Request) {
s.lock.Lock()
defer s.lock.Unlock()
_, _ = w.Write(s.design)
})
http.HandleFunc("/data/layout.json", func(w http.ResponseWriter, _ *http.Request) {
s.lock.Lock()
defer s.lock.Unlock()
b, err := loadLayouts(outDir)
if err != nil {
fmt.Println(err)
} else {
_, _ = w.Write(b)
}
})
http.HandleFunc("/data/save", func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
handleError(w, fmt.Errorf("missing id"))
return
}
s.lock.Lock()
defer s.lock.Unlock()
svgFile := path.Join(outDir, id+".svg")
f, err := os.Create(svgFile)
if err != nil {
handleError(w, err)
return
}
defer func() { _ = f.Close() }()
_, _ = io.Copy(f, r.Body)
w.WriteHeader(http.StatusAccepted)
})
server := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
ReadHeaderTimeout: 3 * time.Second,
}
// start the server
fmt.Printf("Editor started. Open http://localhost:%d in your browser.\n", port)
return server.ListenAndServe()
}
// SetDesign updates the design served by s.
//
// Note: it would have been more efficient to use the raw bytes read from the
// generated file instead of going through the unmarshal/marshal cycle however
// this approach is safer, makes it clearer and easier to compose. Also it is
// not expected that the model would need to be updated often.
func (s *Server) SetDesign(d *mdl.Design) {
b, err := json.Marshal(d)
if err != nil {
panic("failed to serialize design: " + err.Error()) // bug
}
s.lock.Lock()
defer s.lock.Unlock()
s.design = b
}
// handleError writes the given error to stderr and http.Error.
func handleError(w http.ResponseWriter, err error) {
fmt.Fprintln(os.Stderr, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// loadLayouts lists out directory and reads layout info from SVG files
// for backwards compatibility, fallback to layout.json
func loadLayouts(dir string) ([]byte, error) {
beginMark := []byte("<script type=\"application/json\"><![CDATA[")
endMark := []byte("]]></script>")
// first, read the fallback layout.json, then merge individual layouts from SVGs
layouts := make(map[string]Layout)
lj := path.Join(dir, "layout.json")
if fileExists(lj) {
b, err := os.ReadFile(lj)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &layouts)
if err != nil {
return nil, err
}
}
var svgFiles []string
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, f := range files {
if !f.IsDir() && strings.HasSuffix(f.Name(), ".svg") {
svgFiles = append(svgFiles, f.Name())
}
}
for _, file := range svgFiles {
b, err := os.ReadFile(path.Join(dir, file))
if err != nil {
return nil, err
}
// look for the first script block
begin := bytes.Index(b, beginMark) + len(beginMark)
end := bytes.Index(b, endMark)
b = b[begin:end]
l := make(map[string]any)
err = json.Unmarshal(b, &l)
if err != nil {
return nil, err
}
id := file[:len(file)-4]
if l["layout"] != nil {
layouts[id] = l["layout"].(Layout)
}
}
b, err := json.Marshal(layouts)
if err != nil {
return nil, err
}
return b, nil
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
// in case of any other error, like permission denied, let the user know
if err != nil {
fail("Can't read FileInfo: %s", err)
}
return !info.IsDir()
}