Skip to content

Commit

Permalink
Merge pull request #41 from linkdata/staticserve
Browse files Browse the repository at this point in the history
add staticserve
  • Loading branch information
linkdata committed May 29, 2024
2 parents 38ed943 + 1c47bb6 commit 4497766
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 0 deletions.
1 change: 1 addition & 0 deletions staticserve/assets/subdir/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The quick brown fox jumps over the lazy dog.
5 changes: 5 additions & 0 deletions staticserve/assets/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var jaws = null;

function jawsContains(a, v) {
return a.indexOf(String(v).trim().toLowerCase()) !== -1;
}
33 changes: 33 additions & 0 deletions staticserve/muxservefs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package staticserve

import (
"io"
"io/fs"
"net/http"
"path"
)

func MuxServeFS(mux *http.ServeMux, prefix string, fsys fs.FS) (uris map[string]string, err error) {
err = fs.WalkDir(fsys, ".", func(fn string, d fs.DirEntry, err error) error {
if err == nil && !d.IsDir() {
var f fs.File
if f, err = fsys.Open(fn); err == nil {
defer f.Close()
var b []byte
if b, err = io.ReadAll(f); err == nil {
var ss *StaticServe
if ss, err = New(fn, b); err == nil {
uri := path.Join(prefix, ss.Name)
if uris == nil {
uris = make(map[string]string)
}
uris[fn] = uri
mux.Handle(uri, ss)
}
}
}
}
return err
})
return
}
33 changes: 33 additions & 0 deletions staticserve/muxservefs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package staticserve_test

import (
"embed"
"net/http"
"net/http/httptest"
"testing"

"github.com/linkdata/jaws/staticserve"
)

//go:embed assets
var assetsFS embed.FS

func Test_MuxServeFS(t *testing.T) {
mux := http.NewServeMux()
uris, err := staticserve.MuxServeFS(mux, "/", assetsFS)
if err != nil {
t.Error(err)
}
if len(uris) != 2 {
t.Error("expected two uris")
}
for fn, uri := range uris {
t.Log(fn, uri)
rq := httptest.NewRequest(http.MethodGet, uri, nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, rq)
if sc := rr.Result().StatusCode; sc != http.StatusOK {
t.Error(sc)
}
}
}
53 changes: 53 additions & 0 deletions staticserve/servehttp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package staticserve

import (
"bytes"
"compress/gzip"
"io"
"net/http"
"strconv"
)

var headerCacheControl = []string{"public, max-age=31536000, s-maxage=31536000, immutable"}
var headerVary = []string{"Accept-Encoding"}
var headerContentEncoding = []string{"gzip"}

func acceptsGzip(hdr http.Header) bool {
for _, s := range hdr["Accept-Encoding"] {
if s == "gzip" {
return true
}
}
return false
}

func (ss *StaticServe) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body io.Reader
statusCode := http.StatusMethodNotAllowed
if r.Method == http.MethodGet {
hdr := w.Header()
if acceptsGzip(r.Header) {
body = bytes.NewReader(ss.Gz)
hdr["Content-Encoding"] = headerContentEncoding
hdr["Content-Length"] = []string{strconv.Itoa(len(ss.Gz))}
} else {
statusCode = http.StatusInternalServerError
if gzr, err := gzip.NewReader(bytes.NewReader(ss.Gz)); err == nil {
defer gzr.Close()
body = gzr
}
}
if body != nil {
statusCode = http.StatusOK
hdr["Cache-Control"] = headerCacheControl
hdr["Vary"] = headerVary
if ss.ContentType != "" {
hdr["Content-Type"] = []string{ss.ContentType}
}
}
}
w.WriteHeader(statusCode)
if body != nil {
_, _ = io.Copy(w, body)
}
}
77 changes: 77 additions & 0 deletions staticserve/servehttp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package staticserve_test

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/linkdata/jaws/staticserve"
)

const someText = `The quick brown fox jumps over the lazy dog.`

