Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37d02c6a7 | ||
|
|
849a3a2f0a | ||
|
|
33f80ef0d8 | ||
|
|
d1b333d7df | ||
|
|
e5d3225709 | ||
|
|
812bfba433 | ||
|
|
6bbe9f5f92 | ||
|
|
6ae4d0fd58 | ||
|
|
febfc84fab | ||
|
|
eb4571ef23 | ||
|
|
10fa5a0e28 | ||
|
|
d52a5ad579 | ||
|
|
6508b25d50 | ||
|
|
07d10fc197 | ||
|
|
a99ff22c5e | ||
|
|
da00637d41 | ||
|
|
a71f6decd7 | ||
|
|
14b7031d12 | ||
|
|
2b8079cd5b | ||
|
|
2d503ced38 | ||
|
|
3d415c5ab9 | ||
|
|
3cf48080b5 | ||
|
|
a6e62da53a | ||
|
|
9d365f94fe | ||
|
|
3a40f31774 | ||
|
|
36e077b516 | ||
|
|
327543ebd8 | ||
|
|
6da130e231 | ||
|
|
510c861e17 | ||
|
|
ba9d03c873 | ||
|
|
00f2df4f81 | ||
|
|
67cef06deb | ||
|
|
f1f12e3731 | ||
|
|
f13db848b1 | ||
|
|
5b7b545227 | ||
|
|
b181e122f8 | ||
|
|
f63a4d97f6 | ||
|
|
5068c588d5 | ||
|
|
a6ebc88210 | ||
|
|
85b3855a3b | ||
|
|
07b2be93ab | ||
|
|
702fdc5524 | ||
|
|
b0c44b76a2 | ||
|
|
5cfa2c77ad | ||
|
|
ad0177a0df | ||
|
|
358c476bc5 | ||
|
|
7781c4827e | ||
|
|
284da03a2c | ||
|
|
4ead1ea02e | ||
|
|
65a132cdec | ||
|
|
c23c29bd9e | ||
|
|
d252a491e4 | ||
|
|
0b061ec570 | ||
|
|
e51d488c9f | ||
|
|
ba7c8be38a | ||
|
|
047e47c138 | ||
|
|
dc5bca1962 | ||
|
|
750e8c7ea9 | ||
|
|
a35e3d7085 | ||
|
|
f7459c7d89 | ||
|
|
90359524c5 | ||
|
|
f95701f228 | ||
|
|
19a86c6774 | ||
|
|
20b9a399b0 | ||
|
|
65ceba9e32 | ||
|
|
65bbc334f5 | ||
|
|
ae587caf8e | ||
|
|
57684645cc | ||
|
|
57cfd74df7 | ||
|
|
4bd13d8620 | ||
|
|
5d8b7c961b | ||
|
|
ec2ff8dc25 | ||
|
|
baa1327214 | ||
|
|
44f409a807 | ||
|
|
8b9a6a3f7d | ||
|
|
aac3f97894 | ||
|
|
bfa4d5076b | ||
|
|
90c34db593 | ||
|
|
9d6c6cecc7 |
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /mermaidcli
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
11
.github/workflows/build.yaml
vendored
11
.github/workflows/build.yaml
vendored
@@ -7,19 +7,16 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.13.x]
|
go-version: [1.16.x]
|
||||||
platform: [ubuntu-latest]
|
platform: [ubuntu-latest]
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v3
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
|
run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV
|
||||||
- name: Build
|
- name: Build
|
||||||
run: docker build \
|
run: docker build -t tomwright/mermaid-server:latest -t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} -f Dockerfile .
|
||||||
-t tomwright/mermaid-server:latest \
|
|
||||||
-t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} \
|
|
||||||
-f Dockerfile .
|
|
||||||
- name: Login
|
- name: Login
|
||||||
run: echo ${{ secrets.DOCKER_PASS }} | docker login -u${{ secrets.DOCKER_USER }} --password-stdin
|
run: echo ${{ secrets.DOCKER_PASS }} | docker login -u${{ secrets.DOCKER_USER }} --password-stdin
|
||||||
- name: Push
|
- name: Push
|
||||||
|
|||||||
8
.github/workflows/test.yaml
vendored
8
.github/workflows/test.yaml
vendored
@@ -4,17 +4,17 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.13.x]
|
go-version: [1.16.x]
|
||||||
platform: [ubuntu-latest]
|
platform: [ubuntu-latest]
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v3
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v3.0.4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# This stage builds the go executable.
|
# This stage builds the go executable.
|
||||||
FROM golang:1.13-buster as go
|
FROM golang:1.18.3-buster as go
|
||||||
|
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
@@ -10,7 +10,7 @@ RUN go build -o bin/app cmd/app/main.go
|
|||||||
# Final stage that will be pushed.
|
# Final stage that will be pushed.
|
||||||
FROM debian:buster-slim
|
FROM debian:buster-slim
|
||||||
|
|
||||||
FROM node:12.12.0-buster-slim as node
|
FROM node:18.5.0-buster-slim as node
|
||||||
|
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ RUN apt-get update 2>/dev/null && \
|
|||||||
lsb-release \
|
lsb-release \
|
||||||
xdg-utils \
|
xdg-utils \
|
||||||
wget \
|
wget \
|
||||||
|
libxshmfence1 \
|
||||||
2>/dev/null
|
2>/dev/null
|
||||||
|
|
||||||
COPY --from=go /root/bin/app ./app
|
COPY --from=go /root/bin/app ./app
|
||||||
@@ -73,5 +74,5 @@ RUN mkdir -p ./out
|
|||||||
RUN chmod 0777 ./in
|
RUN chmod 0777 ./in
|
||||||
RUN chmod 0777 ./out
|
RUN chmod 0777 ./out
|
||||||
|
|
||||||
CMD ["./app", "--mermaid=./node_modules/.bin/mmdc", "--in=./in", "--out=./out", "--puppeteer=./puppeteer-config.json"]
|
CMD ["./app", "--mermaid=./node_modules/.bin/mmdc", "--in=./in", "--out=./out", "--puppeteer=./puppeteer-config.json", "--allow-all-origins=true"]
|
||||||
|
|
||||||
|
|||||||
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
DOCKER_IMAGE=tomwright/mermaid-server:latest
|
||||||
|
CONTAINER_NAME=mermaid-server
|
||||||
|
|
||||||
|
docker-image:
|
||||||
|
docker build -t ${DOCKER_IMAGE} .
|
||||||
|
|
||||||
|
docker-run:
|
||||||
|
docker run -d --name ${CONTAINER_NAME} -p 80:80 ${DOCKER_IMAGE}
|
||||||
|
|
||||||
|
docker-stop:
|
||||||
|
docker stop ${CONTAINER_NAME} || true
|
||||||
|
|
||||||
|
docker-rm:
|
||||||
|
make docker-stop
|
||||||
|
docker rm ${CONTAINER_NAME} || true
|
||||||
|
|
||||||
|
docker-logs:
|
||||||
|
docker logs -f ${CONTAINER_NAME}
|
||||||
|
|
||||||
|
docker-push:
|
||||||
|
docker push ${DOCKER_IMAGE}
|
||||||
@@ -10,7 +10,7 @@ While this currently serves the diagrams via HTTP, it could easily be manipulate
|
|||||||
|
|
||||||
Run the container:
|
Run the container:
|
||||||
```
|
```
|
||||||
docker run -d --name mermaid-server -p 80:80 tomwright/mermaid-server:latest
|
docker run -d --name mermaid-server -p 80:80 tomwright/mermaid-server:latest --allow-all-origins=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manually as a go command
|
### Manually as a go command
|
||||||
@@ -22,6 +22,8 @@ go run cmd/app/main.go --mermaid=./mermaidcli/node_modules/.bin/mmdc --in=./in -
|
|||||||
|
|
||||||
### Diagram creation
|
### Diagram creation
|
||||||
|
|
||||||
|
Use the query param 'type' to change between 'png' and 'svg' defaults to 'svg'.
|
||||||
|
|
||||||
#### POST
|
#### POST
|
||||||
|
|
||||||
Send a CURL request to generate a diagram via `POST`:
|
Send a CURL request to generate a diagram via `POST`:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/tomwright/lifetime"
|
"github.com/tomwright/grace"
|
||||||
"github.com/tomwright/mermaid-server/internal"
|
"github.com/tomwright/mermaid-server/internal"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -14,6 +14,7 @@ func main() {
|
|||||||
in := flag.String("in", "", "Directory to store input files.")
|
in := flag.String("in", "", "Directory to store input files.")
|
||||||
out := flag.String("out", "", "Directory to store output files.")
|
out := flag.String("out", "", "Directory to store output files.")
|
||||||
puppeteer := flag.String("puppeteer", "", "Full path to optional puppeteer config.")
|
puppeteer := flag.String("puppeteer", "", "Full path to optional puppeteer config.")
|
||||||
|
allowAllOrigins := flag.Bool("allow-all-origins", false, "True to allow all request origins")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *mermaid == "" {
|
if *mermaid == "" {
|
||||||
@@ -31,19 +32,16 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g := grace.Init(context.Background())
|
||||||
|
|
||||||
cache := internal.NewDiagramCache()
|
cache := internal.NewDiagramCache()
|
||||||
generator := internal.NewGenerator(cache, *mermaid, *in, *out, *puppeteer)
|
generator := internal.NewGenerator(cache, *mermaid, *in, *out, *puppeteer)
|
||||||
|
|
||||||
httpService := internal.NewHTTPService(generator)
|
httpRunner := internal.NewHTTPRunner(generator, *allowAllOrigins)
|
||||||
cleanupService := internal.NewCleanupService(generator)
|
cleanupRunner := internal.NewCleanupRunner(generator)
|
||||||
|
|
||||||
lt := lifetime.New(context.Background()).Init()
|
g.Run(httpRunner)
|
||||||
|
g.Run(cleanupRunner)
|
||||||
|
|
||||||
// Start the http service.
|
g.Wait()
|
||||||
lt.Start(httpService)
|
|
||||||
// Start the cleanup service.
|
|
||||||
lt.Start(cleanupService)
|
|
||||||
|
|
||||||
// Wait for all routines to stop running.
|
|
||||||
lt.Wait()
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -1,5 +1,8 @@
|
|||||||
module github.com/tomwright/mermaid-server
|
module github.com/tomwright/mermaid-server
|
||||||
|
|
||||||
go 1.13
|
go 1.15
|
||||||
|
|
||||||
require github.com/tomwright/lifetime v1.0.0
|
require (
|
||||||
|
github.com/tomwright/grace v0.1.2
|
||||||
|
github.com/tomwright/gracehttpserverrunner v0.1.0
|
||||||
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,2 +1,4 @@
|
|||||||
github.com/tomwright/lifetime v1.0.0 h1:Yzj+Td38eUUdZ1ewvOegywFBmKyaCh+8HjKBmeXw6OM=
|
github.com/tomwright/grace v0.1.2 h1:8kH+S2GLqnwgWqUzi9CcjNoWJANZQnw9Xw65NPUr6WA=
|
||||||
github.com/tomwright/lifetime v1.0.0/go.mod h1:GUCHgRaR/zStvtJiOd3B4gIZayeiz3TgApC9kNYAOQI=
|
github.com/tomwright/grace v0.1.2/go.mod h1:RKqz4gB3sQJpyas/CuiiriQQfUxSXhtWRfYtE7MG+Ok=
|
||||||
|
github.com/tomwright/gracehttpserverrunner v0.1.0 h1:n4iafOnJQEmRn05i1QzU+FPS0CU4ybxilyEdBEH/Ulk=
|
||||||
|
github.com/tomwright/gracehttpserverrunner v0.1.0/go.mod h1:FFHjVUgXu7KygMn+QlaoCesVlPOhaCnCvw35nvgzt5I=
|
||||||
|
|||||||
46
internal/cleanup.go
Normal file
46
internal/cleanup.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/tomwright/grace"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCleanupRunner returns a runner that can be used cleanup old diagrams.
|
||||||
|
func NewCleanupRunner(generator Generator) grace.Runner {
|
||||||
|
return &cleanupService{
|
||||||
|
generator: generator,
|
||||||
|
runEvery: time.Minute * 5,
|
||||||
|
cleanupLast: time.Hour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupService is a runner that is used cleanup old diagrams.
|
||||||
|
type cleanupService struct {
|
||||||
|
generator Generator
|
||||||
|
runEvery time.Duration
|
||||||
|
cleanupLast time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the cleanup process.
|
||||||
|
func (s *cleanupService) Run(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.generator.CleanUp(s.cleanupLast); err != nil {
|
||||||
|
log.Printf("error when cleaning up: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(s.runEvery):
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewCleanupService returns a service that can be used cleanup old diagrams.
|
|
||||||
func NewCleanupService(generator Generator) *cleanupService {
|
|
||||||
return &cleanupService{
|
|
||||||
generator: generator,
|
|
||||||
stopCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupService is a service that can be used cleanup old diagrams.
|
|
||||||
type cleanupService struct {
|
|
||||||
generator Generator
|
|
||||||
stopCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the cleanup service.
|
|
||||||
func (s *cleanupService) Start() error {
|
|
||||||
for {
|
|
||||||
if err := s.generator.CleanUp(time.Hour); err != nil {
|
|
||||||
log.Printf("error when cleaning up: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Minute * 5):
|
|
||||||
continue
|
|
||||||
case <-s.stopCh:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the cleanup service.
|
|
||||||
func (s *cleanupService) Stop() {
|
|
||||||
close(s.stopCh)
|
|
||||||
}
|
|
||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewDiagram returns a new diagram.
|
// NewDiagram returns a new diagram.
|
||||||
func NewDiagram(description []byte) *Diagram {
|
func NewDiagram(description []byte, imgType string) *Diagram {
|
||||||
return &Diagram{
|
return &Diagram{
|
||||||
description: []byte(strings.TrimSpace(string(description))),
|
description: []byte(strings.TrimSpace(string(description))),
|
||||||
lastTouched: time.Now(),
|
lastTouched: time.Now(),
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
|
imgType: imgType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ type Diagram struct {
|
|||||||
mu *sync.RWMutex
|
mu *sync.RWMutex
|
||||||
// lastTouched is the time that the diagram was last used.
|
// lastTouched is the time that the diagram was last used.
|
||||||
lastTouched time.Time
|
lastTouched time.Time
|
||||||
|
// the type of image to generate svg or png
|
||||||
|
imgType string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch updates the last touched time of the diagram.
|
// Touch updates the last touched time of the diagram.
|
||||||
@@ -55,7 +58,7 @@ func (d *Diagram) ID() (string, error) {
|
|||||||
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(d.description)
|
encoded := base64.StdEncoding.EncodeToString(d.description)
|
||||||
hash := md5.Sum([]byte(encoded))
|
hash := md5.Sum([]byte(encoded))
|
||||||
d.id = hex.EncodeToString(hash[:])
|
d.id = hex.EncodeToString(hash[:]) + d.imgType
|
||||||
|
|
||||||
return d.id, nil
|
return d.id, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (c cachingGenerator) generate(diagram *Diagram) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inPath := fmt.Sprintf("%s/%s.mmd", c.inPath, id)
|
inPath := fmt.Sprintf("%s/%s.mmd", c.inPath, id)
|
||||||
outPath := fmt.Sprintf("%s/%s.svg", c.outPath, id)
|
outPath := fmt.Sprintf("%s/%s.%s", c.outPath, id, diagram.imgType)
|
||||||
|
|
||||||
if err := ioutil.WriteFile(inPath, diagram.description, 0644); err != nil {
|
if err := ioutil.WriteFile(inPath, diagram.description, 0644); err != nil {
|
||||||
return fmt.Errorf("could not write to input file [%s]: %w", inPath, err)
|
return fmt.Errorf("could not write to input file [%s]: %w", inPath, err)
|
||||||
|
|||||||
104
internal/http.go
104
internal/http.go
@@ -3,13 +3,48 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/tomwright/grace"
|
||||||
|
"github.com/tomwright/gracehttpserverrunner"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewHTTPRunner returns a grace runner that runs a HTTP server.
|
||||||
|
func NewHTTPRunner(generator Generator, allowAllOrigins bool) grace.Runner {
|
||||||
|
httpHandler := generateHTTPHandler(generator)
|
||||||
|
|
||||||
|
if allowAllOrigins {
|
||||||
|
httpHandler = allowAllOriginsMiddleware(httpHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := http.NewServeMux()
|
||||||
|
r.Handle("/generate", httpHandler)
|
||||||
|
|
||||||
|
return &gracehttpserverrunner.HTTPServerRunner{
|
||||||
|
Server: &http.Server{
|
||||||
|
Addr: ":80",
|
||||||
|
Handler: r,
|
||||||
|
},
|
||||||
|
ShutdownTimeout: time.Second * 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowAllOriginsMiddleware sets appropriate CORS headers to allow requests from any origin.
|
||||||
|
func allowAllOriginsMiddleware(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
origin = "*"
|
||||||
|
}
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
|
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
|
||||||
bytes, err := json.Marshal(value)
|
bytes, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -22,12 +57,20 @@ func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSVG(rw http.ResponseWriter, data []byte, status int) {
|
func writeImage(rw http.ResponseWriter, data []byte, status int, imgType string) error {
|
||||||
|
switch imgType {
|
||||||
|
case "png":
|
||||||
|
rw.Header().Set("Content-Type", "image/png")
|
||||||
|
case "svg":
|
||||||
rw.Header().Set("Content-Type", "image/svg+xml")
|
rw.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled image type: %s", imgType)
|
||||||
|
}
|
||||||
rw.WriteHeader(status)
|
rw.WriteHeader(status)
|
||||||
if _, err := rw.Write(data); err != nil {
|
if _, err := rw.Write(data); err != nil {
|
||||||
panic("could not write bytes to response: " + err.Error())
|
return fmt.Errorf("could not write image bytes: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeErr(rw http.ResponseWriter, err error, status int) {
|
func writeErr(rw http.ResponseWriter, err error, status int) {
|
||||||
@@ -41,59 +84,72 @@ func writeErr(rw http.ResponseWriter, err error, status int) {
|
|||||||
// URLParam is the URL parameter getDiagramFromGET uses to look for data.
|
// URLParam is the URL parameter getDiagramFromGET uses to look for data.
|
||||||
const URLParam = "data"
|
const URLParam = "data"
|
||||||
|
|
||||||
func getDiagramFromGET(rw http.ResponseWriter, r *http.Request) *Diagram {
|
func getDiagramFromGET(r *http.Request, imgType string) (*Diagram, error) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeErr(rw, fmt.Errorf("expected HTTP method GET"), http.StatusBadRequest)
|
return nil, fmt.Errorf("expected HTTP method GET")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queryVal := strings.TrimSpace(r.URL.Query().Get(URLParam))
|
queryVal := strings.TrimSpace(r.URL.Query().Get(URLParam))
|
||||||
if queryVal == "" {
|
if queryVal == "" {
|
||||||
writeErr(rw, fmt.Errorf("missing data"), http.StatusBadRequest)
|
return nil, fmt.Errorf("missing data")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
data, err := url.QueryUnescape(queryVal)
|
data, err := url.QueryUnescape(queryVal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(rw, fmt.Errorf("could not read query param: %s", err), http.StatusBadRequest)
|
return nil, fmt.Errorf("could not read query param: %s", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a diagram from the description
|
// Create a diagram from the description
|
||||||
d := NewDiagram([]byte(data))
|
d := NewDiagram([]byte(data), imgType)
|
||||||
return d
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDiagramFromPOST(rw http.ResponseWriter, r *http.Request) *Diagram {
|
func getDiagramFromPOST(r *http.Request, imgType string) (*Diagram, error) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeErr(rw, fmt.Errorf("expected HTTP method POST"), http.StatusBadRequest)
|
return nil, fmt.Errorf("expected HTTP method POST")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
// Get description from request body
|
// Get description from request body
|
||||||
bytes, err := ioutil.ReadAll(r.Body)
|
bytes, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(rw, fmt.Errorf("could not read body: %s", err), http.StatusInternalServerError)
|
return nil, fmt.Errorf("could not read body: %s", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a diagram from the description
|
// Create a diagram from the description
|
||||||
d := NewDiagram(bytes)
|
d := NewDiagram(bytes, imgType)
|
||||||
return d
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const URLParamImageType = "type"
|
||||||
|
|
||||||
// generateHTTPHandler returns a HTTP handler used to generate a diagram.
|
// generateHTTPHandler returns a HTTP handler used to generate a diagram.
|
||||||
func generateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *http.Request) {
|
func generateHTTPHandler(generator Generator) http.Handler {
|
||||||
return func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
var diagram *Diagram
|
var diagram *Diagram
|
||||||
|
|
||||||
|
imgType := r.URL.Query().Get(URLParamImageType)
|
||||||
|
|
||||||
|
switch imgType {
|
||||||
|
case "png", "svg":
|
||||||
|
case "":
|
||||||
|
imgType = "svg"
|
||||||
|
default:
|
||||||
|
writeErr(rw, fmt.Errorf("unsupported image type (%s) use svg or png", imgType), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
diagram = getDiagramFromGET(rw, r)
|
diagram, err = getDiagramFromGET(r, imgType)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
diagram = getDiagramFromPOST(rw, r)
|
diagram, err = getDiagramFromPOST(r, imgType)
|
||||||
default:
|
default:
|
||||||
writeErr(rw, fmt.Errorf("unexpected HTTP method %s", r.Method), http.StatusBadRequest)
|
writeErr(rw, fmt.Errorf("unexpected HTTP method %s", r.Method), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeErr(rw, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if diagram == nil {
|
if diagram == nil {
|
||||||
writeErr(rw, fmt.Errorf("could not create diagram"), http.StatusInternalServerError)
|
writeErr(rw, fmt.Errorf("could not create diagram"), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -112,6 +168,8 @@ func generateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *ht
|
|||||||
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
|
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeSVG(rw, diagramBytes, http.StatusOK)
|
if err := writeImage(rw, diagramBytes, http.StatusOK, imgType); err != nil {
|
||||||
|
writeErr(rw, fmt.Errorf("could not write diagram: %w", err), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewHTTPService returns a service that can be used to start a http server
|
|
||||||
// that will generate diagrams.
|
|
||||||
func NewHTTPService(generator Generator) *httpService {
|
|
||||||
return &httpService{
|
|
||||||
generator: generator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpService is a service that can be used to start a http server
|
|
||||||
// that will generate diagrams.
|
|
||||||
type httpService struct {
|
|
||||||
httpServer *http.Server
|
|
||||||
generator Generator
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the HTTP server.
|
|
||||||
func (s *httpService) Start() error {
|
|
||||||
httpHandler := generateHTTPHandler(s.generator)
|
|
||||||
|
|
||||||
r := http.NewServeMux()
|
|
||||||
r.Handle("/generate", http.HandlerFunc(httpHandler))
|
|
||||||
|
|
||||||
s.httpServer = &http.Server{
|
|
||||||
Addr: ":80",
|
|
||||||
Handler: r,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
if err != http.ErrServerClosed {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *httpService) Stop() {
|
|
||||||
if s != nil {
|
|
||||||
_ = s.httpServer.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
911
mermaidcli/package-lock.json
generated
911
mermaidcli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mermaid-js/mermaid-cli": "^8.6.4"
|
"@mermaid-js/mermaid-cli": "^9.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user