Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7059a302 | ||
|
|
8dd8ebacc1 | ||
|
|
7d96b5bdcb | ||
|
|
6c70ddce17 | ||
|
|
357f702925 | ||
|
|
b32ba090a2 | ||
|
|
4ed5335a07 | ||
|
|
96ea58d32b | ||
|
|
01e98aed67 |
12 changed files with 582 additions and 1228 deletions
|
|
@ -1,15 +1,15 @@
|
|||
module src.lwithers.me.uk/go/htpack/cmd/packserver
|
||||
|
||||
go 1.22
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
src.lwithers.me.uk/go/htpack v1.3.3
|
||||
github.com/spf13/cobra v1.10.2
|
||||
src.lwithers.me.uk/go/htpack v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.22.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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,45 +1,19 @@
|
|||
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/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/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.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=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ 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",
|
||||
|
|
@ -135,15 +139,35 @@ 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 {
|
||||
extraHeaders.Set("Cache-Control", "no-cache")
|
||||
} else {
|
||||
extraHeaders.Set("Cache-Control",
|
||||
fmt.Sprintf("public, max-age=%d", expiry/1e9))
|
||||
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)
|
||||
}
|
||||
|
||||
// optional index file
|
||||
|
|
@ -199,6 +223,7 @@ func run(c *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
packHandler.SetCacheControl(cacheControl200, cacheControl404)
|
||||
if indexFile != "" {
|
||||
packHandler.SetIndex(indexFile)
|
||||
}
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module src.lwithers.me.uk/go/htpack
|
||||
|
||||
require (
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
golang.org/x/sys v0.22.0
|
||||
)
|
||||
go 1.26.0
|
||||
|
||||
go 1.22
|
||||
require (
|
||||
golang.org/x/sys v0.47.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
)
|
||||
|
|
|
|||
39
go.sum
39
go.sum
|
|
@ -1,33 +1,6 @@
|
|||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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/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=
|
||||
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=
|
||||
|
|
|
|||
58
handler.go
58
handler.go
|
|
@ -3,14 +3,12 @@ package htpack
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"src.lwithers.me.uk/go/htpack/packed"
|
||||
)
|
||||
|
||||
|
|
@ -21,33 +19,18 @@ const (
|
|||
|
||||
// New returns a new handler. Standard security headers are set.
|
||||
func New(packfile string) (*Handler, error) {
|
||||
f, err := os.Open(packfile)
|
||||
mapped, err := packed.MapAndLoad(packfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapped, err := unix.Mmap(int(f.Fd()), 0, int(fi.Size()),
|
||||
unix.PROT_READ, unix.MAP_SHARED)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, dir, err := packed.Load(f)
|
||||
if err != nil {
|
||||
unix.Munmap(mapped)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := &Handler{
|
||||
mapped: mapped,
|
||||
dir: dir.Files,
|
||||
headers: make(map[string]string),
|
||||
startTime: time.Now(),
|
||||
mapped: mapped.Data,
|
||||
dir: mapped.Directory.Files,
|
||||
headers: make(map[string]string),
|
||||
cacheControl200: "no-cache",
|
||||
cacheControl404: "no-cache",
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
||||
|
|
@ -61,11 +44,13 @@ func New(packfile string) (*Handler, error) {
|
|||
|
||||
// Handler implements http.Handler and allows options to be set.
|
||||
type Handler struct {
|
||||
mapped []byte
|
||||
dir map[string]*packed.File
|
||||
headers map[string]string
|
||||
startTime time.Time
|
||||
notFound *packed.File
|
||||
mapped []byte
|
||||
dir map[string]*packed.File
|
||||
headers map[string]string
|
||||
cacheControl200 string
|
||||
cacheControl404 string
|
||||
startTime time.Time
|
||||
notFound *packed.File
|
||||
}
|
||||
|
||||
// SetHeader allows a custom header to be set on HTTP responses. These are
|
||||
|
|
@ -126,6 +111,19 @@ func (h *Handler) SetNotFound(notFound string) error {
|
|||
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
|
||||
}
|
||||
|
||||
// ServeHTTP handles requests for files. It supports GET and HEAD methods, with
|
||||
// anything else returning a 405. Exact path matches are required, else a 404 is
|
||||
// returned.
|
||||
|
|
@ -146,6 +144,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
info := h.dir[path.Clean(req.URL.Path)]
|
||||
if info == nil {
|
||||
w.Header().Set("Cache-Control", h.cacheControl404)
|
||||
if h.notFound == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
|
|
@ -154,6 +153,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
140
packed/load.go
140
packed/load.go
|
|
@ -2,9 +2,13 @@ package packed
|
|||
|
||||
import (
|
||||
fmt "fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -17,14 +21,14 @@ const (
|
|||
VersionInitial = 1
|
||||
)
|
||||
|
||||
// Load a ready-packed file.
|
||||
func Load(f *os.File) (*Header, *Directory, error) {
|
||||
hdr, err := loadHeader(f)
|
||||
// Read a ready-packed file.
|
||||
func Read(f []byte) (*Header, *Directory, error) {
|
||||
hdr, err := readHeader(f)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dir, err := loadDirectory(f, hdr)
|
||||
dir, err := readDirectory(f, hdr)
|
||||
if le, ok := err.(*LoadError); ok {
|
||||
// augment error
|
||||
le.Magic = hdr.Magic
|
||||
|
|
@ -33,20 +37,101 @@ func Load(f *os.File) (*Header, *Directory, error) {
|
|||
return hdr, dir, err // we may have a partial dir
|
||||
}
|
||||
|
||||
// loadHeader retrieves and decodes the header from the start of the file. It
|
||||
// ensures the magic number and the version number match. Errors are returned
|
||||
// as type LoadError.
|
||||
func loadHeader(f *os.File) (*Header, error) {
|
||||
raw := make([]byte, 36)
|
||||
if _, err := f.ReadAt(raw, 0); err != nil {
|
||||
return nil, &LoadError{
|
||||
Cause: IOError,
|
||||
Underlying: err,
|
||||
// Mapped holds an mmap()ed packfile.
|
||||
type Mapped struct {
|
||||
Data []byte
|
||||
Header *Header
|
||||
Directory *Directory
|
||||
}
|
||||
|
||||
func (m *Mapped) Close() error {
|
||||
return unix.Munmap(m.Data)
|
||||
}
|
||||
|
||||
// MapAndLoad mmaps the named file and reads the pack data.
|
||||
func MapAndLoad(fname string) (*Mapped, error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{
|
||||
Path: fname,
|
||||
Op: "stat",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{
|
||||
Path: fname,
|
||||
Op: "mmap",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
hdr, dir, err := Read(data)
|
||||
if err != nil {
|
||||
unix.Munmap(data)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Mapped{
|
||||
Data: data,
|
||||
Header: hdr,
|
||||
Directory: dir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load a ready-packed file.
|
||||
//
|
||||
// Deprecated: use [Read] or [MapAndLoad].
|
||||
func Load(f *os.File) (*Header, *Directory, error) {
|
||||
// we need to know the size of the file in order to mmap it
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, nil, &fs.PathError{
|
||||
Path: f.Name(),
|
||||
Op: "stat",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// compatibility: old code may have worked with non-zero offset
|
||||
offset, err := f.Seek(0, os.SEEK_CUR)
|
||||
if err != nil {
|
||||
return nil, nil, &fs.PathError{
|
||||
Path: f.Name(),
|
||||
Op: "seek",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// mmap the file
|
||||
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
|
||||
if err != nil {
|
||||
return nil, nil, &fs.PathError{
|
||||
Path: f.Name(),
|
||||
Op: "mmap",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer unix.Munmap(data)
|
||||
|
||||
// read as though it's from memory
|
||||
return Read(data[offset:])
|
||||
}
|
||||
|
||||
// readHeader retrieves and decodes the header from the start of the file. It
|
||||
// ensures the magic number and the version number match. Errors are returned
|
||||
// as type LoadError.
|
||||
func readHeader(f []byte) (*Header, error) {
|
||||
hdr := new(Header)
|
||||
if err := hdr.Unmarshal(raw); err != nil {
|
||||
if err := proto.Unmarshal(f[:36], hdr); err != nil {
|
||||
return nil, &LoadError{
|
||||
Cause: HeaderUnmarshalError,
|
||||
Underlying: err,
|
||||
|
|
@ -77,35 +162,22 @@ func loadHeader(f *os.File) (*Header, error) {
|
|||
return hdr, nil
|
||||
}
|
||||
|
||||
// loadDirectory reads the directory from a file. The directory is checked
|
||||
// readDirectory reads the directory from a file. The directory is checked
|
||||
// for consistency (offsets, filenames) but not integrity (file data is not
|
||||
// read/checksummed).
|
||||
func loadDirectory(f *os.File, hdr *Header) (*Directory, error) {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, &LoadError{
|
||||
Cause: IOError,
|
||||
Underlying: err,
|
||||
}
|
||||
}
|
||||
fileSize := uint64(fi.Size())
|
||||
func readDirectory(f []byte, hdr *Header) (*Directory, error) {
|
||||
fileSize := uint64(len(f))
|
||||
start := hdr.DirectoryOffset
|
||||
end := start + hdr.DirectoryLength
|
||||
|
||||
if hdr.DirectoryOffset+hdr.DirectoryLength > fileSize {
|
||||
if end > fileSize {
|
||||
return nil, &LoadError{
|
||||
Cause: BadOffsetError,
|
||||
}
|
||||
}
|
||||
|
||||
raw := make([]byte, hdr.DirectoryLength)
|
||||
if _, err := f.ReadAt(raw, int64(hdr.DirectoryOffset)); err != nil {
|
||||
return nil, &LoadError{
|
||||
Cause: IOError,
|
||||
Underlying: err,
|
||||
}
|
||||
}
|
||||
|
||||
dir := new(Directory)
|
||||
if err := dir.Unmarshal(raw); err != nil {
|
||||
if err := proto.Unmarshal(f[start:end], dir); err != nil {
|
||||
return nil, &LoadError{
|
||||
Cause: DirectoryUnmarshalError,
|
||||
Underlying: err,
|
||||
|
|
|
|||
39
packed/load_test.go
Normal file
39
packed/load_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package packed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapAndLoad(t *testing.T) {
|
||||
rawPack, err := os.ReadFile("testdata/helloworld.pack")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawText, err := os.ReadFile("testdata/helloworld.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mapped, err := MapAndLoad("testdata/helloworld.pack")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer mapped.Close()
|
||||
|
||||
if !bytes.Equal(mapped.Data, rawPack) {
|
||||
t.Error("mapped data does not match raw file")
|
||||
}
|
||||
|
||||
file := mapped.Directory.Files["/helloworld.txt"]
|
||||
if file == nil {
|
||||
t.Fatal("directory does not contain /helloworld.txt")
|
||||
}
|
||||
|
||||
start := file.Uncompressed.Offset
|
||||
end := start + file.Uncompressed.Length
|
||||
if !bytes.Equal(rawText, mapped.Data[start:end]) {
|
||||
t.Error("uncompressed file does not match testdata/helloworld.txt")
|
||||
}
|
||||
}
|
||||
1435
packed/packed.pb.go
1435
packed/packed.pb.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package packed;
|
||||
package lwithers.htpack.packed;
|
||||
option go_package = "src.lwithers.me.uk/htpack/packed";
|
||||
|
||||
// Header at start of file. This must be a fixed, known size. Fields cannot
|
||||
// be zero.
|
||||
|
|
@ -54,6 +55,6 @@ message FileData {
|
|||
// the pack.
|
||||
fixed64 offset = 1;
|
||||
|
||||
// Length is the
|
||||
// Length of the (possibly compressed) file data, in bytes.
|
||||
fixed64 length = 2;
|
||||
}
|
||||
|
|
|
|||
BIN
packed/testdata/helloworld.pack
vendored
Normal file
BIN
packed/testdata/helloworld.pack
vendored
Normal file
Binary file not shown.
13
packed/testdata/helloworld.txt
vendored
Normal file
13
packed/testdata/helloworld.txt
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Hello, world
|
||||
|
||||
This file contains a few lines of plain text, really just enough
|
||||
to trigger the "compression worthwhile" heuristic.
|
||||
|
||||
This file contains a few lines of plain text, really just enough
|
||||
to trigger the "compression worthwhile" heuristic.
|
||||
|
||||
This file contains a few lines of plain text, really just enough
|
||||
to trigger the "compression worthwhile" heuristic.
|
||||
|
||||
This file contains a few lines of plain text, really just enough
|
||||
to trigger the "compression worthwhile" heuristic.
|
||||
Loading…
Add table
Add a link
Reference in a new issue