Compare commits

..

24 Commits

Author SHA1 Message Date
Laurence Withers 19b2560e2d cmd/htpacker: swap out progress bar library
Switch to a simpler progress bar library. I've been having trouble tracking
down a panic that seemed to affect the prior code; the new code is much
easier and uses far fewer async channels and things.
2024-08-24 09:54:25 +01:00
Laurence Withers 3974db129e cmd/packserver: updated modules 2024-07-18 14:58:38 +01:00
Laurence Withers 7dffaaa5d7 cmd/htpacker: updated modules 2024-07-18 14:57:49 +01:00
Laurence Withers 2a1eafa306 Updated modules 2024-07-18 14:54:50 +01:00
Laurence Withers 3585b7943a Update JavaScript MIME type
It seems that application/javascript has been supplanted by text/javascript. See
https://www.rfc-editor.org/rfc/rfc9239 and
https://www.iana.org/assignments/media-types/application/javascript .
2024-07-18 14:52:12 +01:00
Laurence Withers 6cbbe7328a Correct some display issues with mpb update 2024-05-03 17:14:12 +01:00
Laurence Withers a83aedd502 cmd/htpacker: update modules 2024-05-03 16:49:33 +01:00
Laurence Withers 8474cfbc5d Update mpb lib to v8 2023-04-28 16:12:19 +01:00
Laurence Withers 2f842a21f3 cmd/htpacker: updated modules 2023-04-28 15:57:34 +01:00
Laurence Withers 439bf2422b cmd/htpacker: don't try to compress tiny files
If we have some really tiny files, it's not worth compressing them. Among other things,
this will work around a bug in go-zopfli for 0- or 1-byte files.
2023-04-28 15:57:28 +01:00
Laurence Withers 2b280de481 Correct handling of multiple packfiles
During the 1.3.1 update, a change was made to stop using http.Handle (and ServeMux
underneath), in order to have manual control over the http.Server object.

Unfortunately, it was overlooked that nothing was doing routing / multiplexing, so
when using multiple packfiles only one handler (picked arbitrarily due to map)
would actually be active on any given invocation.

Correct this by adding an explicit handler. We don't use ServeMux so as to avoid
bringing in any of its more complex behaviours like path cleaning etc.
2022-12-12 10:26:55 +00:00
Laurence Withers 5398dddb02 Add --graceful-shutdown-delay
This option allows a configurable delay after receiving SIGTERM (or
SIGINT) but before the HTTP server stops accepting new connections. It
is quite useful for distributed systems where callers are only notified
asynchronously (e.g. via service discovery) that a service is being shut
down; it prevents the shut down from occurring prior to callers
processing the notification.

This required some minor refactoring to allow the Shutdown() method on
http.Server to be accessed.
2022-11-26 10:43:18 +00:00
Laurence Withers e0ae6bb4b6 cmd/packserver: update to Go 1.19, update dependencies 2022-11-26 10:14:25 +00:00
Laurence Withers 6b836895a0 cmd/htpacker: update to Go 1.19, update dependencies 2022-11-26 10:12:42 +00:00
Laurence Withers 301dc0c7c8 Update to Go 1.19 2022-11-26 10:08:16 +00:00
Laurence Withers a6c2991781 cmd/htpacker: updated dependencies 2022-07-06 13:31:22 +01:00
Laurence Withers d827d8aace cmd/htpacker: add --content-type flag
This allows overriding the content-type of files being packed using the
ad-hoc method (not YAML spec).
2022-07-06 13:29:55 +01:00
Laurence Withers 565a269cef cmd/packserver: add --frames option
Allows override of the X-Frame-Options header on the handler.
2022-07-06 10:33:54 +01:00
Laurence Withers 16d836da9a cmd/packserver: dependency update 2022-07-06 10:17:32 +01:00
Laurence Withers 8cae4d0f8f Correct X-Frame-Options value sameorigin→SAMEORIGIN 2022-07-06 10:15:28 +01:00
Laurence Withers 6ea49bb3b3 Updated dependencies 2022-07-06 10:00:24 +01:00
Laurence Withers 83a5226e1a handler: drop sendfile(2) support
After some experimentation, I found that the sendfile(2) support did not
really save any time compared to just write(2) from an already
memory-mapped file.

After some reading, I think open/sendfile is supposed to be slightly
more efficient than open/mmap/write — but if we already did the mmap
step, then it doesn't save us much.

Moreover, the code to support sendfile(2) is a bit icky, and also forces
us to close the HTTP connection after serving a file.
2022-07-06 09:53:34 +01:00
Laurence Withers 1b84160dcf cmd/htpacker: cope with zero-length input files
Sometimes we might be asked to serve up a zero-length input file,
typically from some machine-generated CSS etc. We make some very
rudimentary guess about the content-type the caller wanted and skip the
mmap(2) call.
2020-04-02 12:33:04 +01:00
Laurence Withers 52213cf67e cmd/htpacker: report filename on I/O error 2020-04-02 12:25:23 +01:00
16 changed files with 711 additions and 376 deletions

View File

