Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b8873a4f5 | ||
|
|
5bece326fb | ||
|
|
0016a5e4fd | ||
|
|
351d279a22 | ||
|
|
203138bc05 | ||
|
|
cd9c8b600e | ||
|
|
5412b97411 |
9 changed files with 246 additions and 129 deletions
|
|
@ -1,15 +1,15 @@
|
||||||
module src.lwithers.me.uk/go/htpack/cmd/packserver
|
module src.lwithers.me.uk/go/htpack/cmd/packserver
|
||||||
|
|
||||||
go 1.26.0
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.8.1
|
||||||
src.lwithers.me.uk/go/htpack v1.5.0
|
src.lwithers.me.uk/go/htpack v1.3.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.12 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,45 @@
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
src.lwithers.me.uk/go/htpack v1.5.0 h1:2UYM4AGIPc/XgktmO6AxUyqFxzmYkqg/jINcDCsZsCk=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
src.lwithers.me.uk/go/htpack v1.5.0/go.mod h1:5dQlPdwV+ZeBAaFBdwiRI6/w5dpSgPsiotO4ZTYau88=
|
src.lwithers.me.uk/go/htpack v1.3.3 h1:Xvl6qR9HfSblmCgPyu+ACQ9o3aLQSIy3l8CrMbzj/jc=
|
||||||
|
src.lwithers.me.uk/go/htpack v1.3.3/go.mod h1:qKgCBgZ6iiiuYOxZkYOPVpXLBzp6gXEd4A0ksxgR6Nk=
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,6 @@ func main() {
|
||||||
"Name of index file (index.html or similar)")
|
"Name of index file (index.html or similar)")
|
||||||
rootCmd.Flags().Duration("expiry", 0,
|
rootCmd.Flags().Duration("expiry", 0,
|
||||||
"Tell client how long it can cache data for; 0 means no caching")
|
"Tell client how long it can cache data for; 0 means no caching")
|
||||||
rootCmd.Flags().Duration("expiry404", 0,
|
|
||||||
"Tell client how long it can cache 404 responses for; 0 means default (inherit from --expiry, max. 60s); -1 means no caching")
|
|
||||||
rootCmd.Flags().Bool("immutable", false,
|
|
||||||
"Tell client that cached assets are immutable (will not be revalidated on page reload)")
|
|
||||||
rootCmd.Flags().String("fallback-404", "",
|
rootCmd.Flags().String("fallback-404", "",
|
||||||
"Name of file to return if response would be 404 (spa.html or similar)")
|
"Name of file to return if response would be 404 (spa.html or similar)")
|
||||||
rootCmd.Flags().String("frames", "sameorigin",
|
rootCmd.Flags().String("frames", "sameorigin",
|
||||||
|
|
@ -139,35 +135,15 @@ func run(c *cobra.Command, args []string) error {
|
||||||
|
|
||||||
// parse expiry time
|
// parse expiry time
|
||||||
// NB: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
// NB: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||||
cacheControl200, cacheControl404 := "no-cache", "no-cache"
|
|
||||||
immutable, err := c.Flags().GetBool("immutable")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var immutableDirective string
|
|
||||||
if immutable {
|
|
||||||
immutableDirective = ", immutable"
|
|
||||||
}
|
|
||||||
|
|
||||||
expiry, err := c.Flags().GetDuration("expiry")
|
expiry, err := c.Flags().GetDuration("expiry")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if expiry > 0 {
|
if expiry <= 0 {
|
||||||
cacheControl200 = fmt.Sprintf("public, max-age=%d%s", expiry/1e9, immutableDirective)
|
extraHeaders.Set("Cache-Control", "no-cache")
|
||||||
}
|
} else {
|
||||||
|
extraHeaders.Set("Cache-Control",
|
||||||
expiry404, err := c.Flags().GetDuration("expiry404")
|
fmt.Sprintf("public, max-age=%d", expiry/1e9))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case expiry404 == 0 && expiry == 0:
|
|
||||||
case expiry404 == 0 && expiry > 0:
|
|
||||||
expiry404 = min(60*time.Second, expiry)
|
|
||||||
fallthrough
|
|
||||||
case expiry404 > 0:
|
|
||||||
cacheControl404 = fmt.Sprintf("public, max-age=%d", expiry404/1e9)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional index file
|
// optional index file
|
||||||
|
|
@ -223,7 +199,6 @@ func run(c *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
packHandler.SetCacheControl(cacheControl200, cacheControl404)
|
|
||||||
if indexFile != "" {
|
if indexFile != "" {
|
||||||
packHandler.SetIndex(indexFile)
|
packHandler.SetIndex(indexFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -1,8 +1,9 @@
|
||||||
module src.lwithers.me.uk/go/htpack
|
module src.lwithers.me.uk/go/htpack
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/sys v0.47.0
|
github.com/google/go-cmp v0.7.0
|
||||||
google.golang.org/protobuf v1.36.12
|
golang.org/x/sys v0.45.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
go.sum
8
go.sum
|
|
@ -1,6 +1,6 @@
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
|
|
||||||
163
handler.go
163
handler.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -15,6 +14,7 @@ import (
|
||||||
const (
|
const (
|
||||||
encodingGzip = "gzip"
|
encodingGzip = "gzip"
|
||||||
encodingBrotli = "br"
|
encodingBrotli = "br"
|
||||||
|
encodingZstd = "zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New returns a new handler. Standard security headers are set.
|
// New returns a new handler. Standard security headers are set.
|
||||||
|
|
@ -23,14 +23,21 @@ func New(packfile string) (*Handler, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return NewMapped("/", mapped), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapped returns a new handler, given an already-mapped datafile. Standard
|
||||||
|
// security headers are set.
|
||||||
|
//
|
||||||
|
// Files are served under the directory prefix given by path, which may be
|
||||||
|
// empty or "/" for the root path. Note the [Handler.ServeMapped] method may specify
|
||||||
|
// additional files to be served under different paths, not affected by this
|
||||||
|
// path.
|
||||||
|
func NewMapped(path string, mapped *packed.Mapped) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
mapped: mapped.Data,
|
files: make(map[string]handlerFile),
|
||||||
dir: mapped.Directory.Files,
|
headers: make(map[string]string),
|
||||||
headers: make(map[string]string),
|
startTime: time.Now(),
|
||||||
cacheControl200: "no-cache",
|
|
||||||
cacheControl404: "no-cache",
|
|
||||||
startTime: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
||||||
|
|
@ -39,18 +46,24 @@ func New(packfile string) (*Handler, error) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
|
||||||
h.SetHeader("X-Content-Type-Options", "nosniff")
|
h.SetHeader("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
return h, nil
|
h.addFiles(path, mapped)
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler implements http.Handler and allows options to be set.
|
// Handler implements http.Handler and allows options to be set.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
mapped []byte
|
files map[string]handlerFile
|
||||||
dir map[string]*packed.File
|
headers map[string]string
|
||||||
headers map[string]string
|
startTime time.Time
|
||||||
cacheControl200 string
|
notFound *handlerFile
|
||||||
cacheControl404 string
|
}
|
||||||
startTime time.Time
|
|
||||||
notFound *packed.File
|
// AddMapped serves up additional files from the given mapped packfile. The
|
||||||
|
// given path is taken as a serving prefix. If a file for a given path already
|
||||||
|
// exists, the existing file takes precedence.
|
||||||
|
func (h *Handler) AddMapped(path string, mapped *packed.Mapped) {
|
||||||
|
h.addFiles(path, mapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHeader allows a custom header to be set on HTTP responses. These are
|
// SetHeader allows a custom header to be set on HTTP responses. These are
|
||||||
|
|
@ -79,11 +92,11 @@ func (h *Handler) SetHeader(key, value string) {
|
||||||
// multiple times with different filenames (noting later calls would not
|
// multiple times with different filenames (noting later calls would not
|
||||||
// overwrite files matching earlier calls).
|
// overwrite files matching earlier calls).
|
||||||
func (h *Handler) SetIndex(filename string) {
|
func (h *Handler) SetIndex(filename string) {
|
||||||
for k, v := range h.dir {
|
for k, v := range h.files {
|
||||||
if filepath.Base(k) == filename {
|
if path.Base(k) == filename {
|
||||||
routeToAdd := filepath.Dir(k)
|
routeToAdd := path.Dir(k)
|
||||||
if _, exists := h.dir[routeToAdd]; !exists {
|
if _, exists := h.files[routeToAdd]; !exists {
|
||||||
h.dir[routeToAdd] = v
|
h.files[routeToAdd] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,25 +116,60 @@ func (h *Handler) SetNotFound(notFound string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
notFound = path.Clean(notFound)
|
notFound = path.Clean(notFound)
|
||||||
dir := h.dir[path.Clean(notFound)]
|
f, ok := h.files[path.Clean(notFound)]
|
||||||
if dir == nil {
|
if !ok {
|
||||||
return fmt.Errorf("no such resource %q", notFound)
|
return fmt.Errorf("no such resource %q", notFound)
|
||||||
}
|
}
|
||||||
h.notFound = dir
|
h.notFound = &f
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCacheControl allows control over the cache-control header. If not called,
|
type handlerFile struct {
|
||||||
// "Cache-Control: no-cache" is the default.
|
contentType, etag string
|
||||||
//
|
uncompressed, gzip, brotli, zstd []byte
|
||||||
// The first and second arguments are required, and specify the header to use
|
}
|
||||||
// for 200 responses and 404 responses respectively.
|
|
||||||
//
|
func (h *Handler) addFiles(prefix string, mapped *packed.Mapped) {
|
||||||
// TODO: could be expanded to take extra varargs for more specific control over
|
// our directory contains files like "/index.html". We will add
|
||||||
// particular assets, like different expiry per content-type.
|
// a prefix, which might be "" or "/foo".
|
||||||
func (h *Handler) SetCacheControl(hdr200, hdr404 string) {
|
prefix = path.Clean(prefix)
|
||||||
h.cacheControl200 = hdr200
|
prefix = strings.TrimSuffix(prefix, "/")
|
||||||
h.cacheControl404 = hdr404
|
switch {
|
||||||
|
case prefix == "":
|
||||||
|
case prefix == ".":
|
||||||
|
prefix = ""
|
||||||
|
case !strings.HasPrefix(prefix, "/"):
|
||||||
|
prefix = "/" + prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
for filePath, file := range mapped.Directory.Files {
|
||||||
|
handlerPath := prefix + filePath
|
||||||
|
if _, exists := h.files[handlerPath]; exists {
|
||||||
|
// don't overwrite existing entries
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.files[handlerPath] = buildHandlerFile(mapped.Data, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHandlerFile(data []byte, file *packed.File) handlerFile {
|
||||||
|
byteSlice := func(fileData *packed.FileData) []byte {
|
||||||
|
if fileData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
start := fileData.Offset
|
||||||
|
end := start + fileData.Length
|
||||||
|
return data[start:end]
|
||||||
|
}
|
||||||
|
hf := handlerFile{
|
||||||
|
contentType: file.ContentType,
|
||||||
|
etag: file.Etag,
|
||||||
|
}
|
||||||
|
hf.uncompressed = byteSlice(file.Uncompressed)
|
||||||
|
hf.gzip = byteSlice(file.Gzip)
|
||||||
|
hf.brotli = byteSlice(file.Brotli)
|
||||||
|
hf.zstd = byteSlice(file.Zstd)
|
||||||
|
return hf
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles requests for files. It supports GET and HEAD methods, with
|
// ServeHTTP handles requests for files. It supports GET and HEAD methods, with
|
||||||
|
|
@ -142,46 +190,48 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := h.dir[path.Clean(req.URL.Path)]
|
info, ok := h.files[path.Clean(req.URL.Path)]
|
||||||
if info == nil {
|
if !ok {
|
||||||
w.Header().Set("Cache-Control", h.cacheControl404)
|
|
||||||
if h.notFound == nil {
|
if h.notFound == nil {
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info = h.notFound
|
info = *h.notFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// set standard headers
|
// set standard headers
|
||||||
w.Header().Set("Cache-Control", h.cacheControl200)
|
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
w.Header().Set("Vary", "Accept-Encoding")
|
||||||
w.Header().Set("Etag", info.Etag)
|
w.Header().Set("Etag", info.etag)
|
||||||
w.Header().Set("Content-Type", info.ContentType)
|
w.Header().Set("Content-Type", info.contentType)
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|
||||||
// process etag / modtime
|
// process etag / modtime
|
||||||
if clientHasCachedVersion(info.Etag, h.startTime, req) {
|
if clientHasCachedVersion(info.etag, h.startTime, req) {
|
||||||
w.WriteHeader(http.StatusNotModified)
|
w.WriteHeader(http.StatusNotModified)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// select compression
|
// select compression
|
||||||
data := info.Uncompressed
|
data := info.uncompressed
|
||||||
gzip, brotli := acceptedEncodings(req)
|
gzip, brotli, zstd := acceptedEncodings(req)
|
||||||
if brotli && info.Brotli != nil {
|
switch {
|
||||||
data = info.Brotli
|
case zstd && info.zstd != nil:
|
||||||
|
data = info.zstd
|
||||||
|
w.Header().Set("Content-Encoding", encodingZstd)
|
||||||
|
case brotli && info.brotli != nil:
|
||||||
|
data = info.brotli
|
||||||
w.Header().Set("Content-Encoding", encodingBrotli)
|
w.Header().Set("Content-Encoding", encodingBrotli)
|
||||||
} else if gzip && info.Gzip != nil {
|
case gzip && info.gzip != nil:
|
||||||
data = info.Gzip
|
data = info.gzip
|
||||||
w.Header().Set("Content-Encoding", encodingGzip)
|
w.Header().Set("Content-Encoding", encodingGzip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// range support (single-part ranges only)
|
// range support (single-part ranges only)
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests#Single_part_ranges
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests#Single_part_ranges
|
||||||
offset, length, isPartial := getFileRange(data, req)
|
offset, length, isPartial := getFileRange(uint64(len(data)), req)
|
||||||
if isPartial {
|
if isPartial {
|
||||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d",
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d",
|
||||||
offset, offset+length-1, data.Length))
|
offset, offset+length-1, len(data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we know exactly what we're writing, finalise HTTP header
|
// now we know exactly what we're writing, finalise HTTP header
|
||||||
|
|
@ -197,11 +247,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += data.Offset
|
w.Write(data[offset : offset+length])
|
||||||
w.Write(h.mapped[offset : offset+length])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptedEncodings(req *http.Request) (gzip, brotli bool) {
|
func acceptedEncodings(req *http.Request) (gzip, brotli, zstd bool) {
|
||||||
encodings := req.Header.Get("Accept-Encoding")
|
encodings := req.Header.Get("Accept-Encoding")
|
||||||
for _, enc := range strings.Split(encodings, ",") {
|
for _, enc := range strings.Split(encodings, ",") {
|
||||||
switch strings.TrimSpace(enc) {
|
switch strings.TrimSpace(enc) {
|
||||||
|
|
@ -244,8 +293,8 @@ func clientHasCachedVersion(etag string, startTime time.Time, req *http.Request,
|
||||||
|
|
||||||
// getFileRange returns the byte offset and length of the file to serve, along
|
// getFileRange returns the byte offset and length of the file to serve, along
|
||||||
// with whether or not it's partial content.
|
// with whether or not it's partial content.
|
||||||
func getFileRange(data *packed.FileData, req *http.Request) (offset, length uint64, isPartial bool) {
|
func getFileRange(dataLength uint64, req *http.Request) (offset, length uint64, isPartial bool) {
|
||||||
length = data.Length
|
length = dataLength
|
||||||
|
|
||||||
// only accept "Range: bytes=…"
|
// only accept "Range: bytes=…"
|
||||||
r := req.Header.Get("Range")
|
r := req.Header.Get("Range")
|
||||||
|
|
@ -272,8 +321,8 @@ func getFileRange(data *packed.FileData, req *http.Request) (offset, length uint
|
||||||
// validate the interval lies within the file
|
// validate the interval lies within the file
|
||||||
switch {
|
switch {
|
||||||
case from > to,
|
case from > to,
|
||||||
from >= data.Length,
|
from >= dataLength,
|
||||||
to >= data.Length:
|
to >= dataLength:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
handler_test.go
Normal file
49
handler_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package htpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"src.lwithers.me.uk/go/htpack/packed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler(t *testing.T) {
|
||||||
|
mapped, err := packed.MapAndLoad("packed/testdata/helloworld.pack")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("loading test packfile:", err)
|
||||||
|
}
|
||||||
|
defer mapped.Close()
|
||||||
|
|
||||||
|
orig, err := os.ReadFile("packed/testdata/helloworld.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("loading test textfile:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewMapped("", mapped)
|
||||||
|
h.AddMapped("/foo", mapped)
|
||||||
|
|
||||||
|
testsv := httptest.NewServer(h)
|
||||||
|
defer testsv.Close()
|
||||||
|
cl := testsv.Client()
|
||||||
|
url := testsv.URL + "/helloworld.txt"
|
||||||
|
|
||||||
|
resp, err := cl.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("executing GET request:", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("reading response body:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(body, orig) {
|
||||||
|
t.Error("difference in response (- got, + expected):")
|
||||||
|
t.Error(cmp.Diff(string(body), string(orig)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -168,6 +168,8 @@ type File struct {
|
||||||
Gzip *FileData `protobuf:"bytes,4,opt,name=gzip,proto3" json:"gzip,omitempty"`
|
Gzip *FileData `protobuf:"bytes,4,opt,name=gzip,proto3" json:"gzip,omitempty"`
|
||||||
// Brotli compressed version of the file.
|
// Brotli compressed version of the file.
|
||||||
Brotli *FileData `protobuf:"bytes,5,opt,name=brotli,proto3" json:"brotli,omitempty"`
|
Brotli *FileData `protobuf:"bytes,5,opt,name=brotli,proto3" json:"brotli,omitempty"`
|
||||||
|
// Zstd compressed version of the file.
|
||||||
|
Zstd *FileData `protobuf:"bytes,6,opt,name=zstd,proto3" json:"zstd,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *File) Reset() {
|
func (x *File) Reset() {
|
||||||
|
|
@ -237,6 +239,13 @@ func (x *File) GetBrotli() *FileData {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *File) GetZstd() *FileData {
|
||||||
|
if x != nil {
|
||||||
|
return x.Zstd
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// FileData records the position of the file data within the pack.
|
// FileData records the position of the file data within the pack.
|
||||||
type FileData struct {
|
type FileData struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
|
|
@ -321,7 +330,7 @@ var file_packed_proto_rawDesc = []byte{
|
||||||
0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x77, 0x69, 0x74, 0x68,
|
0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x77, 0x69, 0x74, 0x68,
|
||||||
0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x65,
|
0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x65,
|
||||||
0x64, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
|
0x64, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
|
||||||
0x01, 0x22, 0xf3, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
|
0x01, 0x22, 0xa9, 0x02, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
|
||||||
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
|
0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
|
||||||
0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61,
|
0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61,
|
||||||
|
|
@ -336,13 +345,17 @@ var file_packed_proto_rawDesc = []byte{
|
||||||
0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e,
|
0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e,
|
||||||
0x6c, 0x77, 0x69, 0x74, 0x68, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2e,
|
0x6c, 0x77, 0x69, 0x74, 0x68, 0x65, 0x72, 0x73, 0x2e, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2e,
|
||||||
0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52,
|
0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52,
|
||||||
0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x22, 0x3a, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x44,
|
0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x12, 0x34, 0x0a, 0x04, 0x7a, 0x73, 0x74, 0x64, 0x18,
|
||||||
0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20,
|
0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6c, 0x77, 0x69, 0x74, 0x68, 0x65, 0x72, 0x73,
|
||||||
0x01, 0x28, 0x06, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c,
|
0x2e, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2e, 0x46,
|
||||||
0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x06, 0x52, 0x06, 0x6c, 0x65, 0x6e,
|
0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x7a, 0x73, 0x74, 0x64, 0x22, 0x3a, 0x0a,
|
||||||
0x67, 0x74, 0x68, 0x42, 0x22, 0x5a, 0x20, 0x73, 0x72, 0x63, 0x2e, 0x6c, 0x77, 0x69, 0x74, 0x68,
|
0x08, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66,
|
||||||
0x65, 0x72, 0x73, 0x2e, 0x6d, 0x65, 0x2e, 0x75, 0x6b, 0x2f, 0x68, 0x74, 0x70, 0x61, 0x63, 0x6b,
|
0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65,
|
||||||
0x2f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
|
0x06, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x22, 0x5a, 0x20, 0x73, 0x72, 0x63,
|
||||||
|
0x2e, 0x6c, 0x77, 0x69, 0x74, 0x68, 0x65, 0x72, 0x73, 0x2e, 0x6d, 0x65, 0x2e, 0x75, 0x6b, 0x2f,
|
||||||
|
0x68, 0x74, 0x70, 0x61, 0x63, 0x6b, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x62, 0x06, 0x70,
|
||||||
|
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -370,12 +383,13 @@ var file_packed_proto_depIdxs = []int32{
|
||||||
3, // 1: lwithers.htpack.packed.File.uncompressed:type_name -> lwithers.htpack.packed.FileData
|
3, // 1: lwithers.htpack.packed.File.uncompressed:type_name -> lwithers.htpack.packed.FileData
|
||||||
3, // 2: lwithers.htpack.packed.File.gzip:type_name -> lwithers.htpack.packed.FileData
|
3, // 2: lwithers.htpack.packed.File.gzip:type_name -> lwithers.htpack.packed.FileData
|
||||||
3, // 3: lwithers.htpack.packed.File.brotli:type_name -> lwithers.htpack.packed.FileData
|
3, // 3: lwithers.htpack.packed.File.brotli:type_name -> lwithers.htpack.packed.FileData
|
||||||
2, // 4: lwithers.htpack.packed.Directory.FilesEntry.value:type_name -> lwithers.htpack.packed.File
|
3, // 4: lwithers.htpack.packed.File.zstd:type_name -> lwithers.htpack.packed.FileData
|
||||||
5, // [5:5] is the sub-list for method output_type
|
2, // 5: lwithers.htpack.packed.Directory.FilesEntry.value:type_name -> lwithers.htpack.packed.File
|
||||||
5, // [5:5] is the sub-list for method input_type
|
6, // [6:6] is the sub-list for method output_type
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
6, // [6:6] is the sub-list for method input_type
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
6, // [6:6] is the sub-list for extension type_name
|
||||||
0, // [0:5] is the sub-list for field type_name
|
6, // [6:6] is the sub-list for extension extendee
|
||||||
|
0, // [0:6] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_packed_proto_init() }
|
func init() { file_packed_proto_init() }
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ message File {
|
||||||
|
|
||||||
// Brotli compressed version of the file.
|
// Brotli compressed version of the file.
|
||||||
FileData brotli = 5;
|
FileData brotli = 5;
|
||||||
|
|
||||||
|
// Zstd compressed version of the file.
|
||||||
|
FileData zstd = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileData records the position of the file data within the pack.
|
// FileData records the position of the file data within the pack.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue