2 Commits

Author SHA1 Message Date
Tom Wright
93ca2b4966 Update build workflow 2020-08-09 11:44:25 +01:00
Tom Wright
7f9e6c91bd Update build workflow 2020-08-09 11:41:47 +01:00
17 changed files with 267 additions and 3433 deletions

View File

@@ -1,18 +0,0 @@
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

View File

@@ -7,16 +7,16 @@ jobs:
build:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.13.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@v2.3.4
uses: actions/checkout@v1
- name: Set env
run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- name: Build
run: docker build -t tomwright/mermaid-server:latest -t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} -f Dockerfile .
run: docker build --ssh default -t tomwright/mermaid-server:latest -t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} -f Dockerfile .
- name: Login
run: echo ${{ secrets.DOCKER_PASS }} | docker login -u${{ secrets.DOCKER_USER }} --password-stdin
- name: Push

View File

@@ -4,17 +4,17 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.13.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2.1.3
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2.3.4
- uses: actions/cache@v2.1.6
uses: actions/checkout@v1
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

View File

@@ -1,5 +1,5 @@
# This stage builds the go executable.
FROM golang:1.16.7-buster as go
FROM golang:1.13-buster as go
WORKDIR /root
COPY ./ ./
@@ -10,7 +10,7 @@ RUN go build -o bin/app cmd/app/main.go
# Final stage that will be pushed.
FROM debian:buster-slim
FROM node:16.6.2-buster-slim as node
FROM node:12.12.0-buster-slim as node
WORKDIR /root
@@ -64,7 +64,6 @@ RUN apt-get update 2>/dev/null && \
lsb-release \
xdg-utils \
wget \
libxshmfence1 \
2>/dev/null
COPY --from=go /root/bin/app ./app

View File

@@ -1,21 +0,0 @@
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}

View File

@@ -22,8 +22,6 @@ go run cmd/app/main.go --mermaid=./mermaidcli/node_modules/.bin/mmdc --in=./in -
### Diagram creation
Use the query param 'type' to change between 'png' and 'svg' defaults to 'svg'.
#### POST
Send a CURL request to generate a diagram via `POST`:

View File

@@ -4,7 +4,7 @@ import (
"context"
"flag"
"fmt"
"github.com/tomwright/grace"
"github.com/tomwright/lifetime"
"github.com/tomwright/mermaid-server/internal"
"os"
)
@@ -31,16 +31,19 @@ func main() {
os.Exit(1)
}
g := grace.Init(context.Background())
cache := internal.NewDiagramCache()
generator := internal.NewGenerator(cache, *mermaid, *in, *out, *puppeteer)
httpRunner := internal.NewHTTPRunner(generator)
cleanupRunner := internal.NewCleanupRunner(generator)
httpService := internal.NewHTTPService(generator)
cleanupService := internal.NewCleanupService(generator)
g.Run(httpRunner)
g.Run(cleanupRunner)
lt := lifetime.New(context.Background()).Init()
g.Wait()
// Start the http service.
lt.Start(httpService)
// Start the cleanup service.
lt.Start(cleanupService)
// Wait for all routines to stop running.
lt.Wait()
}

7
go.mod
View File

@@ -1,8 +1,5 @@
module github.com/tomwright/mermaid-server
go 1.15
go 1.13
require (
github.com/tomwright/grace v0.1.2
github.com/tomwright/gracehttpserverrunner v0.1.0
)
require github.com/tomwright/lifetime v1.0.0

6
go.sum
View File

@@ -1,4 +1,2 @@
github.com/tomwright/grace v0.1.2 h1:8kH+S2GLqnwgWqUzi9CcjNoWJANZQnw9Xw65NPUr6WA=
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=
github.com/tomwright/lifetime v1.0.0 h1:Yzj+Td38eUUdZ1ewvOegywFBmKyaCh+8HjKBmeXw6OM=
github.com/tomwright/lifetime v1.0.0/go.mod h1:GUCHgRaR/zStvtJiOd3B4gIZayeiz3TgApC9kNYAOQI=

View File

@@ -1,46 +0,0 @@
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
}
}
}

View File

@@ -0,0 +1,41 @@
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)
}

View File

