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 }