@ -0,0 +1,105 @@
package main
import (
"fmt"
"path/filepath"
"strings"
"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
)
type ctGlobEntry struct {
pattern, contentType string
pathComponents int
}
type ctGlobList []ctGlobEntry
func parseGlobs(flags []string) (ctGlobList, error) {
var ctGlobs ctGlobList
for _, flag := range flags {
// split pattern:content-type
pos := strings.LastIndexByte(flag, ':')
if pos == -1 {
return nil, &parseGlobError{
Value: flag,
Err: "must be pattern:content-type",
}
}
pattern, ct := flag[:pos], flag[pos+1:]
// patterns starting with "/" must match the entire directory
// prefix; otherwise, an arbitrary number of path components are
// allowed prior to the prefix
var pathComponents int
if strings.HasPrefix(pattern, "/") {
pathComponents = -1
pattern = strings.TrimPrefix(pattern, "/")
} else {
pathComponents = 1 + strings.Count(pattern, "/")
}
// test that the pattern's syntax is valid
if _, err := filepath.Match(pattern, "test"); err != nil {
return nil, &parseGlobError{
Value: flag,
Err: err.Error(),
}
}
ctGlobs = append(ctGlobs, ctGlobEntry{
pattern: pattern,
contentType: ct,
pathComponents: pathComponents,
})
}
return ctGlobs, nil
}
// ApplyContentTypes will scan the list of files to pack, matching by filename,
// and on match will apply the given content type.
func (ctGlobs ctGlobList) ApplyContentTypes(ftp packer.FilesToPack) {
for name := range ftp {
for _, entry := range ctGlobs {
testName := trimPathComponents(name, entry.pathComponents)
matched, _ := filepath.Match(entry.pattern, testName)
if matched {
f := ftp[name]
f.ContentType = entry.contentType
ftp[name] = f
break
}
}
}
}
func trimPathComponents(name string, components int) string {
name = strings.TrimPrefix(name, "/") // FilesToPack keys = absolute path
// if we are matching the full prefix, don't otherwise manipulate the
// name
if components < 0 {
return name
}
// otherwise, trim the number of components remaining in the path so
// that we are only matching the trailing path components from the
// FilesToPack key
parts := 1 + strings.Count(name, "/")
for ; parts > components; parts-- {
pos := strings.IndexByte(name, '/')
name = name[pos+1:]
}
return name
}
// parseGlobError is returned from parseGlobs on error.
type parseGlobError struct {
Value string
Err string
}
func (pge *parseGlobError) Error() string {
return fmt.Sprintf("--content-type entry %q: %s", pge.Value, pge.Err)
}

View File

@ -0,0 +1,109 @@
package main
import (
"testing"
"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
)
func TestParseGlobs(t *testing.T) {
ctGlobs, err := parseGlobs([]string{
"*.foo:text/html",
"*.bar:text/plain",
"baz/qux/*.js:application/javascript",
"/abs/file:image/png",
})
if err != nil {
t.Fatal(err)
}
check := func(pos int, pattern, contentType string, pathComponents int) {
if pos >= len(ctGlobs) {
t.Errorf("entry %d not present", pos)
return
}
if pattern != ctGlobs[pos].pattern {
t.Errorf("entry %d: expected pattern %q but got %q",
pos, pattern, ctGlobs[pos].pattern)
}
if contentType != ctGlobs[pos].contentType {
t.Errorf("entry %d: expected content type %q but got %q",
pos, contentType, ctGlobs[pos].contentType)
}
if pathComponents != ctGlobs[pos].pathComponents {
t.Errorf("entry %d: expected num. path components %d but got %d",
pos, pathComponents, ctGlobs[pos].pathComponents)
}
}
check(0, "*.foo", "text/html", 1)
check(1, "*.bar", "text/plain", 1)
check(2, "baz/qux/*.js", "application/javascript", 3)
check(3, "abs/file", "image/png", -1)
}
func TestParseGlobsErrSep(t *testing.T) {
const badValue = "hello/dave.js" // missing ":" separator
_, err := parseGlobs([]string{badValue})
switch err := err.(type) {
case *parseGlobError:
if err.Value != badValue {
t.Errorf("expected value %q but got %q", badValue, err.Value)
}
case nil:
t.Fatal("expected error")
default:
t.Errorf("unexpected error type %T (value %v)", err, err)
}
}
func TestParseGlobsErrPattern(t *testing.T) {
const badValue = "[-z]:foo/bar" // malformed character class
_, err := parseGlobs([]string{badValue})
switch err := err.(type) {
case *parseGlobError:
if err.Value != badValue {
t.Errorf("expected value %q but got %q", badValue, err.Value)
}
case nil:
t.Fatal("expected error")
default:
t.Errorf("unexpected error type %T (value %v)", err, err)
}
}
func TestApplyContentTypes(t *testing.T) {
// XXX: we program our _expectation_ of content-type into the Filename field
ftp := packer.FilesToPack{
"foo.txt": packer.FileToPack{Filename: "text/plain"},
"baz/foo.txt": packer.FileToPack{Filename: "text/plain"},
"baz/qux.png": packer.FileToPack{Filename: "image/png"},
"foo/qux.png": packer.FileToPack{},
"foo/baz/qux.png": packer.FileToPack{Filename: "image/png"},
"bar.jpeg": packer.FileToPack{},
"foo/baz/bar.jpeg": packer.FileToPack{},
"baz/bar.jpeg": packer.FileToPack{Filename: "image/jpeg"},
}
ctGlobs, err := parseGlobs([]string{
"*.txt:text/plain", // should match anywhere
"baz/qux.png:image/png", // won't match /foo/qux.png
"/baz/bar.jpeg:image/jpeg", // exact prefix match
})
if err != nil {
t.Fatal(err)
}
ctGlobs.ApplyContentTypes(ftp)
for k, v := range ftp {
if v.Filename != v.ContentType {
t.Errorf("filename %q: expected content type %q but got %q",
k, v.Filename, v.ContentType)
}
}
}

