diff --git a/cmd/packserver/go.mod b/cmd/packserver/go.mod index 958080f..794e446 100644 --- a/cmd/packserver/go.mod +++ b/cmd/packserver/go.mod @@ -1,15 +1,15 @@ module src.lwithers.me.uk/go/htpack/cmd/packserver -go 1.26.0 +go 1.22 require ( - github.com/spf13/cobra v1.10.2 - src.lwithers.me.uk/go/htpack v1.5.0 + github.com/spf13/cobra v1.8.1 + src.lwithers.me.uk/go/htpack v1.3.3 ) require ( + github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - golang.org/x/sys v0.47.0 // indirect - google.golang.org/protobuf v1.36.12 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.22.0 // indirect ) diff --git a/cmd/packserver/go.sum b/cmd/packserver/go.sum index 180bbaa..cda383a 100644 --- a/cmd/packserver/go.sum +++ b/cmd/packserver/go.sum @@ -1,19 +1,45 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +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/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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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= -src.lwithers.me.uk/go/htpack v1.5.0 h1:2UYM4AGIPc/XgktmO6AxUyqFxzmYkqg/jINcDCsZsCk= -src.lwithers.me.uk/go/htpack v1.5.0/go.mod h1:5dQlPdwV+ZeBAaFBdwiRI6/w5dpSgPsiotO4ZTYau88= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= diff --git a/cmd/packserver/main.go b/cmd/packserver/main.go index a374a66..641a100 100644 --- a/cmd/packserver/main.go +++ b/cmd/packserver/main.go @@ -55,10 +55,6 @@ func main() { "Name of index file (index.html or similar)") rootCmd.Flags().Duration("expiry", 0, "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", "", "Name of file to return if response would be 404 (spa.html or similar)") rootCmd.Flags().String("frames", "sameorigin", @@ -139,35 +135,15 @@ func run(c *cobra.Command, args []string) error { // parse expiry time // 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") if err != nil { return err } - if expiry > 0 { - cacheControl200 = fmt.Sprintf("public, max-age=%d%s", expiry/1e9, immutableDirective) - } - - expiry404, err := c.Flags().GetDuration("expiry404") - 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) + if expiry <= 0 { + extraHeaders.Set("Cache-Control", "no-cache") + } else { + extraHeaders.Set("Cache-Control", + fmt.Sprintf("public, max-age=%d", expiry/1e9)) } // optional index file @@ -223,7 +199,6 @@ func run(c *cobra.Command, args []string) error { if err != nil { return err } - packHandler.SetCacheControl(cacheControl200, cacheControl404) if indexFile != "" { packHandler.SetIndex(indexFile) } diff --git a/go.mod b/go.mod index 075ad33..9f23a57 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module src.lwithers.me.uk/go/htpack -go 1.26.0 +go 1.25.0 require ( - golang.org/x/sys v0.47.0 - google.golang.org/protobuf v1.36.12 + github.com/google/go-cmp v0.7.0 + golang.org/x/sys v0.45.0 + google.golang.org/protobuf v1.36.11 ) diff --git a/go.sum b/go.sum index 3bd003e..f246438 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/handler.go b/handler.go index 9f13f42..a429f19 100644 --- a/handler.go +++ b/handler.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "path" - "path/filepath" "strconv" "strings" "time" @@ -15,6 +14,7 @@ import ( const ( encodingGzip = "gzip" encodingBrotli = "br" + encodingZstd = "zstd" ) // New returns a new handler. Standard security headers are set. @@ -23,14 +23,21 @@ func New(packfile string) (*Handler, error) { if err != nil { 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{ - mapped: mapped.Data, - dir: mapped.Directory.Files, - headers: make(map[string]string), - cacheControl200: "no-cache", - cacheControl404: "no-cache", - startTime: time.Now(), + files: make(map[string]handlerFile), + headers: make(map[string]string), + startTime: time.Now(), } // 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 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. type Handler struct { - mapped []byte - dir map[string]*packed.File - headers map[string]string - cacheControl200 string - cacheControl404 string - startTime time.Time - notFound *packed.File + files map[string]handlerFile + headers map[string]string + startTime time.Time + notFound *handlerFile +} + +// 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 @@ -79,11 +92,11 @@ func (h *Handler) SetHeader(key, value string) { // multiple times with different filenames (noting later calls would not // overwrite files matching earlier calls). func (h *Handler) SetIndex(filename string) { - for k, v := range h.dir { - if filepath.Base(k) == filename { - routeToAdd := filepath.Dir(k) - if _, exists := h.dir[routeToAdd]; !exists { - h.dir[routeToAdd] = v + for k, v := range h.files { + if path.Base(k) == filename { + routeToAdd := path.Dir(k) + if _, exists := h.files[routeToAdd]; !exists { + h.files[routeToAdd] = v } } } @@ -103,25 +116,60 @@ func (h *Handler) SetNotFound(notFound string) error { } notFound = path.Clean(notFound) - dir := h.dir[path.Clean(notFound)] - if dir == nil { + f, ok := h.files[path.Clean(notFound)] + if !ok { return fmt.Errorf("no such resource %q", notFound) } - h.notFound = dir + h.notFound = &f return nil } -// SetCacheControl allows control over the cache-control header. If not called, -// "Cache-Control: no-cache" is the default. -// -// The first and second arguments are required, and specify the header to use -// for 200 responses and 404 responses respectively. -// -// TODO: could be expanded to take extra varargs for more specific control over -// particular assets, like different expiry per content-type. -func (h *Handler) SetCacheControl(hdr200, hdr404 string) { - h.cacheControl200 = hdr200 - h.cacheControl404 = hdr404 +type handlerFile struct { + contentType, etag string + uncompressed, gzip, brotli, zstd []byte +} + +func (h *Handler) addFiles(prefix string, mapped *packed.Mapped) { + // our directory contains files like "/index.html". We will add + // a prefix, which might be "" or "/foo". + prefix = path.Clean(prefix) + prefix = strings.TrimSuffix(prefix, "/") + 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 @@ -142,46 +190,48 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - info := h.dir[path.Clean(req.URL.Path)] - if info == nil { - w.Header().Set("Cache-Control", h.cacheControl404) + info, ok := h.files[path.Clean(req.URL.Path)] + if !ok { if h.notFound == nil { http.NotFound(w, req) return } - info = h.notFound + info = *h.notFound } // set standard headers - w.Header().Set("Cache-Control", h.cacheControl200) w.Header().Set("Vary", "Accept-Encoding") - w.Header().Set("Etag", info.Etag) - w.Header().Set("Content-Type", info.ContentType) + w.Header().Set("Etag", info.etag) + w.Header().Set("Content-Type", info.contentType) w.Header().Set("Accept-Ranges", "bytes") // process etag / modtime - if clientHasCachedVersion(info.Etag, h.startTime, req) { + if clientHasCachedVersion(info.etag, h.startTime, req) { w.WriteHeader(http.StatusNotModified) return } // select compression - data := info.Uncompressed - gzip, brotli := acceptedEncodings(req) - if brotli && info.Brotli != nil { - data = info.Brotli + data := info.uncompressed + gzip, brotli, zstd := acceptedEncodings(req) + switch { + 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) - } else if gzip && info.Gzip != nil { - data = info.Gzip + case gzip && info.gzip != nil: + data = info.gzip w.Header().Set("Content-Encoding", encodingGzip) } // range support (single-part ranges only) // 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 { 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 @@ -197,11 +247,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - offset += data.Offset - w.Write(h.mapped[offset : offset+length]) + w.Write(data[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") for _, enc := range strings.Split(encodings, ",") { 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 // with whether or not it's partial content. -func getFileRange(data *packed.FileData, req *http.Request) (offset, length uint64, isPartial bool) { - length = data.Length +func getFileRange(dataLength uint64, req *http.Request) (offset, length uint64, isPartial bool) { + length = dataLength // only accept "Range: bytes=…" 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 switch { case from > to, - from >= data.Length, - to >= data.Length: + from >= dataLength, + to >= dataLength: return } diff --git a/handler_test.go b/handler_test.go new file mode 100644 index 0000000..c0b986f --- /dev/null +++ b/handler_test.go @@ -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))) + } +} diff --git a/packed/packed.pb.go b/packed/packed.pb.go index 32fd75a..aa3429f 100644 --- a/packed/packed.pb.go +++ b/packed/packed.pb.go @@ -168,6 +168,8 @@ type File struct { Gzip *FileData `protobuf:"bytes,4,opt,name=gzip,proto3" json:"gzip,omitempty"` // Brotli compressed version of the file. 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() { @@ -237,6 +239,13 @@ func (x *File) GetBrotli() *FileData { 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. type FileData struct { 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, 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, - 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, 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, @@ -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, 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, - 0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x22, 0x3a, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x44, - 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x06, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 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, + 0x06, 0x62, 0x72, 0x6f, 0x74, 0x6c, 0x69, 0x12, 0x34, 0x0a, 0x04, 0x7a, 0x73, 0x74, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 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, 0x04, 0x7a, 0x73, 0x74, 0x64, 0x22, 0x3a, 0x0a, + 0x08, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 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 ( @@ -370,12 +383,13 @@ var file_packed_proto_depIdxs = []int32{ 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, // 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 - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 3, // 4: lwithers.htpack.packed.File.zstd:type_name -> lwithers.htpack.packed.FileData + 2, // 5: lwithers.htpack.packed.Directory.FilesEntry.value:type_name -> lwithers.htpack.packed.File + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension 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() } diff --git a/packed/packed.proto b/packed/packed.proto index 771e1ec..f83ed0e 100644 --- a/packed/packed.proto +++ b/packed/packed.proto @@ -47,6 +47,9 @@ message File { // Brotli compressed version of the file. FileData brotli = 5; + + // Zstd compressed version of the file. + FileData zstd = 6; } // FileData records the position of the file data within the pack.