func Test_ServeHTTP_Raw(t *testing.T) {
ss, err := staticserve.New("test.txt", []byte(someText))
if err != nil {
t.Fatal(err)
}
rq := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder()
ss.ServeHTTP(rr, rq)
if sc := rr.Result().StatusCode; sc != http.StatusOK {
t.Error(sc)
}
b, err := io.ReadAll(rr.Result().Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, []byte(someText)) {
t.Error(string(b))
}
}

func Test_ServeHTTP_GZip(t *testing.T) {
ss, err := staticserve.New("test.txt", []byte(someText))
if err != nil {
t.Fatal(err)
}
rq := httptest.NewRequest(http.MethodGet, "/", nil)
rq.Header.Set("Accept-Encoding", "gzip")
rr := httptest.NewRecorder()
ss.ServeHTTP(rr, rq)
res := rr.Result()
if sc := res.StatusCode; sc != http.StatusOK {
t.Error(sc)
}
if ce := res.Header.Get("Content-Encoding"); ce != "gzip" {
t.Error(res.Header)
}
b, err := io.ReadAll(rr.Result().Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, ss.Gz) {
t.Error("data mismatch")
}
}

func Test_ServeHTTP_Errors(t *testing.T) {
ss := &staticserve.StaticServe{
Gz: []byte{0},
}
rq := httptest.NewRequest(http.MethodPut, "/", nil)
rr := httptest.NewRecorder()
ss.ServeHTTP(rr, rq)
if sc := rr.Result().StatusCode; sc != http.StatusMethodNotAllowed {
t.Error(sc)
}

rq = httptest.NewRequest(http.MethodGet, "/", nil)
rr = httptest.NewRecorder()
ss.ServeHTTP(rr, rq)
if sc := rr.Result().StatusCode; sc != http.StatusInternalServerError {
t.Error(sc)
}
}
49 changes: 49 additions & 0 deletions staticserve/staticserve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package staticserve

import (
"bytes"
"compress/gzip"
"hash/fnv"
"mime"
"path/filepath"
"strconv"
"strings"
)

type StaticServe struct {
Name string
ContentType string
Gz []byte
}

func New(filename string, data []byte) (ss *StaticServe, err error) {
var gz []byte
if strings.HasSuffix(filename, ".gz") {
gz = data
filename = strings.TrimSuffix(filename, ".gz")
} else {
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
defer gzw.Close()
if _, err = gzw.Write(data); err == nil {
if err = gzw.Flush(); err == nil {
gz = buf.Bytes()
}
}
}

if err == nil {
ext := filepath.Ext(filename)
filename = strings.TrimSuffix(filename, ext)
h := fnv.New64a()
if _, err = h.Write(gz); err == nil {
ss = &StaticServe{
Name: filename + "." + strconv.FormatUint(h.Sum64(), 36) + ext,
ContentType: mime.TypeByExtension(ext),
Gz: gz,
}
}
}

return
}
46 changes: 46 additions & 0 deletions staticserve/staticserve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package staticserve_test

import (
"bytes"
"strings"
"testing"

"github.com/linkdata/jaws/staticserve"
)

const someJavascript = `var jaws = null;
function jawsContains(a, v) {
return a.indexOf(String(v).trim().toLowerCase()) !== -1;
}
`

func Test_New(t *testing.T) {
ss, err := staticserve.New("test.js", []byte(someJavascript))
if err != nil {
t.Error(err)
}
if !strings.Contains(ss.ContentType, "javascript") {
t.Error("ss not javascript")
}
ss2, err := staticserve.New("test.js.gz", ss.Gz)
if err != nil {
t.Error(err)
}
if !bytes.Equal(ss2.Gz, ss.Gz) {
t.Error("bytes differ")
}
if !strings.Contains(ss2.ContentType, "javascript") {
t.Error("ss2 not javascript")
}
if ss.Name != ss2.Name {
t.Error(ss.Name, "!=", ss2.Name)
}
ss3, err := staticserve.New("test.foo123", nil)
if err != nil {
t.Error(err)
}
if ss3.ContentType != "" {
t.Error(ss3.ContentType)
}
}

0 comments on commit 4497766

Please sign in to comment.