about summary refs log tree commit diff stats
path: root/internal/storage/files/reader.go
diff options
context:
space:
mode:
authorAlan Pearce2025-01-26 12:21:26 +0100
committerAlan Pearce2025-01-26 13:38:16 +0100
commit11457c6fdb101df1078b2a438486245263d7a292 (patch)
treead19707bfc46cfb0414296a3c9061c227c431400 /internal/storage/files/reader.go
parent7d9e98a0996eca0372e38cee4b8826d84a9ace2b (diff)
downloadwebsite-11457c6fdb101df1078b2a438486245263d7a292.tar.lz
website-11457c6fdb101df1078b2a438486245263d7a292.tar.zst
website-11457c6fdb101df1078b2a438486245263d7a292.zip
refactor mux filemap into files.Reader
Diffstat (limited to 'internal/storage/files/reader.go')
-rw-r--r--internal/storage/files/reader.go141
1 files changed, 141 insertions, 0 deletions
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
+}