View File

@ -1,17 +1,24 @@
module src.lwithers.me.uk/go/htpack/cmd/htpacker module src.lwithers.me.uk/go/htpack/cmd/htpacker
go 1.12 go 1.22
require ( require (
github.com/andybalholm/brotli v1.0.0 github.com/andybalholm/brotli v1.1.0
github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2 github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2
github.com/kr/pretty v0.1.0 // indirect github.com/gosuri/uiprogress v0.0.1
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 github.com/spf13/cobra v1.8.1
github.com/spf13/cobra v0.0.5 golang.org/x/sys v0.22.0
github.com/vbauerster/mpb/v4 v4.11.2 gopkg.in/yaml.v2 v2.4.0
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 src.lwithers.me.uk/go/htpack v1.3.3
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2
src.lwithers.me.uk/go/htpack v1.1.5
src.lwithers.me.uk/go/writefile v1.0.1 src.lwithers.me.uk/go/writefile v1.0.1
) )
require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gosuri/uilive v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

View File

@ -1,75 +1,68 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2 h1:VA6jElpcJ+wkwEBufbnVkSBCA2TEnxdRppjRT5Kvh0A= github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2 h1:VA6jElpcJ+wkwEBufbnVkSBCA2TEnxdRppjRT5Kvh0A=
github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2/go.mod h1:Yi95+RbwKz7uGndSuUhoq7LJKh8qH8DT9fnL4ewU30k= github.com/foobaz/go-zopfli v0.0.0-20140122214029-7432051485e2/go.mod h1:Yi95+RbwKz7uGndSuUhoq7LJKh8qH8DT9fnL4ewU30k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/vbauerster/mpb/v4 v4.11.2 h1:ynkUoKzi65DZ1UsQPx7sgi/KN6G9f7br+Us2nKm35AM=
github.com/vbauerster/mpb/v4 v4.11.2/go.mod h1:jIuIRCltGJUnm6DCyPVkwjlLUk4nHTH+m4eD14CdFF0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
src.lwithers.me.uk/go/htpack v1.1.5 h1:2JzgqLZ1ROYc53+96NezfJ3S9fkwHZNd6QgJhMXnlSE= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
src.lwithers.me.uk/go/htpack v1.1.5/go.mod h1:JWofpm01RJbCTIyKfIPftUsxk6KlFkrYwyHgCVdKY+s= 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/writefile v1.0.1 h1:bwBGtvyZfCxFIM14e1aYgJWlZuowKkwJx53OJlUPd0s= src.lwithers.me.uk/go/writefile v1.0.1 h1:bwBGtvyZfCxFIM14e1aYgJWlZuowKkwJx53OJlUPd0s=
src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA= src.lwithers.me.uk/go/writefile v1.0.1/go.mod h1:NahlmRCtB7kg4ai+zHZgxXdUs+MR8VqWG8mql35TsxA=

View File

