package files import ( "fmt" "hash/fnv" "io" "mime" "os" "path/filepath" "strings" "gitlab.com/tozd/go/errors" "go.alanpearce.eu/website/internal/storage" ) type File struct { storage.File } var encodings = map[string]string{ "br": ".br", "gzip": ".gz", } func (r *Reader) OpenFile(path string, filename string) (*File, error) { f, err := os.Open(filename) if err != nil { return nil, errors.WithMessage(err, "could not open file for reading") } stat, err := f.Stat() if err != nil { return nil, errors.WithMessage(err, "could not stat file") } etag, err := etag(f) if err != nil { return nil, errors.WithMessage(err, "could not calculate etag") } file := &File{ File: storage.File{ Path: path, ContentType: mime.TypeByExtension(filepath.Ext(filename)), LastModified: stat.ModTime(), Etag: etag, Encodings: map[string]io.ReadSeekCloser{ "identity": f, }, }, } for enc, suffix := range encodings { _, err := os.Stat(filename + suffix) if err != nil { if errors.Is(err, os.ErrNotExist) { continue } return nil, errors.WithMessagef(err, "could not stat file %s", filename+suffix) } file.Encodings[enc], err = os.Open(filename + suffix) if err != nil { return nil, errors.WithMessagef(err, "could not read file %s", filename+suffix) } } return file, nil } func etag(f io.Reader) (string, error) { hash := fnv.New64a() if _, err := io.Copy(hash, f); err != nil { return "", errors.WithMessage(err, "could not hash file") } return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil } func (f *File) Close() error { var errs []error for _, enc := range f.Encodings { if err := enc.Close(); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func pathNameToFileName(pathname string) string { if strings.HasSuffix(pathname, "/") { pathname = pathname + "index.html" } return pathname } func fileNameToPathName(filename string) string { pathname, _ := strings.CutSuffix(filename, "index.html") return pathname }