package files
import (
"errors"
"fmt"
"hash/fnv"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"alin.ovh/homestead/shared/buffer"
"alin.ovh/homestead/shared/storage"
)
var encodings = map[string]string{
"br": ".br",
"gzip": ".gz",
"zstd": ".zstd",
}
func (r *Reader) OpenFile(path string, filename string) (*storage.File, error) {
f, err := os.Open(filename)
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not open file for reading"))
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not stat file"))
}
buf := new(buffer.Buffer)
if _, err := f.WriteTo(buf); err != nil {
return nil, fault.Wrap(err, fmsg.With("could not read file"))
}
etag, err := etag(buf)
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not calculate etag"))
}
file := &storage.File{
Path: path,
FSPath: filename,
ContentType: getContentType(filename, buf),
LastModified: stat.ModTime(),
Etag: etag,
Encodings: map[string]*buffer.Buffer{
"identity": buf,
},
}
for enc, suffix := range encodings {
_, err := os.Stat(filename + suffix)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not stat file %s", filename+suffix)),
)
}
bytes, err := os.ReadFile(filename + suffix)
if err != nil {
return nil, fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not read file %s", filename+suffix)),
)
}
buf := buffer.NewBuffer(bytes)
file.Encodings[enc] = buf
}
return file, nil
}
func etag(f io.Reader) (string, error) {
hash := fnv.New64a()
if _, err := io.Copy(hash, f); err != nil {
return "", fault.Wrap(err, fmsg.With("could not hash file"))
}
return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil
}
func pathNameToFileName(pathname string) string {
if strings.HasSuffix(pathname, "/") {
pathname += "index.html"
}
return strings.TrimPrefix(pathname, "/")
}
func cutSuffix(s, suffix string) string {
out, _ := strings.CutSuffix(s, suffix)
return out
}
func fileNameToPathName(filename string) string {
return cutSuffix(
cutSuffix(filename, "index.html"),
".html",
)
}
func getContentType(filename string, buf *buffer.Buffer) string {
ext := filepath.Ext(filename)
if ext == "" {
return http.DetectContentType(buf.FirstBlock())
}
return mime.TypeByExtension(ext)
}
shared/storage/files/file.go (view raw)