common/embed: replace all go:embed use by an embedded archive

Some of the files were quite big:

- asns.csv ~ 3 MB
- index.js ~ 1.5 MB
- *.svg ~ 2 MB

Use a ZIP archive to put them all and embed it. This reduce the binary
size from 89 MB to 82 MB. 🤯

This also pulls some code modernization (use of http.ServeFileFS).
This commit is contained in:
Vincent Bernat
2025-09-02 23:30:20 +02:00
parent d102e5f20e
commit b1d6382585
13 changed files with 107 additions and 47 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- name: Setup - name: Setup
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Install dependencies - name: Install dependencies
run: sudo apt-get install -qqy shared-mime-info curl run: sudo apt-get install -qqy shared-mime-info curl zip
# Regular tests # Regular tests
- name: Go race tests - name: Go race tests

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
/orchestrator/clickhouse/data/udp.csv /orchestrator/clickhouse/data/udp.csv
/console/filter/parser.go /console/filter/parser.go
/common/schema/definition_gen.go /common/schema/definition_gen.go
/common/embed/data/embed.zip
mock_*.go mock_*.go
*_enumer.go *_enumer.go
*.pb.go *.pb.go

View File

@@ -22,7 +22,7 @@ run tests:
# past but this is a slight burden to maintain in addition to GitHub CI. # past but this is a slight burden to maintain in addition to GitHub CI.
# Check commit ceaa6ebf8ef6 for the last version supporting functional # Check commit ceaa6ebf8ef6 for the last version supporting functional
# tests. # tests.
- time apk add --no-cache git make gcc musl-dev shared-mime-info npm curl - time apk add --no-cache git make gcc musl-dev shared-mime-info npm curl zip
- export GOMODCACHE=$PWD/.go-cache - export GOMODCACHE=$PWD/.go-cache
- npm config --user set cache $PWD/.npm-cache - npm config --user set cache $PWD/.npm-cache
- time go mod download - time go mod download

View File

@@ -38,7 +38,8 @@ GENERATED_TEST_GO = \
GENERATED = \ GENERATED = \
$(GENERATED_GO) \ $(GENERATED_GO) \
$(GENERATED_JS) \ $(GENERATED_JS) \
console/data/frontend console/data/frontend \
common/embed/data/embed.zip
.PHONY: all all-indep .PHONY: all all-indep
BUILD_ARGS = BUILD_ARGS =
@@ -153,6 +154,11 @@ default-%.pgo:
[ -n $$ip ] ; \ [ -n $$ip ] ; \
curl -so $@ "http://$$ip:8080/debug/pprof/profile?seconds=30" curl -so $@ "http://$$ip:8080/debug/pprof/profile?seconds=30"
common/embed/data/embed.zip: console/data/frontend console/authentication/data/avatars console/data/docs
common/embed/data/embed.zip: orchestrator/clickhouse/data/protocols.csv orchestrator/clickhouse/data/icmp.csv orchestrator/clickhouse/data/asns.csv orchestrator/clickhouse/data/tcp.csv orchestrator/clickhouse/data/udp.csv
common/embed/data/embed.zip: ; $(info $(M) generate embed.zip)
$Q mkdir -p common/embed/data && zip --quiet --recurse-paths --filesync $@ $^
# Tests # Tests
.PHONY: check test tests test-race test-short test-bench test-coverage .PHONY: check test tests test-race test-short test-bench test-coverage

41
common/embed/fs.go Normal file
View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2025 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
// Package embed provides access to the compressed archive containing all the
// embedded files.
package embed
import (
"archive/zip"
"bytes"
_ "embed"
"fmt"
"io/fs"
"sync"
)
var (
//go:embed data/embed.zip
embeddedZip []byte
data fs.FS
dataOnce sync.Once
dataReady chan struct{}
)
// Data returns a filesystem with the files contained in the embedded archive.
func Data() fs.FS {
dataOnce.Do(func() {
r, err := zip.NewReader(bytes.NewReader(embeddedZip), int64(len(embeddedZip)))
if err != nil {
panic(fmt.Sprintf("cannot read embedded archive: %s", err))
}
data = r
close(dataReady)
})
<-dataReady
return data
}
func init() {
dataReady = make(chan struct{})
}

28
common/embed/fs_test.go Normal file
View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package embed_test
import (
"io"
"testing"
"akvorado/common/embed"
"akvorado/common/helpers"
)
func TestData(t *testing.T) {
f, err := embed.Data().Open("orchestrator/clickhouse/data/protocols.csv")
if err != nil {
t.Fatalf("Open() error:\n%+v", err)
}
expected := "proto,name,description"
got := make([]byte, len(expected))
_, err = io.ReadFull(f, got)
if err != nil {
t.Fatalf("ReadFull() error:\n%+v", err)
}
if diff := helpers.Diff(string(got), expected); diff != "" {
t.Fatalf("ReadFull() (-got, +want):\n%s", diff)
}
}

