fupstatic: create hashfs
I want to serve static files with hashes in, and I'm too lazy to construct a preprocessing step.
This commit is contained in:
parent
ecbf5a6450
commit
03cf4ea939
2 changed files with 215 additions and 0 deletions
172
web/fup/fupstatic/hashfs.go
Normal file
172
web/fup/fupstatic/hashfs.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package fupstatic
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
)
|
||||
|
||||
// adaptError adapts an error from an underlying filesystem.
|
||||
func adaptError(err error, name string) error {
|
||||
var pErr *fs.PathError
|
||||
if errors.As(err, &pErr) {
|
||||
pErr.Path = name
|
||||
return pErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type staticFS struct {
|
||||
fs fs.ReadFileFS
|
||||
|
||||
// We need:
|
||||
// original name -> new name
|
||||
// new name -> original name
|
||||
toHashName map[string]string
|
||||
toPlainName map[string]string
|
||||
}
|
||||
|
||||
type staticFSFileInfo struct {
|
||||
fs.FileInfo
|
||||
newName string
|
||||
}
|
||||
|
||||
func (fi staticFSFileInfo) Name() string { return path.Base(fi.newName) }
|
||||
|
||||
type staticFSDirEntry struct {
|
||||
fs.DirEntry
|
||||
newName string
|
||||
}
|
||||
|
||||
func (dent staticFSDirEntry) Name() string { return path.Base(dent.newName) }
|
||||
func (dent staticFSDirEntry) Info() (fs.FileInfo, error) {
|
||||
fi, err := dent.DirEntry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return staticFSFileInfo{fi, dent.newName}, nil
|
||||
}
|
||||
|
||||
type staticFSFile struct {
|
||||
fs.File
|
||||
newName string
|
||||
}
|
||||
|
||||
func (f *staticFSFile) Stat() (fs.FileInfo, error) {
|
||||
fi, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return staticFSFileInfo{fi, f.newName}, nil
|
||||
}
|
||||
|
||||
type staticFSDirFile struct {
|
||||
fs.ReadDirFile
|
||||
basePath string
|
||||
s *staticFS
|
||||
}
|
||||
|
||||
func (f staticFSDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
dents, err := f.ReadDirFile.ReadDir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for n, dent := range dents {
|
||||
fpath := path.Join(f.basePath, dent.Name())
|
||||
if hashName, ok := f.s.toHashName[fpath]; ok {
|
||||
dents[n] = staticFSDirEntry{dent, hashName}
|
||||
}
|
||||
}
|
||||
return dents, nil
|
||||
}
|
||||
|
||||
// build computes the hashes needed to serve files.
|
||||
func (s *staticFS) build() error {
|
||||
toHashName := make(map[string]string)
|
||||
toPlainName := make(map[string]string)
|
||||
if err := fs.WalkDir(s.fs, ".", func(fpath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
// We don't care about directories.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compute the hash of this file...
|
||||
bs, err := s.fs.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %w", fpath, err)
|
||||
}
|
||||
hexDigest := hex.EncodeToString(sha512.New().Sum(bs)[:6])
|
||||
|
||||
// and fix it up in the filename.
|
||||
oldName := fpath
|
||||
ext := path.Ext(oldName)
|
||||
var newName string
|
||||
if len(ext) == 0 {
|
||||
newName = fmt.Sprintf("%s.%s", oldName, hexDigest)
|
||||
} else {
|
||||
newName = fmt.Sprintf("%s.%s%s", oldName[:len(oldName)-len(ext)], hexDigest, ext)
|
||||
}
|
||||
toHashName[oldName] = newName
|
||||
toPlainName[newName] = oldName
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
s.toHashName = toHashName
|
||||
s.toPlainName = toPlainName
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupHashedName looks up the filename for the given original name.
|
||||
func (s *staticFS) LookupHashedName(name string) (string, bool) {
|
||||
n, ok := s.toHashName[name]
|
||||
return n, ok
|
||||
}
|
||||
|
||||
// Open opens the named file.
|
||||
func (s *staticFS) Open(name string) (fs.File, error) {
|
||||
fn, ok := s.toPlainName[name]
|
||||
if !ok {
|
||||
// Try opening it as a plain name.
|
||||
fn = name
|
||||
}
|
||||
f, err := s.fs.Open(fn)
|
||||
if err != nil {
|
||||
return nil, adaptError(err, name)
|
||||
}
|
||||
if rdf, ok := f.(fs.ReadDirFile); ok {
|
||||
return &staticFSDirFile{rdf, name, s}, nil
|
||||
}
|
||||
return &staticFSFile{f, name}, nil
|
||||
}
|
||||
|
||||
// ReadFile provides an optimised ReadFile implementation.
|
||||
func (s *staticFS) ReadFile(name string) ([]byte, error) {
|
||||
fn, ok := s.toPlainName[name]
|
||||
if !ok {
|
||||
// Try opening it as a plain name.
|
||||
fn = name
|
||||
}
|
||||
f, err := s.fs.ReadFile(fn)
|
||||
if err != nil {
|
||||
return nil, adaptError(err, name)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func newStaticFS(fs fs.ReadFileFS) *staticFS {
|
||||
s := &staticFS{
|
||||
fs: fs,
|
||||
}
|
||||
if err := s.build(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
43
web/fup/fupstatic/hashfs_test.go
Normal file
43
web/fup/fupstatic/hashfs_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package fupstatic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestHashingFS(t *testing.T) {
|
||||
baseFS := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
"bar/bar.txt": &fstest.MapFile{
|
||||
Data: []byte("foo bar baz"),
|
||||
},
|
||||
}
|
||||
|
||||
f := newStaticFS(baseFS)
|
||||
|
||||
tcs := []struct {
|
||||
origName string
|
||||
newName string
|
||||
wantContent string
|
||||
}{
|
||||
{"foo", "foo.68656c6c6f2c", "hello, world"},
|
||||
{"bar/bar.txt", "bar/bar.666f6f206261.txt", "foo bar baz"},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.origName, func(t *testing.T) {
|
||||
n, ok := f.LookupHashedName(tc.origName)
|
||||
if !ok {
|
||||
t.Errorf("LookupHashedName returned false")
|
||||
} else if n != tc.newName {
|
||||
t.Errorf("LookupHashedName returned %q; want %q", n, tc.newName)
|
||||
}
|
||||
|
||||
if err := fstest.TestFS(f, tc.newName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue