From 11457c6fdb101df1078b2a438486245263d7a292 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Sun, 26 Jan 2025 12:21:26 +0100 Subject: refactor mux filemap into files.Reader --- internal/server/server.go | 12 +++- internal/storage/files/reader.go | 141 +++++++++++++++++++++++++++++++++++++++ internal/website/filemap.go | 113 ------------------------------- internal/website/mux.go | 37 ++++------ 4 files changed, 163 insertions(+), 140 deletions(-) create mode 100644 internal/storage/files/reader.go delete mode 100644 internal/website/filemap.go (limited to 'internal') diff --git a/internal/server/server.go b/internal/server/server.go index b5526a0..e4ff63b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -16,6 +16,7 @@ import ( "go.alanpearce.eu/website/internal/builder" cfg "go.alanpearce.eu/website/internal/config" + "go.alanpearce.eu/website/internal/storage/files" "go.alanpearce.eu/website/internal/vcs" "go.alanpearce.eu/website/internal/website" "go.alanpearce.eu/x/log" @@ -174,7 +175,14 @@ func New(runtimeConfig *Config, log *log.Logger) (*Server, error) { } loggingMux := http.NewServeMux() - mux, err := website.NewMux(config, runtimeConfig.Root, log.Named("website")) + + log.Debug("registering content files", "root", runtimeConfig.Root) + reader, err := files.NewReader(runtimeConfig.Root, log.Named("files")) + if err != nil { + return nil, errors.WithMessage(err, "could not create file reader") + } + + mux, err := website.NewMux(config, reader, log.Named("website")) if err != nil { return nil, errors.WithMessage(err, "could not create website mux") } @@ -188,7 +196,7 @@ func New(runtimeConfig *Config, log *log.Logger) (*Server, error) { loggingMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch { case slices.Contains(config.Domains, r.Host): - path, _ := website.CanonicalisePath(r.URL.Path) + path, _ := reader.CanonicalisePath(r.URL.Path) http.Redirect( w, r, diff --git a/internal/storage/files/reader.go b/internal/storage/files/reader.go new file mode 100644 index 0000000..0bad9ef --- /dev/null +++ b/internal/storage/files/reader.go @@ -0,0 +1,141 @@ +package files + +import ( + "fmt" + "hash/fnv" + "io" + "io/fs" + "mime" + "os" + "path/filepath" + "strings" + + "go.alanpearce.eu/x/log" + + "gitlab.com/tozd/go/errors" +) + +type File struct { + ContentType string + Etag string + Alternatives map[string]string +} + +func (f *File) AvailableEncodings() []string { + encs := []string{} + for enc := range f.Alternatives { + encs = append(encs, enc) + } + + return encs +} + +type Reader struct { + root string + log *log.Logger + files map[string]*File +} + +func NewReader(path string, log *log.Logger) (*Reader, error) { + r := &Reader{ + root: path, + log: log, + files: make(map[string]*File), + } + if err := r.registerContentFiles(); err != nil { + return nil, errors.WithMessagef(err, "registering content files") + } + + return r, nil +} + +func hashFile(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", errors.WithMessagef(err, "could not open file %s for hashing", filename) + } + defer f.Close() + hash := fnv.New64a() + if _, err := io.Copy(hash, f); err != nil { + return "", errors.WithMessagef(err, "could not hash file %s", filename) + } + + return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil +} + +var encodings = map[string]string{ + "br": ".br", + "gzip": ".gz", +} + +func (r *Reader) registerFile(urlpath string, fp string) error { + hash, err := hashFile(fp) + if err != nil { + return err + } + f := File{ + ContentType: mime.TypeByExtension(filepath.Ext(fp)), + Etag: hash, + Alternatives: map[string]string{ + "identity": fp, + }, + } + for enc, suffix := range encodings { + _, err := os.Stat(fp + suffix) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + + return err + } + f.Alternatives[enc] = fp + suffix + } + r.files[urlpath] = &f + + return nil +} + +func (r *Reader) registerContentFiles() error { + err := filepath.WalkDir(r.root, func(filePath string, f fs.DirEntry, err error) error { + if err != nil { + return errors.WithMessagef(err, "failed to access path %s", filePath) + } + relPath, err := filepath.Rel(r.root, filePath) + if err != nil { + return errors.WithMessagef(err, "failed to make path relative, path: %s", filePath) + } + urlPath, _ := strings.CutSuffix("/"+relPath, "index.html") + if !f.IsDir() { + switch filepath.Ext(relPath) { + case ".br", ".gz": + return nil + } + r.log.Debug("registering file", "urlpath", urlPath) + + return r.registerFile(urlPath, filePath) + } + + return nil + }) + if err != nil { + return errors.WithMessage(err, "could not walk directory") + } + + return nil +} + +func (r *Reader) GetFile(urlPath string) *File { + return r.files[urlPath] +} + +func (r *Reader) CanonicalisePath(path string) (cPath string, differs bool) { + cPath = path + if strings.HasSuffix(path, "/index.html") { + cPath, differs = strings.CutSuffix(path, "index.html") + } else if !strings.HasSuffix(path, "/") && r.files[path+"/"] != nil { + cPath, differs = path+"/", true + } + + return cPath, differs +} diff --git a/internal/website/filemap.go b/internal/website/filemap.go deleted file mode 100644 index edef7cd..0000000 --- a/internal/website/filemap.go +++ /dev/null @@ -1,113 +0,0 @@ -package website - -import ( - "fmt" - "hash/fnv" - "io" - "io/fs" - "mime" - "os" - "path/filepath" - "strings" - - "go.alanpearce.eu/x/log" - - "gitlab.com/tozd/go/errors" -) - -type File struct { - contentType string - etag string - alternatives map[string]string -} - -func (f *File) AvailableEncodings() []string { - encs := []string{} - for enc := range f.alternatives { - encs = append(encs, enc) - } - - return encs -} - -var files = map[string]*File{} - -func hashFile(filename string) (string, error) { - f, err := os.Open(filename) - if err != nil { - return "", errors.WithMessagef(err, "could not open file %s for hashing", filename) - } - defer f.Close() - hash := fnv.New64a() - if _, err := io.Copy(hash, f); err != nil { - return "", errors.WithMessagef(err, "could not hash file %s", filename) - } - - return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil -} - -var encodings = map[string]string{ - "br": ".br", - "gzip": ".gz", -} - -func registerFile(urlpath string, fp string) error { - hash, err := hashFile(fp) - if err != nil { - return err - } - f := File{ - contentType: mime.TypeByExtension(filepath.Ext(fp)), - etag: hash, - alternatives: map[string]string{ - "identity": fp, - }, - } - for enc, suffix := range encodings { - _, err := os.Stat(fp + suffix) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - continue - } - - return err - } - f.alternatives[enc] = fp + suffix - } - files[urlpath] = &f - - return nil -} - -func registerContentFiles(root string, log *log.Logger) error { - err := filepath.WalkDir(root, func(filePath string, f fs.DirEntry, err error) error { - if err != nil { - return errors.WithMessagef(err, "failed to access path %s", filePath) - } - relPath, err := filepath.Rel(root, filePath) - if err != nil { - return errors.WithMessagef(err, "failed to make path relative, path: %s", filePath) - } - urlPath, _ := strings.CutSuffix("/"+relPath, "index.html") - if !f.IsDir() { - switch filepath.Ext(relPath) { - case ".br", ".gz": - return nil - } - log.Debug("registering file", "urlpath", urlPath) - - return registerFile(urlPath, filePath) - } - - return nil - }) - if err != nil { - return errors.WithMessage(err, "could not walk directory") - } - - return nil -} - -func GetFile(urlPath string) *File { - return files[urlPath] -} diff --git a/internal/website/mux.go b/internal/website/mux.go index 6844551..5f571f8 100644 --- a/internal/website/mux.go +++ b/internal/website/mux.go @@ -7,25 +7,14 @@ import ( "go.alanpearce.eu/website/internal/config" ihttp "go.alanpearce.eu/website/internal/http" - "go.alanpearce.eu/x/log" + "go.alanpearce.eu/website/internal/storage/files" "go.alanpearce.eu/website/templates" + "go.alanpearce.eu/x/log" "github.com/benpate/digit" "github.com/kevinpollet/nego" - "gitlab.com/tozd/go/errors" ) -func CanonicalisePath(path string) (cPath string, differs bool) { - cPath = path - if strings.HasSuffix(path, "/index.html") { - cPath, differs = strings.CutSuffix(path, "index.html") - } else if !strings.HasSuffix(path, "/") && files[path+"/"] != nil { - cPath, differs = path+"/", true - } - - return cPath, differs -} - type webHandler func(http.ResponseWriter, *http.Request) *ihttp.Error type WrappedWebHandler struct { @@ -62,31 +51,29 @@ func (fn WrappedWebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func NewMux(cfg *config.Config, root string, log *log.Logger) (mux *http.ServeMux, err error) { +func NewMux( + cfg *config.Config, + reader *files.Reader, + log *log.Logger, +) (mux *http.ServeMux, err error) { mux = &http.ServeMux{} - - log.Debug("registering content files", "root", root) - err = registerContentFiles(root, log) - if err != nil { - return nil, errors.WithMessagef(err, "registering content files") - } templates.Setup() mux.Handle("/", wrapHandler(cfg, func(w http.ResponseWriter, r *http.Request) *ihttp.Error { - urlPath, shouldRedirect := CanonicalisePath(r.URL.Path) + urlPath, shouldRedirect := reader.CanonicalisePath(r.URL.Path) if shouldRedirect { http.Redirect(w, r, urlPath, 302) return nil } - file := GetFile(urlPath) + file := reader.GetFile(urlPath) if file == nil { return &ihttp.Error{ Message: "File not found", Code: http.StatusNotFound, } } - w.Header().Add("ETag", file.etag) + w.Header().Add("ETag", file.Etag) w.Header().Add("Vary", "Accept-Encoding") w.Header().Add("Content-Security-Policy", cfg.CSP.String()) for k, v := range cfg.Extra.Headers { @@ -96,9 +83,9 @@ func NewMux(cfg *config.Config, root string, log *log.Logger) (mux *http.ServeMu switch enc { case "br", "gzip": w.Header().Add("Content-Encoding", enc) - w.Header().Add("Content-Type", file.contentType) + w.Header().Add("Content-Type", file.ContentType) } - http.ServeFile(w, r, files[urlPath].alternatives[enc]) + http.ServeFile(w, r, file.Alternatives[enc]) return nil }, log)) -- cgit 1.4.1