diff options
Diffstat (limited to 'internal/storage/files')
-rw-r--r-- | internal/storage/files/reader.go | 141 |
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 +} |