@@ -10,12 +10,11 @@ import (
)
// NewDiagram returns a new diagram.
func NewDiagram(description []byte, imgType string) *Diagram {
func NewDiagram(description []byte) *Diagram {
return &Diagram{
description: []byte(strings.TrimSpace(string(description))),
lastTouched: time.Now(),
mu: &sync.RWMutex{},
imgType: imgType,
}
}
@@ -31,8 +30,6 @@ type Diagram struct {
mu *sync.RWMutex
// lastTouched is the time that the diagram was last used.
lastTouched time.Time
// the type of image to generate svg or png
imgType string
}
// Touch updates the last touched time of the diagram.
@@ -58,7 +55,7 @@ func (d *Diagram) ID() (string, error) {
encoded := base64.StdEncoding.EncodeToString(d.description)
hash := md5.Sum([]byte(encoded))
d.id = hex.EncodeToString(hash[:]) + d.imgType
d.id = hex.EncodeToString(hash[:])
return d.id, nil
}

View File

@@ -79,7 +79,7 @@ func (c cachingGenerator) generate(diagram *Diagram) error {
}
inPath := fmt.Sprintf("%s/%s.mmd", c.inPath, id)
outPath := fmt.Sprintf("%s/%s.%s", c.outPath, id, diagram.imgType)
outPath := fmt.Sprintf("%s/%s.svg", c.outPath, id)
if err := ioutil.WriteFile(inPath, diagram.description, 0644); err != nil {
return fmt.Errorf("could not write to input file [%s]: %w", inPath, err)

View File

@@ -3,32 +3,13 @@ package internal
import (
"encoding/json"
"fmt"
"github.com/tomwright/grace"
"github.com/tomwright/gracehttpserverrunner"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)
// NewHTTPRunner returns a grace runner that runs a HTTP server.
func NewHTTPRunner(generator Generator) grace.Runner {
httpHandler := generateHTTPHandler(generator)
r := http.NewServeMux()
r.Handle("/generate", http.HandlerFunc(httpHandler))
return &gracehttpserverrunner.HTTPServerRunner{
Server: &http.Server{
Addr: ":80",
Handler: r,
},
ShutdownTimeout: time.Second * 5,
}
}
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
bytes, err := json.Marshal(value)
if err != nil {
@@ -41,20 +22,12 @@ func writeJSON(rw http.ResponseWriter, value interface{}, 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")
default:
return fmt.Errorf("unhandled image type: %s", imgType)
}
func writeSVG(rw http.ResponseWriter, data []byte, status int) {
rw.Header().Set("Content-Type", "image/svg+xml")
rw.WriteHeader(status)
if _, err := rw.Write(data); err != nil {
return fmt.Errorf("could not write image bytes: %w", err)
panic("could not write bytes to response: " + err.Error())
}
return nil
}
func writeErr(rw http.ResponseWriter, err error, status int) {
@@ -68,72 +41,59 @@ func writeErr(rw http.ResponseWriter, err error, status int) {
// URLParam is the URL parameter getDiagramFromGET uses to look for data.
const URLParam = "data"
func getDiagramFromGET(r *http.Request, imgType string) (*Diagram, error) {
func getDiagramFromGET(rw http.ResponseWriter, r *http.Request) *Diagram {
if r.Method != http.MethodGet {
return nil, fmt.Errorf("expected HTTP method GET")
writeErr(rw, fmt.Errorf("expected HTTP method GET"), http.StatusBadRequest)
return nil
}
queryVal := strings.TrimSpace(r.URL.Query().Get(URLParam))
if queryVal == "" {
return nil, fmt.Errorf("missing data")
writeErr(rw, fmt.Errorf("missing data"), http.StatusBadRequest)
return nil
}
data, err := url.QueryUnescape(queryVal)
if err != nil {
return nil, fmt.Errorf("could not read query param: %s", err)
writeErr(rw, fmt.Errorf("could not read query param: %s", err), http.StatusBadRequest)
return nil
}
// Create a diagram from the description
d := NewDiagram([]byte(data), imgType)
return d, nil
d := NewDiagram([]byte(data))
return d
}
func getDiagramFromPOST(r *http.Request, imgType string) (*Diagram, error) {
func getDiagramFromPOST(rw http.ResponseWriter, r *http.Request) *Diagram {
if r.Method != http.MethodPost {
return nil, fmt.Errorf("expected HTTP method POST")
writeErr(rw, fmt.Errorf("expected HTTP method POST"), http.StatusBadRequest)
return nil
}
// Get description from request body
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("could not read body: %s", err)
writeErr(rw, fmt.Errorf("could not read body: %s", err), http.StatusInternalServerError)
return nil
}
// Create a diagram from the description
d := NewDiagram(bytes, imgType)
return d, nil
d := NewDiagram(bytes)
return d
}
const URLParamImageType = "type"
// generateHTTPHandler returns a HTTP handler used to generate a diagram.
func generateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *http.Request) {
return func(rw http.ResponseWriter, r *http.Request) {
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 {
case http.MethodGet:
diagram, err = getDiagramFromGET(r, imgType)
diagram = getDiagramFromGET(rw, r)
case http.MethodPost:
diagram, err = getDiagramFromPOST(r, imgType)
diagram = getDiagramFromPOST(rw, r)
default:
writeErr(rw, fmt.Errorf("unexpected HTTP method %s", r.Method), http.StatusBadRequest)
return
}
if err != nil {
writeErr(rw, err, http.StatusBadRequest)
return
}
if diagram == nil {
writeErr(rw, fmt.Errorf("could not create diagram"), http.StatusInternalServerError)
return
@@ -152,8 +112,6 @@ func generateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *ht
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
return
}
if err := writeImage(rw, diagramBytes, http.StatusOK, imgType); err != nil {
writeErr(rw, fmt.Errorf("could not write diagram: %w", err), http.StatusInternalServerError)
}
writeSVG(rw, diagramBytes, http.StatusOK)
}
}

47
internal/http_service.go Normal file
View File

@@ -0,0 +1,47 @@
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()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"@mermaid-js/mermaid-cli": "^8.11.4"
"@mermaid-js/mermaid-cli": "^8.6.4"
}
}