Skip to content

Commit e79dad8

Browse files
authored
Add zstd support (#28)
1 parent 840c7c5 commit e79dad8

File tree

8 files changed

+178
-42
lines changed

8 files changed

+178
-42
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Recommended way of embedding assets is to compress assets before the build, so t
8989
files. This can be inconvenient in some cases, there is `EncodeOnInit` option to compress assets in runtime when
9090
creating file server. Once compressed, assets will be served directly without additional dynamic compression.
9191

92-
Files with extensions ".gz", ".br", ".gif", ".jpg", ".png", ".webp" are excluded from runtime encoding by default.
92+
Files with extensions ".gz", ".br", ".zst", ".gif", ".jpg", ".png", ".webp" are excluded from runtime encoding by default.
9393

9494
> **_NOTE:_** Compressing assets in runtime can degrade startup performance and increase memory usage to prepare and store compressed data.
9595
@@ -106,6 +106,7 @@ import (
106106
"net/http"
107107

108108
"github.com/vearutop/statigz"
109+
"github.com/vearutop/statigz/zstd"
109110
"github.com/vearutop/statigz/brotli"
110111
)
111112

@@ -116,7 +117,7 @@ var st embed.FS
116117

117118
func main() {
118119
// Plug static assets handler to your server or router.
119-
err := http.ListenAndServe(":80", statigz.FileServer(st, brotli.AddEncoding, statigz.FSPrefix("static")))
120+
err := http.ListenAndServe(":80", statigz.FileServer(st, brotli.AddEncoding, zstd.AddEncoding, statigz.FSPrefix("static")))
120121
if err != nil {
121122
log.Fatal(err)
122123
}
@@ -125,17 +126,17 @@ func main() {
125126

126127
### Custom error handling
127128

128-
Error states can be handled with the `staticgz.OnError` and `staticgz.OnNotFound` options. These allow you to customize
129+
Error states can be handled with the `statigz.OnError` and `statigz.OnNotFound` options. These allow you to customize
129130
the response sent to the client when an error occurs or when no resource is found.
130131

131132
```go
132133
fileServer := statigz.FileServer(
133134
st,
134-
staticgz.OnError(func(w http.ResponseWriter, r *http.Request, err error) {
135+
statigz.OnError(func(w http.ResponseWriter, r *http.Request, err error) {
135136
// Handle error.
136137
http.Error(w, err.Error(), http.StatusInternalServerError)
137138
}),
138-
staticgz.OnNotFound(func(w http.ResponseWriter, r *http.Request) {
139+
statigz.OnNotFound(func(w http.ResponseWriter, r *http.Request) {
139140
// Handle not found.
140141
http.Error(w, "Not found", http.StatusNotFound)
141142

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module github.com/vearutop/statigz
22

3-
go 1.17
3+
go 1.22
44

55
require (
66
github.com/andybalholm/brotli v1.1.1
77
github.com/bool64/dev v0.2.39
8+
github.com/klauspost/compress v1.18.0
89
github.com/stretchr/testify v1.4.0
910
)
1011

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
44
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
55
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
66
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
8+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
79
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
810
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
911
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

server.go

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const (
6969
)
7070

7171
// SkipCompressionExt lists file extensions of data that is already compressed.
72-
var SkipCompressionExt = []string{".gz", ".br", ".gif", ".jpg", ".png", ".webp"}
72+
var SkipCompressionExt = []string{".gz", ".br", ".zst", ".gif", ".jpg", ".png", ".webp"}
7373

7474
// FileServer creates an instance of Server from file system.
7575
//
@@ -123,50 +123,54 @@ func (s *Server) encodeFiles() error {
123123
}
124124

125125
for fn, i := range s.info {
126-
isEncoded := false
127-
128-
for _, ext := range SkipCompressionExt {
129-
if strings.HasSuffix(fn, ext) {
130-
isEncoded = true
131-
132-
break
133-
}
134-
}
135-
136-
if isEncoded {
126+
if i.isDir {
137127
continue
138128
}
139129

140-
if _, found := s.info[fn+enc.FileExt]; found {
141-
continue
130+
if err := s.encodeFile(fn, i, enc); err != nil {
131+
return err
142132
}
133+
}
134+
}
143135

144-
// Skip encoding of small data.
145-
if i.size < minSizeToEncode {
146-
continue
147-
}
136+
return nil
137+
}
148138

149-
f, err := s.fs.Open(fn)
150-
if err != nil {
151-
return err
152-
}
139+
func (s *Server) encodeFile(fn string, i fileInfo, enc Encoding) error {
140+
for _, ext := range SkipCompressionExt {
141+
if strings.HasSuffix(fn, ext) {
142+
return nil
143+
}
144+
}
153145

154-
b, err := enc.Encoder(f)
155-
if err != nil {
156-
return err
157-
}
146+
if _, found := s.info[fn+enc.FileExt]; found {
147+
return nil
148+
}
158149

159-
// Skip encoding for non-compressible data.
160-
if float64(len(b))/float64(i.size) > minCompressionRatio {
161-
continue
162-
}
150+
// Skip encoding of small data.
151+
if i.size < minSizeToEncode {
152+
return nil
153+
}
163154

164-
s.info[fn+enc.FileExt] = fileInfo{
165-
hash: i.hash + enc.FileExt,
166-
size: len(b),
167-
content: b[0:len(b):len(b)],
168-
}
169-
}
155+
f, err := s.fs.Open(fn)
156+
if err != nil {
157+
return err
158+
}
159+
160+
b, err := enc.Encoder(f)
161+
if err != nil {
162+
return err
163+
}
164+
165+
// Skip encoding for non-compressible data.
166+
if float64(len(b))/float64(i.size) > minCompressionRatio {
167+
return nil
168+
}
169+
170+
s.info[fn+enc.FileExt] = fileInfo{
171+
hash: i.hash + enc.FileExt,
172+
size: len(b),
173+
content: b[0:len(b):len(b)],
170174
}
171175

172176
return nil

server_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"testing"
1313

1414
brotli2 "github.com/andybalholm/brotli"
15+
zstd2 "github.com/klauspost/compress/zstd"
1516
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"
1718
"github.com/vearutop/statigz"
1819
"github.com/vearutop/statigz/brotli"
20+
"github.com/vearutop/statigz/zstd"
1921
)
2022

2123
//go:embed testdata/*
@@ -285,6 +287,62 @@ func TestServer_ServeHTTP_get_br(t *testing.T) {
285287
assert.Equal(t, raw, decoded)
286288
}
287289

290+
func TestServer_ServeHTTP_get_zst_init(t *testing.T) {
291+
s := statigz.FileServer(v, statigz.EncodeOnInit, brotli.AddEncoding, zstd.AddEncoding)
292+
293+
req, err := http.NewRequest(http.MethodGet, "/testdata/swagger.json", nil)
294+
require.NoError(t, err)
295+
296+
req.Header.Set("Accept-Encoding", "zstd")
297+
298+
rw := httptest.NewRecorder()
299+
s.ServeHTTP(rw, req)
300+
301+
assert.Equal(t, http.StatusOK, rw.Code)
302+
assert.Equal(t, "zstd", rw.Header().Get("Content-Encoding"))
303+
assert.Equal(t, "1bp69hxb9nd93.zst", rw.Header().Get("Etag"))
304+
assert.NotEmpty(t, rw.Body.String())
305+
306+
r, err := zstd2.NewReader(rw.Body)
307+
require.NoError(t, err)
308+
309+
decoded, err := io.ReadAll(r)
310+
assert.NoError(t, err)
311+
312+
raw, err := os.ReadFile("testdata/swagger.json")
313+
assert.NoError(t, err)
314+
315+
assert.Equal(t, raw, decoded)
316+
}
317+
318+
func TestServer_ServeHTTP_get_zst(t *testing.T) {
319+
s := statigz.FileServer(v, statigz.EncodeOnInit, brotli.AddEncoding, zstd.AddEncoding)
320+
321+
req, err := http.NewRequest(http.MethodGet, "/testdata/deeper/swagger.json", nil)
322+
require.NoError(t, err)
323+
324+
req.Header.Set("Accept-Encoding", "zstd")
325+
326+
rw := httptest.NewRecorder()
327+
s.ServeHTTP(rw, req)
328+
329+
assert.Equal(t, http.StatusOK, rw.Code)
330+
assert.Equal(t, "zstd", rw.Header().Get("Content-Encoding"))
331+
assert.Equal(t, "1061t8bc8jx4s", rw.Header().Get("Etag"))
332+
assert.NotEmpty(t, rw.Body.String())
333+
334+
r, err := zstd2.NewReader(rw.Body)
335+
require.NoError(t, err)
336+
337+
decoded, err := io.ReadAll(r)
338+
assert.NoError(t, err)
339+
340+
raw, err := os.ReadFile("testdata/swagger.json")
341+
assert.NoError(t, err)
342+
343+
assert.Equal(t, raw, decoded)
344+
}
345+
288346
func TestServer_ServeHTTP_indexCompressed(t *testing.T) {
289347
s := statigz.FileServer(v)
290348

testdata/deeper/swagger.json.zst

3.59 KB
Binary file not shown.

zstd/encoding.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Package zstd provides encoding for statigz.Server.
2+
package zstd
3+
4+
import (
5+
"bytes"
6+
"io"
7+
8+
"github.com/klauspost/compress/zstd"
9+
"github.com/vearutop/statigz"
10+
)
11+
12+
// AddEncoding is an option that prepends zstd to encodings of statigz.Server.
13+
//
14+
// It is located in a separate package to allow better control of imports graph.
15+
func AddEncoding(server *statigz.Server) {
16+
enc := statigz.Encoding{
17+
FileExt: ".zst",
18+
ContentEncoding: "zstd",
19+
Decoder: func(r io.Reader) (io.Reader, error) {
20+
return zstd.NewReader(r)
21+
},
22+
Encoder: func(r io.Reader) ([]byte, error) {
23+
res := bytes.NewBuffer(nil)
24+
25+
w, err := zstd.NewWriter(res, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
if _, err := io.Copy(w, r); err != nil {
31+
return nil, err
32+
}
33+
34+
if err := w.Close(); err != nil {
35+
return nil, err
36+
}
37+
38+
return res.Bytes(), nil
39+
},
40+
}
41+
42+
server.Encodings = append([]statigz.Encoding{enc}, server.Encodings...)
43+
}

zstd/encoding_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package zstd_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/vearutop/statigz"
9+
"github.com/vearutop/statigz/zstd"
10+
)
11+
12+
func TestAddEncoding(t *testing.T) {
13+
s := &statigz.Server{}
14+
s.Encodings = append(s.Encodings, statigz.GzipEncoding())
15+
zstd.AddEncoding(s)
16+
17+
assert.Equal(t, ".zst", s.Encodings[0].FileExt)
18+
assert.Equal(t, ".gz", s.Encodings[1].FileExt)
19+
d, err := s.Encodings[0].Decoder(nil)
20+
assert.NoError(t, err)
21+
assert.NotNil(t, d)
22+
23+
e, err := s.Encodings[0].Encoder(strings.NewReader(strings.Repeat("A", 10000)))
24+
assert.NoError(t, err)
25+
assert.NotEmpty(t, e)
26+
assert.Less(t, len(e), 100)
27+
}

0 commit comments

Comments
 (0)