@ -16,6 +16,20 @@ import (
var packCmd = &cobra.Command{ var packCmd = &cobra.Command{
Use: "pack", Use: "pack",
Short: "creates a packfile from a YAML spec or set of files/dirs", Short: "creates a packfile from a YAML spec or set of files/dirs",
Long: `When given a YAML spec file (a template for which can be generated
with the "yaml" command), files will be packed exactly as per the spec. The
--content-type flag cannot be used and no extra files can be specified.
When given a list of files and directories to pack, the content type for each
file will be automatically detected. It is possible to override the content
type by specifying one or more --content-type flags. These take an argument in
the form "pattern:content/type". The pattern is matched using common glob
(* = wildcard), very similar to .gitignore. If the pattern contains any
directory names, these must match the final components of the file to pack's
path. If the pattern starts with a "/", then the full path must be matched
exactly.
`,
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
// convert "out" to an absolute path, so that it will still // convert "out" to an absolute path, so that it will still
// work after chdir // work after chdir
@ -51,6 +65,16 @@ var packCmd = &cobra.Command{
} }
} }
// parse content-type globs
ctGlobList, err := c.Flags().GetStringArray("content-type")
if err != nil {
return err
}
ctGlobs, err := parseGlobs(ctGlobList)
if err != nil {
return err
}
// if "spec" is not present, then we expect a list of input // if "spec" is not present, then we expect a list of input
// files, and we'll build a spec from them // files, and we'll build a spec from them
if spec == "" { if spec == "" {
@ -58,12 +82,16 @@ var packCmd = &cobra.Command{
return errors.New("need --yaml, " + return errors.New("need --yaml, " +
"or one or more filenames") "or one or more filenames")
} }
err = PackFiles(c, args, out) err = PackFiles2(c, args, ctGlobs, out)
} else { } else {
if len(args) != 0 { if len(args) != 0 {
return errors.New("cannot specify files " + return errors.New("cannot specify files " +
"when using --yaml") "when using --yaml")
} }
if ctGlobs != nil {
return errors.New("cannot specify --content-type " +
"when using --yaml")
}
err = PackSpec(c, spec, out) err = PackSpec(c, spec, out)
} }
if err != nil { if err != nil {
@ -83,13 +111,20 @@ func init() {
"YAML specification file (if not present, just pack files)") "YAML specification file (if not present, just pack files)")
packCmd.Flags().StringP("chdir", "C", "", packCmd.Flags().StringP("chdir", "C", "",
"Change to directory before searching for input files") "Change to directory before searching for input files")
packCmd.Flags().StringArrayP("content-type", "", nil,
"Override content type for pattern, e.g. \"*.foo=bar/baz\" (like .gitignore)")
} }
func PackFiles(c *cobra.Command, args []string, out string) error { func PackFiles(c *cobra.Command, args []string, out string) error {
return PackFiles2(c, args, nil, out)
}
func PackFiles2(c *cobra.Command, args []string, ctGlobs ctGlobList, out string) error {
ftp, err := filesFromList(args) ftp, err := filesFromList(args)
if err != nil { if err != nil {
return err return err
} }
ctGlobs.ApplyContentTypes(ftp)
return doPack(ftp, out) return doPack(ftp, out)
} }
@ -109,7 +144,7 @@ func PackSpec(c *cobra.Command, spec, out string) error {
} }
func doPack(ftp packer.FilesToPack, out string) error { func doPack(ftp packer.FilesToPack, out string) error {
prog := mpbProgress(ftp) prog := newUiProgress(ftp)
err := packer.Pack2(ftp, out, prog) err := packer.Pack2(ftp, out, prog)
prog.Complete() prog.Complete()

View File

@ -1,144 +0,0 @@
package main
import (
"github.com/logrusorgru/aurora"
"github.com/vbauerster/mpb/v4"
"github.com/vbauerster/mpb/v4/decor"
"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
)
const mpbBarStyle = "[██░]"
// mpbProgress returns a new progress object that keeps the user informed via
// the visual mpb library.
func mpbProgress(ftp packer.FilesToPack) *mpbProg {
mp := new(mpbProg)
mp.un.max = len(ftp)
for _, f := range ftp {
if !f.DisableCompression && !f.DisableGzip {
mp.gzip.max++
}
if !f.DisableCompression && !f.DisableBrotli {
mp.brotli.max++
}
}
mp.p = mpb.New()
mp.un.bar = mp.p.AddBar(int64(mp.un.max),
mpb.PrependDecorators(barName("uncompressed")),
mpb.BarStyle(mpbBarStyle),
mpb.AppendDecorators(&mp.un))
if mp.gzip.max > 0 {
mp.gzip.bar = mp.p.AddBar(int64(mp.gzip.max),
mpb.PrependDecorators(barName("gzip")),
mpb.BarStyle(mpbBarStyle),
mpb.AppendDecorators(&mp.gzip))
}
if mp.brotli.max > 0 {
mp.brotli.bar = mp.p.AddBar(int64(mp.brotli.max),
mpb.PrependDecorators(barName("brotli")),
mpb.BarStyle(mpbBarStyle),
mpb.AppendDecorators(&mp.brotli))
}
return mp
}
func barName(n string) decor.Decorator {
return decor.Name(aurora.Magenta(n).String(), decor.WCSyncWidth)
}
// mpbProg is the mpb progress tracker. It has one bar for each type of
// compression, and its methods simply dispatch onto the type-specific
// bars.
type mpbProg struct {
un, gzip, brotli mpbProg1
p *mpb.Progress
}
func (mp *mpbProg) Count(_ int) {
}
func (mp *mpbProg) Begin(filename, compression string) {
switch compression {
case "uncompressed":
mp.un.Begin(filename)
case "gzip":
mp.gzip.Begin(filename)
case "brotli":
mp.brotli.Begin(filename)
default:
return
}
}
func (mp *mpbProg) End(filename, compression string) {
switch compression {
case "uncompressed":
mp.un.End(filename)
case "gzip":
mp.gzip.End(filename)
case "brotli":
mp.brotli.End(filename)
default:
return
}
}
func (mp *mpbProg) Complete() {
mp.un.Complete()
mp.gzip.Complete()
mp.brotli.Complete()
mp.p.Wait()
}
// mpbProg1 is a type-specific progress bar. In addition to holding state and
// methods for updating the bar, it also implements decor.Decor.
type mpbProg1 struct {
max int // number of items we expect
done int // number of items completed
cur []string // list of currently-packing filenames
bar *mpb.Bar
// embedding this type lets us implement decor.Decor
decor.WC
}
func (mp1 *mpbProg1) Decor(stat *decor.Statistics) string {
if stat.Completed {
return ""
}
switch len(mp1.cur) {
case 0:
return aurora.Gray(8, "(idle)").String()
case 1:
return aurora.Blue(mp1.cur[0]).String()
default:
return aurora.Sprintf(aurora.Green("%s + %d more"), aurora.Blue(mp1.cur[0]), len(mp1.cur)-1)
}
}
func (mp1 *mpbProg1) Begin(filename string) {
mp1.cur = append(mp1.cur, filename)
}
func (mp1 *mpbProg1) End(filename string) {
for i, v := range mp1.cur {
if v == filename {
mp1.cur[i] = mp1.cur[len(mp1.cur)-1]
mp1.cur = mp1.cur[:len(mp1.cur)-1]
break
}
}
mp1.done++
if mp1.bar != nil {
mp1.bar.SetCurrent(int64(mp1.done))
}
}
func (mp1 *mpbProg1) Complete() {
if mp1.bar != nil {
mp1.bar.SetTotal(int64(mp1.max), true)
}
}

View File

@ -64,6 +64,11 @@ func (ignoreProgress) Begin(_, _ string) {}
func (ignoreProgress) End(_, _ string) {} func (ignoreProgress) End(_, _ string) {}
const ( const (
// minCompressionFileSize is the minimum filesize we need before
// considering compression. Note this must be at least 2, to avoid
// known bugs in go-zopfli.
minCompressionFileSize = 128
// minCompressionSaving means we'll only use the compressed version of // minCompressionSaving means we'll only use the compressed version of
// the file if it's at least this many bytes smaller than the original. // the file if it's at least this many bytes smaller than the original.
// Chosen somewhat arbitrarily; we have to add an HTTP header, and the // Chosen somewhat arbitrarily; we have to add an HTTP header, and the
@ -325,11 +330,14 @@ func (p *packer) packFile(path string, fileToPack FileToPack) {
return return
} }
data, err := unix.Mmap(int(f.Fd()), 0, int(fi.Size()), var data []byte
unix.PROT_READ, unix.MAP_SHARED) if fi.Size() > 0 {
if err != nil { data, err = unix.Mmap(int(f.Fd()), 0, int(fi.Size()),
p.Abort(fmt.Errorf("mmap %s: %v", fileToPack.Filename, err)) unix.PROT_READ, unix.MAP_SHARED)
return if err != nil {
p.Abort(fmt.Errorf("mmap %s: %v", fileToPack.Filename, err))
return
}
} }
// prepare initial directory entry // prepare initial directory entry
@ -356,7 +364,7 @@ func (p *packer) packFile(path string, fileToPack FileToPack) {
defer p.progress.End(fileToPack.Filename, "gzip") defer p.progress.End(fileToPack.Filename, "gzip")
if err := p.Gzip(data, info); err != nil { if err := p.Gzip(data, info); err != nil {
return fmt.Errorf("gzip compression of %s "+ return fmt.Errorf("gzip compression of %s "+
"failed: %v", fileToPack.Filename, err) "failed: %w", fileToPack.Filename, err)
} }
return nil return nil
}) })
@ -367,7 +375,7 @@ func (p *packer) packFile(path string, fileToPack FileToPack) {
defer p.progress.End(fileToPack.Filename, "brotli") defer p.progress.End(fileToPack.Filename, "brotli")
if err := p.Brotli(data, info); err != nil { if err := p.Brotli(data, info); err != nil {
return fmt.Errorf("brotli compression of %s "+ return fmt.Errorf("brotli compression of %s "+
"failed: %v", fileToPack.Filename, err) "failed: %w", fileToPack.Filename, err)
} }
return nil return nil
}) })
@ -430,6 +438,10 @@ func (p *packer) Uncompressed(srcPath string, dir *packed.File) error {
// Gzip will gzip input data to a temporary file, and then append that to the // Gzip will gzip input data to a temporary file, and then append that to the
// output file. // output file.
func (p *packer) Gzip(data []byte, dir *packed.File) error { func (p *packer) Gzip(data []byte, dir *packed.File) error {
if len(data) < minCompressionFileSize {
return nil
}
// write via temporary file // write via temporary file
tmpfile, err := ioutil.TempFile("", "") tmpfile, err := ioutil.TempFile("", "")
if err != nil { if err != nil {
@ -471,6 +483,9 @@ func (p *packer) Gzip(data []byte, dir *packed.File) error {
// Brotli will compress input data to a temporary file, and then append that to // Brotli will compress input data to a temporary file, and then append that to
// the output file. // the output file.
func (p *packer) Brotli(data []byte, dir *packed.File) error { func (p *packer) Brotli(data []byte, dir *packed.File) error {
if len(data) < minCompressionFileSize {
return nil
}
// write via temporary file // write via temporary file
tmpfile, err := ioutil.TempFile("", "") tmpfile, err := ioutil.TempFile("", "")
if err != nil { if err != nil {

131
cmd/htpacker/progress.go Normal file
View File

@ -0,0 +1,131 @@
package main
import (
"bytes"
"slices"
"sync"
"github.com/gosuri/uiprogress"
"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
)
type uiProgress struct {
p *uiprogress.Progress
uncompressed, gzip, brotli *uiProgressBar
}
func newUiProgress(ftp packer.FilesToPack) *uiProgress {
up := &uiProgress{
p: uiprogress.New(),
}
up.uncompressed = newUiProgressBar(up.p, len(ftp), "uncompressed")
var nGzip, nBrotli int
for _, f := range ftp {
if !f.DisableCompression && !f.DisableGzip {
nGzip++
}
if !f.DisableCompression && !f.DisableBrotli {
nBrotli++
}
}
if nGzip > 0 {
up.gzip = newUiProgressBar(up.p, nGzip, "gzip")
}
if nBrotli > 0 {
up.brotli = newUiProgressBar(up.p, nGzip, "brotli")
}
up.p.Start()
return up
}
func (up *uiProgress) Count(_ int) {
}
func (up *uiProgress) Begin(filename, compression string) {
up.bar(compression).begin(filename)
}
func (up *uiProgress) End(filename, compression string) {
up.bar(compression).end(filename)
}
func (up *uiProgress) bar(compression string) *uiProgressBar {
switch compression {
case "uncompressed":
return up.uncompressed
case "gzip":
return up.gzip
case "brotli":
return up.brotli
}
return nil
}
func (up *uiProgress) Complete() {
up.p.Stop()
}
type uiProgressBar struct {
bar *uiprogress.Bar
lock sync.Mutex
inflight []string
}
func newUiProgressBar(p *uiprogress.Progress, total int, compression string) *uiProgressBar {
bar := &uiProgressBar{
bar: p.AddBar(total).AppendCompleted(),
}
var buf bytes.Buffer
bar.bar.PrependFunc(func(*uiprogress.Bar) string {
bar.lock.Lock()
defer bar.lock.Unlock()
buf.Reset()
buf.WriteString(compression)
if len(bar.inflight) > 0 {
buf.WriteString(" (")
for i, f := range bar.inflight {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(f)
}
buf.WriteRune(')')
}
if buf.Len() < 40 {
buf.WriteString(" ")
buf.Truncate(40)
} else if buf.Len() > 40 {
buf.Truncate(39)
buf.WriteString("…")
}
return buf.String()
})
return bar
}
func (bar *uiProgressBar) begin(filename string) {
if bar == nil {
return
}
bar.lock.Lock()
defer bar.lock.Unlock()
bar.inflight = append(bar.inflight, filename)
}
func (bar *uiProgressBar) end(filename string) {
if bar == nil {
return
}
bar.lock.Lock()
defer bar.lock.Unlock()
bar.bar.Incr()
if idx := slices.Index(bar.inflight, filename); idx != -1 {
bar.inflight = slices.Delete(bar.inflight, idx, idx+1)
}
}

View File

@ -3,6 +3,7 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
@ -125,13 +126,22 @@ func filesFromListR(prefix, arg string, ftp packer.FilesToPack) error {
case fi.Mode().IsRegular(): case fi.Mode().IsRegular():
// sniff content type // sniff content type
var ctype string
buf := make([]byte, 512) buf := make([]byte, 512)
n, err := f.Read(buf) n, err := f.Read(buf)
if err != nil { switch err {
return err case nil:
buf = buf[:n]
ctype = http.DetectContentType(buf)
case io.EOF:
// Empty file; this is typically due to things like
// npm webpack producing empty .css files.
ctype = "text/plain; charset=UTF-8"
default:
return fmt.Errorf("failed to read %s: %v", arg, err)
} }
buf = buf[:n]
ctype := http.DetectContentType(buf)
// augmented rules for JS / CSS / etc. // augmented rules for JS / CSS / etc.
switch { switch {
@ -140,7 +150,7 @@ func filesFromListR(prefix, arg string, ftp packer.FilesToPack) error {
case ".css": case ".css":
ctype = "text/css" ctype = "text/css"
case ".js": case ".js":
ctype = "application/javascript" ctype = "text/javascript"
case ".json": case ".json":
ctype = "application/json" ctype = "application/json"
case ".svg": case ".svg":

View File

@ -1,8 +1,15 @@
module src.lwithers.me.uk/go/htpack/cmd/packserver module src.lwithers.me.uk/go/htpack/cmd/packserver
go 1.13 go 1.22
require ( require (
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v1.8.1
src.lwithers.me.uk/go/htpack v1.2.0 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.5 // indirect
golang.org/x/sys v0.22.0 // indirect
) )

View File

@ -1,42 +1,45 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
src.lwithers.me.uk/go/htpack v1.2.0 h1:z0bI8KalSD5kPYOcnuqZRPun80Ww0wbnjMkYYuv+xAI= src.lwithers.me.uk/go/htpack v1.3.3 h1:Xvl6qR9HfSblmCgPyu+ACQ9o3aLQSIy3l8CrMbzj/jc=
src.lwithers.me.uk/go/htpack v1.2.0/go.mod h1:JWofpm01RJbCTIyKfIPftUsxk6KlFkrYwyHgCVdKY+s= src.lwithers.me.uk/go/htpack v1.3.3/go.mod h1:qKgCBgZ6iiiuYOxZkYOPVpXLBzp6gXEd4A0ksxgR6Nk=

View File

@ -5,12 +5,18 @@ package main
import ( import (
"bufio" "bufio"
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"sync/atomic"
"syscall"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"src.lwithers.me.uk/go/htpack" "src.lwithers.me.uk/go/htpack"
@ -51,6 +57,10 @@ func main() {
"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().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",
"Override X-Frame-Options header (can be sameorigin, deny, allow)")
rootCmd.Flags().Duration("graceful-shutdown-delay", 3*time.Second,
"Number of seconds to wait after receiving SIGTERM before initiating graceful shutdown")
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
@ -82,6 +92,23 @@ func run(c *cobra.Command, args []string) error {
certFile = keyFile certFile = keyFile
} }
// parse frames header
framesHeader := "SAMEORIGIN"
frames, err := c.Flags().GetString("frames")
if err != nil {
return err
}
switch frames {
case "sameorigin":
framesHeader = "SAMEORIGIN"
case "allow":
framesHeader = ""
case "deny":
framesHeader = "DENY"
default:
return errors.New("--frames must be one of sameorigin, deny, allow")
}
// parse extra headers // parse extra headers
extraHeaders := make(http.Header) extraHeaders := make(http.Header)
hdrs, err := c.Flags().GetStringSlice("header") hdrs, err := c.Flags().GetStringSlice("header")
@ -132,6 +159,15 @@ func run(c *cobra.Command, args []string) error {
return err return err
} }
// graceful shutdown delay must be > 0
gracefulShutdownDelay, err := c.Flags().GetDuration("graceful-shutdown-delay")
if err != nil {
return err
}
if gracefulShutdownDelay <= 0 {
return errors.New("graceful shutdown delay must be > 0s")
}
// verify .htpack specifications // verify .htpack specifications
if len(args) == 0 { if len(args) == 0 {
return errors.New("must specify one or more .htpack files") return errors.New("must specify one or more .htpack files")
@ -157,6 +193,7 @@ func run(c *cobra.Command, args []string) error {
} }
// load packfiles, registering handlers as we go // load packfiles, registering handlers as we go
router := &routerHandler{}
for prefix, packfile := range packPaths { for prefix, packfile := range packPaths {
packHandler, err := htpack.New(packfile) packHandler, err := htpack.New(packfile)
if err != nil { if err != nil {
@ -169,27 +206,61 @@ func run(c *cobra.Command, args []string) error {
return fmt.Errorf("%s: fallback-404 resource %q "+ return fmt.Errorf("%s: fallback-404 resource %q "+
"not found in packfile", prefix, fallback404File) "not found in packfile", prefix, fallback404File)
} }
packHandler.SetHeader("X-Frame-Options", framesHeader)
handler := &addHeaders{ var handler http.Handler = &addHeaders{
extraHeaders: extraHeaders, extraHeaders: extraHeaders,
handler: packHandler, handler: packHandler,
} }
if prefix != "/" { if prefix != "/" {
http.Handle(prefix+"/", handler = http.StripPrefix(prefix, handler)
http.StripPrefix(prefix, handler))
} else {
http.Handle("/", handler)
} }
router.AddRoute(prefix, handler)
} }
// HTTP server object setup
sv := &http.Server{
Addr: bindAddr,
Handler: router,
}
// register SIGINT, SIGTERM handler
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
var (
// if we are shut down by a signal, then http.ListenAndServe()
// returns straight away, but we actually need to wait for
// Shutdown() to complete prior to returning / exiting
isSignalled atomic.Bool
signalDone = make(chan struct{})
)
go func() {
<-sigch
time.Sleep(gracefulShutdownDelay)
isSignalled.Store(true)
shutctx, shutcancel := context.WithTimeout(context.Background(), gracefulShutdownDelay)
sv.Shutdown(shutctx)
shutcancel()
close(signalDone)
}()
// main server loop // main server loop
if keyFile == "" { if keyFile == "" {
err = http.ListenAndServe(bindAddr, nil) err = sv.ListenAndServe()
} else { } else {
err = http.ListenAndServeTLS(bindAddr, certFile, keyFile, nil) err = sv.ListenAndServeTLS(certFile, keyFile)
} }
if err != nil {
// if we were shut down by a signal, wait for Shutdown() to return
if isSignalled.Load() {
<-signalDone
}
switch err {
case nil, http.ErrServerClosed:
// OK
default:
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
@ -240,3 +311,53 @@ func (ah *addHeaders) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
ah.handler.ServeHTTP(w, r) ah.handler.ServeHTTP(w, r)
} }
// routeEntry is used within routerHandler to map a specific prefix to a
// specific handler.
type routeEntry struct {
// prefix is a path prefix with trailing "/" such as "/foo/".
prefix string
// handler for the request if prefix matches.
handler http.Handler
}
// routerHandler holds a list of routes sorted by longest-prefix-first.
type routerHandler struct {
// entries are the list of prefixes, with longest prefix strings first.
// The sorting ensures we can iterate through from the start and match
// "/dir/subdir/" in preference to just "/dir/".
entries []routeEntry
}
// AddRoute adds a new entry into the handler. It is not concurrency safe; the
// handler should not be in use.
func (rh *routerHandler) AddRoute(prefix string, handler http.Handler) {
if !strings.HasSuffix(prefix, "/") {
prefix += "/"
}
rh.entries = append(rh.entries, routeEntry{
prefix: prefix,
handler: handler,
})
sort.Slice(rh.entries, func(i, j int) bool {
l1, l2 := len(rh.entries[i].prefix), len(rh.entries[j].prefix)
if l1 > l2 {
return true
}
if l1 == l2 {
return rh.entries[i].prefix < rh.entries[j].prefix
}
return false
})
}
func (rh *routerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, entry := range rh.entries {
if strings.HasPrefix(r.URL.Path, entry.prefix) {
entry.handler.ServeHTTP(w, r)
return
}
}
http.NotFound(w, r)
}

6
go.mod
View File

@ -1,8 +1,8 @@
module src.lwithers.me.uk/go/htpack module src.lwithers.me.uk/go/htpack
require ( require (
github.com/gogo/protobuf v1.2.1 github.com/gogo/protobuf v1.3.2
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 golang.org/x/sys v0.22.0
) )
go 1.13 go 1.22

38
go.sum
View File

@ -1,7 +1,33 @@
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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=

View File

@ -2,14 +2,12 @@ package htpack
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@ -21,14 +19,13 @@ const (
encodingBrotli = "br" encodingBrotli = "br"
) )
// TODO: logging
// New returns a new handler. Standard security headers are set. // New returns a new handler. Standard security headers are set.
func New(packfile string) (*Handler, error) { func New(packfile string) (*Handler, error) {
f, err := os.Open(packfile) f, err := os.Open(packfile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close()
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
@ -37,19 +34,16 @@ func New(packfile string) (*Handler, error) {
mapped, err := unix.Mmap(int(f.Fd()), 0, int(fi.Size()), mapped, err := unix.Mmap(int(f.Fd()), 0, int(fi.Size()),
unix.PROT_READ, unix.MAP_SHARED) unix.PROT_READ, unix.MAP_SHARED)
if err != nil { if err != nil {
f.Close()
return nil, err return nil, err
} }
_, dir, err := packed.Load(f) _, dir, err := packed.Load(f)
if err != nil { if err != nil {
unix.Munmap(mapped) unix.Munmap(mapped)
f.Close()
return nil, err return nil, err
} }
h := &Handler{ h := &Handler{
f: f,
mapped: mapped, mapped: mapped,
dir: dir.Files, dir: dir.Files,
headers: make(map[string]string), headers: make(map[string]string),
@ -57,7 +51,7 @@ func New(packfile string) (*Handler, error) {
} }
// 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
h.SetHeader("X-Frame-Options", "sameorigin") h.SetHeader("X-Frame-Options", "SAMEORIGIN")
// 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")
@ -67,7 +61,6 @@ func New(packfile string) (*Handler, error) {
// 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 {
f *os.File
mapped []byte mapped []byte
dir map[string]*packed.File dir map[string]*packed.File
headers map[string]string headers map[string]string
@ -203,87 +196,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.Method == "HEAD" { if req.Method == "HEAD" {
return return
} }
h.sendfile(w, data, offset, length)
}
func (h *Handler) sendfile(w http.ResponseWriter, data *packed.FileData,
offset, length uint64,
) {
hj, ok := w.(http.Hijacker)
if !ok {
// fallback
h.copyfile(w, data, offset, length)
return
}
conn, buf, err := hj.Hijack()
if err != nil {
// fallback
h.copyfile(w, data, offset, length)
return
}
tcp, ok := conn.(*net.TCPConn)
if !ok {
// fallback
h.copyfile(w, data, offset, length)
return
}
defer tcp.Close()
rawsock, err := tcp.SyscallConn()
if err == nil {
err = buf.Flush()
}
if err != nil {
// error only returned if the underlying connection is broken,
// so there's no point calling sendfile
return
}
var breakErr error
off := int64(data.Offset + offset)
remain := length
for breakErr == nil && remain > 0 {
// sendfile(2) can send a maximum of 1GiB
var amt int
if remain > (1 << 30) {
amt = (1 << 30)
} else {
amt = int(remain)
}
// behaviour of control function:
// · some bytes written: sets written > 0, returns true (breaks
// out of loop on first write)
// · EAGAIN: returns false (causes Write() to loop until
// success or permanent failure)
// · other error: sets breakErr
var written int
rawsock.Write(func(outfd uintptr) bool {
written, err = unix.Sendfile(int(outfd), int(h.f.Fd()), &off, amt)
switch err {
case nil:
return true
case syscall.EAGAIN:
return false
default:
breakErr = err
return true
}
})
// we may have had a partial write, or file may have been > 1GiB
remain -= uint64(written)
}
}
// copyfile is a fallback handler that uses write(2) on our memory-mapped data
// to push out the response.
func (h *Handler) copyfile(w http.ResponseWriter, data *packed.FileData,
offset, length uint64,
) {
offset += data.Offset offset += data.Offset
w.Write(h.mapped[offset : offset+length]) w.Write(h.mapped[offset : offset+length])
} }

View File

@ -15,11 +15,15 @@
*/ */
package packed package packed
import proto "github.com/gogo/protobuf/proto" import (
import fmt "fmt" fmt "fmt"
import math "math"
import io "io" proto "github.com/gogo/protobuf/proto"
math "math"
io "io"
)
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal var _ = proto.Marshal