From b8db0fb9cbd5231c25389715d975f575f4c66a37 Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Tue, 1 Oct 2024 11:35:18 +0200 Subject: [PATCH 1/9] Embed resources into go binary for portability --- .gitignore | 4 +-- Makefile | 6 ++-- cmd/tales-server/main.go | 32 ++++++++++-------- package.json | 4 +-- {public => pkg/web/public}/favicon.ico | Bin .../web/public}/images/missing-image.svg | 0 .../web/public}/images/tales-icon-512x512.png | Bin {public => pkg/web/public}/images/tales.svg | 0 {public => pkg/web/public}/index.html | 0 {public => pkg/web/public}/viewer.html | 0 pkg/web/resources.go | 8 +++++ pkg/web/server.go | 11 +++--- public | 1 + 13 files changed, 39 insertions(+), 27 deletions(-) rename {public => pkg/web/public}/favicon.ico (100%) rename {public => pkg/web/public}/images/missing-image.svg (100%) rename {public => pkg/web/public}/images/tales-icon-512x512.png (100%) rename {public => pkg/web/public}/images/tales.svg (100%) rename {public => pkg/web/public}/index.html (100%) rename {public => pkg/web/public}/viewer.html (100%) create mode 100644 pkg/web/resources.go create mode 120000 public diff --git a/.gitignore b/.gitignore index 5bab8810..203d589b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ coverage.html coverage.out dist/ node_modules/ -public/js/ -public/css/ \ No newline at end of file +pkg/web/public/js/ +pkg/web/public/css/ \ No newline at end of file diff --git a/Makefile b/Makefile index d29fd42b..058806ae 100644 --- a/Makefile +++ b/Makefile @@ -80,13 +80,13 @@ test-js: npm run test run: - ${SERVER_BINARY} -resources public/ + ${SERVER_BINARY} -resources pkg/web/public dist: dist-go dist-js dist-go: tales-server.zip -tales-server.zip: bin/* public/* +tales-server.zip: bin/* pkg/web/public/* mkdir -p dist/tales-server cp -r bin public dist/tales-server/ if which zip; then \ @@ -104,4 +104,4 @@ clean-go: rm -rf coverage.out coverage.html ${BINARIES} clean-js: - rm -rf dist/main.{js,js.map} public/js/tales.{js,js.map} public/js/viewer.{js,js.map} + rm -rf dist/main.{js,js.map} pkg/web/public/js/tales.{js,js.map} pkg/web/public/js/viewer.{js,js.map} diff --git a/cmd/tales-server/main.go b/cmd/tales-server/main.go index 172e3be2..10ee915d 100644 --- a/cmd/tales-server/main.go +++ b/cmd/tales-server/main.go @@ -4,6 +4,7 @@ import ( "context" "errors" "flag" + "io/fs" "log" "net/http" "os" @@ -30,33 +31,33 @@ func main() { flag.StringVar(&projectsDir, "projects", defaultProjectsDir(), "path to projects") flag.Parse() - if resourcesDir == "" { - flag.Usage() - os.Exit(1) - } - log.Printf("Starting tales-server %s (%s)", buildinfo.Version, buildinfo.FormattedGitSHA()) - var err error - resourcesDir, err = filepath.Abs(resourcesDir) + projectsDir, err := filepath.Abs(projectsDir) if err != nil { log.Fatal(err) } - projectsDir, err = filepath.Abs(projectsDir) - if err != nil { - log.Fatal(err) - } + log.Printf("Projects directory is at \"%s\"", projectsDir) - shutdownTimeout := 5 * time.Second + var resourceFS fs.FS + if resourcesDir != "" { + resourcesDir, err := filepath.Abs(resourcesDir) + if err != nil { + log.Fatal(err) + } - log.Printf("Projects directory is at \"%s\"", projectsDir) - log.Printf("Resources directory is at \"%s\"", resourcesDir) + log.Printf("Resources directory is at \"%s\"", resourcesDir) + resourceFS = os.DirFS(resourcesDir) + } else { + resourceFS, _ = fs.Sub(web.EmbeddedResources, "public") + log.Println("Using embedded resources") + } server := http.Server{ - Handler: web.NewServer(projectsDir, resourcesDir), + Handler: web.NewServer(projectsDir, resourceFS), Addr: "127.0.0.1:3000", WriteTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second, @@ -72,6 +73,7 @@ func main() { waitForInterrupt() + shutdownTimeout := 5 * time.Second log.Printf("Shutting down... (will timeout in %v)", shutdownTimeout) ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) diff --git a/package.json b/package.json index 5aaf6478..a178afef 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "postinstall": "electron-builder install-app-deps", "scripts": { "build": "rollup -c && npm run build:css", - "build:css": "postcss src/css/*.css --dir public/css/", + "build:css": "postcss src/css/*.css --dir pkg/web/public/css/", "dist": "electron-builder --publish=never", "format": "prettier --write 'src/js/**/*.js' 'src/css/**/*.css'", "lint": "eslint src/js", @@ -18,7 +18,7 @@ "preversion": "npm test && npm run lint", "test": "jest", "watch": "rollup -c -w", - "watch:css": "postcss src/css/*.css --dir public/css/ -w" + "watch:css": "postcss src/css/*.css --dir pkg/web/public/css/ -w" }, "repository": { "type": "git", diff --git a/public/favicon.ico b/pkg/web/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to pkg/web/public/favicon.ico diff --git a/public/images/missing-image.svg b/pkg/web/public/images/missing-image.svg similarity index 100% rename from public/images/missing-image.svg rename to pkg/web/public/images/missing-image.svg diff --git a/public/images/tales-icon-512x512.png b/pkg/web/public/images/tales-icon-512x512.png similarity index 100% rename from public/images/tales-icon-512x512.png rename to pkg/web/public/images/tales-icon-512x512.png diff --git a/public/images/tales.svg b/pkg/web/public/images/tales.svg similarity index 100% rename from public/images/tales.svg rename to pkg/web/public/images/tales.svg diff --git a/public/index.html b/pkg/web/public/index.html similarity index 100% rename from public/index.html rename to pkg/web/public/index.html diff --git a/public/viewer.html b/pkg/web/public/viewer.html similarity index 100% rename from public/viewer.html rename to pkg/web/public/viewer.html diff --git a/pkg/web/resources.go b/pkg/web/resources.go new file mode 100644 index 00000000..f529639a --- /dev/null +++ b/pkg/web/resources.go @@ -0,0 +1,8 @@ +package web + +import "embed" + +// EmbeddedResources holds static web server content. +// +//go:embed public +var EmbeddedResources embed.FS diff --git a/pkg/web/server.go b/pkg/web/server.go index 88b15f98..0207e34f 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -1,6 +1,7 @@ package web import ( + "io/fs" "net/http" "synyx.de/tales/pkg/project" @@ -17,7 +18,7 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // NewServer creates a http.Handler ready to handle tales requests. -func NewServer(projectsDir, resourcesDir string) http.Handler { +func NewServer(projectsDir string, resourcesDir fs.FS) http.Handler { r := http.NewServeMux() repository := &project.FilesystemRepository{ ProjectDir: projectsDir, @@ -27,9 +28,9 @@ func NewServer(projectsDir, resourcesDir string) http.Handler { repository: repository, } - fs := http.FileServer(http.Dir(projectsDir)) - r.Handle("GET /editor/", http.StripPrefix("/editor", fs)) - r.Handle("GET /presenter/", http.StripPrefix("/presenter", fs)) + projectFileServer := http.FileServer(http.Dir(projectsDir)) + r.Handle("GET /editor/", http.StripPrefix("/editor", projectFileServer)) + r.Handle("GET /presenter/", http.StripPrefix("/presenter", projectFileServer)) r.HandleFunc("GET /api/tales/", s.listProjects) r.HandleFunc("POST /api/tales/", s.createProject) @@ -38,7 +39,7 @@ func NewServer(projectsDir, resourcesDir string) http.Handler { r.HandleFunc("DELETE /api/tales/{slug}", s.deleteProject) r.HandleFunc("PUT /api/tales/{slug}/image", s.saveProjectImage) - r.Handle("GET /", http.FileServer(http.Dir(resourcesDir))) + r.Handle("GET /", http.FileServer(http.FS(resourcesDir))) return s } diff --git a/public b/public new file mode 120000 index 00000000..a02d3e3e --- /dev/null +++ b/public @@ -0,0 +1 @@ +pkg/web/public \ No newline at end of file From c7141ff2c958c9502daa2abfaba06bf2f9aa59d1 Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Tue, 1 Oct 2024 11:39:09 +0200 Subject: [PATCH 2/9] go server, make http server addr configurable * prepares for running within containers --- cmd/tales-server/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/tales-server/main.go b/cmd/tales-server/main.go index 10ee915d..d4a9a6fe 100644 --- a/cmd/tales-server/main.go +++ b/cmd/tales-server/main.go @@ -20,6 +20,7 @@ import ( var ( resourcesDir string projectsDir string + bindAddr string ) func init() { @@ -29,6 +30,7 @@ func init() { func main() { flag.StringVar(&resourcesDir, "resources", "", "path to public resources") flag.StringVar(&projectsDir, "projects", defaultProjectsDir(), "path to projects") + flag.StringVar(&bindAddr, "bind", "127.0.0.1:3000", "HTTP server address") flag.Parse() log.Printf("Starting tales-server %s (%s)", @@ -58,7 +60,7 @@ func main() { server := http.Server{ Handler: web.NewServer(projectsDir, resourceFS), - Addr: "127.0.0.1:3000", + Addr: bindAddr, WriteTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second, } From d718b622e3701214a260f3d82ae655c58b7938e8 Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Tue, 1 Oct 2024 13:17:34 +0200 Subject: [PATCH 3/9] Add docker packaging * Use `make docker` to build locally --- .dockerignore | 3 +++ Dockerfile | 10 ++++++++++ Makefile | 7 ++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..274e25ed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +* +!pkg/web/public/ +!bin/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2b3e2a83 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM gcr.io/distroless/static-debian12 + +EXPOSE 3000 + +COPY bin/tales-server / + +VOLUME /work + +ENTRYPOINT ["/tales-server"] +CMD ["-projects", "/work"] diff --git a/Makefile b/Makefile index 058806ae..88d6ddcb 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ BINARIES=${SERVER_BINARY} ${MIGRATE_BINARY} coverage coverage-go coverage-js \ lint lint-go lint-js \ test test-go test-js \ - run dist + run dist docker all: build @@ -98,6 +98,11 @@ tales-server.zip: bin/* pkg/web/public/* dist-js: npm run dist +CONTAINER_BUILDER := $(shell which docker 2>/dev/null || which podman 2>/dev/null) +docker: + CGO_ENABLED=0 go build -ldflags "$(LDFLAGS) -extldflags=-static" -o bin/tales-server $(PKG)/cmd/tales-server + ${CONTAINER_BUILDER} build -t tales . + clean: clean-go clean-js clean-go: From ce776a9b8c5cc90d150b38c441afebfe69b4335f Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Tue, 1 Oct 2024 13:31:54 +0200 Subject: [PATCH 4/9] docker, start rootless --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 2b3e2a83..970d2514 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,5 +6,7 @@ COPY bin/tales-server / VOLUME /work +USER 1000 + ENTRYPOINT ["/tales-server"] CMD ["-projects", "/work"] From 9e15ee96c0c66840c62d93f553e01f7c17ab8267 Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Tue, 1 Oct 2024 17:19:15 +0200 Subject: [PATCH 5/9] go server, fix tests --- pkg/web/server_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/web/server_test.go b/pkg/web/server_test.go index 26e2fb62..2847a0a5 100644 --- a/pkg/web/server_test.go +++ b/pkg/web/server_test.go @@ -3,6 +3,7 @@ package web import ( "bytes" "encoding/json" + "io/fs" "net/http" "net/http/httptest" "os" @@ -23,7 +24,7 @@ type TestClient struct { func NewTestClient(t *testing.T) *TestClient { dir, err := os.MkdirTemp("", "tales-test") assert.NoError(t, err) - handler := NewServer(dir, "") + handler := NewServer(dir, emptyFS{}) repo := &project.FilesystemRepository{ ProjectDir: dir, } @@ -59,6 +60,13 @@ func (tc *TestClient) Cleanup() { func TestServer_ServeHTTP(t *testing.T) { t.Run("http handler", func(t *testing.T) { - assert.Implements(t, new(http.Handler), NewServer("", "")) + assert.Implements(t, new(http.Handler), NewServer("", emptyFS{})) }) } + +type emptyFS struct { +} + +func (f emptyFS) Open(_ string) (fs.File, error) { + return nil, fs.ErrNotExist +} From f748b3a0bdebfd35913f28f3933a54bf39e8c1ec Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Wed, 2 Oct 2024 12:41:19 +0200 Subject: [PATCH 6/9] docker, add examples to docker container --- .dockerignore | 1 + Dockerfile | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 274e25ed..b2e46a9a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ * !pkg/web/public/ !bin/ +!examples/ diff --git a/Dockerfile b/Dockerfile index 970d2514..5125b9a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,11 @@ FROM gcr.io/distroless/static-debian12 EXPOSE 3000 COPY bin/tales-server / +COPY examples/ /Tales -VOLUME /work +VOLUME /Tales USER 1000 ENTRYPOINT ["/tales-server"] -CMD ["-projects", "/work"] +CMD ["-bind", "127.0.0.1:3000", "-projects", "/Tales"] From cd4c8da2e1fa46e2d8747fc226208d6bc42fe68a Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Wed, 2 Oct 2024 13:03:08 +0200 Subject: [PATCH 7/9] docker, revise dockerfile organization --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5125b9a8..09c95c66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ FROM gcr.io/distroless/static-debian12 -EXPOSE 3000 +USER 1000 COPY bin/tales-server / COPY examples/ /Tales VOLUME /Tales -USER 1000 +EXPOSE 3000 ENTRYPOINT ["/tales-server"] CMD ["-bind", "127.0.0.1:3000", "-projects", "/Tales"] From 932198cd27a7982c3c2d2927102f624a8435def8 Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Wed, 2 Oct 2024 14:11:52 +0200 Subject: [PATCH 8/9] readme, add demo instance badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bad2559f..e50f8a06 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://github.com/synyx/tales/workflows/Tales%20CI/badge.svg)](https://github.com/synyx/tales/actions?query=workflow%3A%22Tales+CI%22) +[![Build Status](https://github.com/synyx/tales/workflows/Tales%20CI/badge.svg)](https://github.com/synyx/tales/actions?query=workflow%3A%22Tales+CI%22) [![Demo](https://img.shields.io/badge/demo-instance-blue)](https://tales-demo.synyx.codes/) # Tales @@ -44,7 +44,7 @@ Storyline of your Tale. **Chapters** can further structure your Sections, making ## Prerequisites -All you need is a computer running Windows, Linux or MacOS – and the image with your Tale! :) +All you need is a computer running Windows, Linux or macOS – and the image with your Tale! :) Tales supports all kinds of image formats, from simple JPEGs to sophisticated vector graphics like SVGs. From 8dea061c9e137ba60c314656f836be81d8f9ff7c Mon Sep 17 00:00:00 2001 From: Jonathan Buch Date: Wed, 2 Oct 2024 14:14:04 +0200 Subject: [PATCH 9/9] readme, mention demo instance for examples --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e50f8a06..60ce67b5 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ A video tutorial is coming soon! Some examples of already existing Tales: - [What is Tales: A Tale about Tales](examples/A_Tale_about_Tales.html) + See [Demo Instance](https://tales-demo.synyx.codes) on how it is built. # Contributing