View File

@@ -4,31 +4,20 @@
package console package console
import ( import (
"embed"
"net/http" "net/http"
"time"
) )
//go:embed data/frontend
var embeddedAssets embed.FS
func (c *Component) defaultHandlerFunc(w http.ResponseWriter, req *http.Request) { func (c *Component) defaultHandlerFunc(w http.ResponseWriter, req *http.Request) {
assets := c.embedOrLiveFS(embeddedAssets, "data/frontend") assets := c.embedOrLiveFS("data/frontend")
f, err := http.FS(assets).Open("index.html") http.ServeFileFS(w, req, assets, "index.html")
if err != nil {
http.Error(w, "Application not found.", http.StatusInternalServerError)
return
}
http.ServeContent(w, req, "index.html", time.Time{}, f)
f.Close()
} }
func (c *Component) staticAssetsHandlerFunc(w http.ResponseWriter, req *http.Request) { func (c *Component) staticAssetsHandlerFunc(w http.ResponseWriter, req *http.Request) {
assets := c.embedOrLiveFS(embeddedAssets, "data/frontend/assets") assets := c.embedOrLiveFS("data/frontend/assets")
http.FileServer(http.FS(assets)).ServeHTTP(w, req) http.FileServer(http.FS(assets)).ServeHTTP(w, req)
} }
func (c *Component) docAssetsHandlerFunc(w http.ResponseWriter, req *http.Request) { func (c *Component) docAssetsHandlerFunc(w http.ResponseWriter, req *http.Request) {
docs := c.embedOrLiveFS(embeddedDocs, "data/docs") docs := c.embedOrLiveFS("data/docs")
http.FileServer(http.FS(docs)).ServeHTTP(w, req) http.FileServer(http.FS(docs)).ServeHTTP(w, req)
} }

View File

@@ -5,7 +5,6 @@ package authentication
import ( import (
"bufio" "bufio"
"embed"
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"image" "image"
@@ -15,12 +14,11 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"akvorado/common/embed"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
//go:embed data/avatars
var avatarParts embed.FS
// UserInfoHandlerFunc returns the information about the currently logged user. // UserInfoHandlerFunc returns the information about the currently logged user.
func (c *Component) UserInfoHandlerFunc(gc *gin.Context) { func (c *Component) UserInfoHandlerFunc(gc *gin.Context) {
info := gc.MustGet("user").(UserInformation) info := gc.MustGet("user").(UserInformation)
@@ -44,6 +42,12 @@ func (c *Component) UserAvatarHandlerFunc(gc *gin.Context) {
} }
// Grab list of parts // Grab list of parts
avatarParts, err := fs.Sub(embed.Data(), "console/authentication")
if err != nil {
c.r.Err(err).Msg("cannot open embedded archive")
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Cannot build avatar."})
return
}
parts := []string{} parts := []string{}
partList, err := avatarParts.Open("data/avatars/partlist.txt") partList, err := avatarParts.Open("data/avatars/partlist.txt")
if err != nil { if err != nil {

View File

@@ -5,7 +5,6 @@ package console
import ( import (
"bytes" "bytes"
"embed"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -24,8 +23,6 @@ import (
) )
var ( var (
//go:embed data/docs
embeddedDocs embed.FS
internalLinkRegexp = regexp.MustCompile("^(([0-9]+)-([a-z]+).md)(#.*|$)") internalLinkRegexp = regexp.MustCompile("^(([0-9]+)-([a-z]+).md)(#.*|$)")
) )
@@ -43,7 +40,7 @@ type DocumentTOC struct {
} }
func (c *Component) docsHandlerFunc(gc *gin.Context) { func (c *Component) docsHandlerFunc(gc *gin.Context) {
docs := c.embedOrLiveFS(embeddedDocs, "data/docs") docs := c.embedOrLiveFS("data/docs")
requestedDocument := gc.Param("name") requestedDocument := gc.Param("name")
var markdown []byte var markdown []byte

View File

@@ -19,6 +19,7 @@ import (
"akvorado/common/clickhousedb" "akvorado/common/clickhousedb"
"akvorado/common/daemon" "akvorado/common/daemon"
"akvorado/common/embed"
"akvorado/common/httpserver" "akvorado/common/httpserver"
"akvorado/common/reporter" "akvorado/common/reporter"
"akvorado/common/schema" "akvorado/common/schema"
@@ -138,14 +139,14 @@ func (c *Component) Stop() error {
// embedOrLiveFS returns a subset of the provided embedded filesystem, // embedOrLiveFS returns a subset of the provided embedded filesystem,
// except if the component is configured to use the live filesystem. // except if the component is configured to use the live filesystem.
// Then, it returns the provided tree. // Then, it returns the provided tree.
func (c *Component) embedOrLiveFS(embed fs.FS, p string) fs.FS { func (c *Component) embedOrLiveFS(prefix string) fs.FS {
var fileSystem fs.FS var fileSystem fs.FS
if c.config.ServeLiveFS { if c.config.ServeLiveFS {
_, src, _, _ := runtime.Caller(0) _, src, _, _ := runtime.Caller(0)
fileSystem = os.DirFS(filepath.Join(path.Dir(src), p)) fileSystem = os.DirFS(filepath.Join(path.Dir(src), prefix))
} else { } else {
var err error var err error
fileSystem, err = fs.Sub(embed, p) fileSystem, err = fs.Sub(embed.Data(), path.Join("console", prefix))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -6,7 +6,7 @@ COPY Makefile .
RUN make console/data/frontend RUN make console/data/frontend
FROM --platform=$BUILDPLATFORM golang:alpine AS build-go FROM --platform=$BUILDPLATFORM golang:alpine AS build-go
RUN apk add --no-cache make mailcap curl RUN apk add --no-cache make mailcap curl zip
WORKDIR /build WORKDIR /build
# Cache for modules # Cache for modules
COPY go.mod go.sum . COPY go.mod go.sum .

View File

@@ -49,6 +49,7 @@
src = ./.; src = ./.;
vendorHash = l.readFile ./nix/vendorHash.txt; vendorHash = l.readFile ./nix/vendorHash.txt;
proxyVendor = true; # generated code may contain additional dependencies proxyVendor = true; # generated code may contain additional dependencies
nativeBuildInputs = [ pkgs.zip ];
buildPhase = '' buildPhase = ''
cp -r ${frontend}/node_modules console/frontend/node_modules cp -r ${frontend}/node_modules console/frontend/node_modules
cp -r ${frontend}/data console/data/frontend cp -r ${frontend}/data console/data/frontend
@@ -145,6 +146,7 @@
nodejs nodejs
pkgs.git pkgs.git
pkgs.curl pkgs.curl
pkgs.zip
]; ];
}; };
}); });

View File

@@ -5,40 +5,31 @@ package clickhouse
import ( import (
"compress/gzip" "compress/gzip"
"embed"
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
"akvorado/common/embed"
) )
//go:embed data/protocols.csv
//go:embed data/icmp.csv
//go:embed data/asns.csv
//go:embed data/tcp.csv
//go:embed data/udp.csv
var data embed.FS
func (c *Component) addHandlerEmbedded(url string, path string) { func (c *Component) addHandlerEmbedded(url string, path string) {
data, _ := fs.Sub(embed.Data(), "orchestrator/clickhouse")
c.d.HTTP.AddHandler(url, c.d.HTTP.AddHandler(url,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f, err := http.FS(data).Open(path) http.ServeFileFS(w, r, data, path)
if err != nil {
c.r.Err(err).Msgf("unable to open %s", path)
http.Error(w, fmt.Sprintf("Unable to open %q.", path), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, path, time.Time{}, f)
f.Close()
})) }))
} }
// registerHTTPHandler register some handlers that will be useful for // registerHTTPHandler register some handlers that will be useful for
// ClickHouse // ClickHouse
func (c *Component) registerHTTPHandlers() error { func (c *Component) registerHTTPHandlers() error {
data, _ := fs.Sub(embed.Data(), "orchestrator/clickhouse")
// Add handler for custom dicts // Add handler for custom dicts
for name, dict := range c.d.Schema.GetCustomDictConfig() { for name, dict := range c.d.Schema.GetCustomDictConfig() {
c.d.HTTP.AddHandler(fmt.Sprintf("/api/v0/orchestrator/clickhouse/custom_dict_%s.csv", name), http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { c.d.HTTP.AddHandler(fmt.Sprintf("/api/v0/orchestrator/clickhouse/custom_dict_%s.csv", name), http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -163,7 +154,7 @@ func (c *Component) registerHTTPHandlers() error {
} }
// Static CSV files // Static CSV files
entries, err := data.ReadDir("data") entries, err := data.(fs.ReadDirFS).ReadDir("data")
if err != nil { if err != nil {
return fmt.Errorf("unable to read data directory: %w", err) return fmt.Errorf("unable to read data directory: %w", err)
} }