package storage import ( "errors" "fmt" "hash/fnv" "io" "mime" "os" "path/filepath" "strings" "time" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg" ) type File struct { Path string FSPath string LastModified time.Time Etag string Title string Encodings map[string]*os.File ContentType string } var ( ErrEncodingNotFound = errors.New("encoding not found") encodings = map[string]string{ "br": ".br", "gzip": ".gz", "zstd": ".zstd", } ) func NewFile(urlPath string, filename string) (*File, error) { f, err := os.Open(filename) if err != nil { return nil, err } stat, err := os.Stat(filename) if err != nil { return nil, err } file := &File{ Path: urlPath, FSPath: filename, LastModified: stat.ModTime(), Etag: "", Title: filename, Encodings: map[string]*os.File{ "identity": f, }, } file.ContentType = file.getContentType() return file, nil } func (f *File) AvailableEncodings() []string { encs := make([]string, 0, len(f.Encodings)) for enc := range f.Encodings { encs = append(encs, enc) } return encs } func (f *File) getContentType() string { ext := filepath.Ext(f.FSPath) if ext == "" { ext = ".html" } f.ContentType = mime.TypeByExtension(ext) return f.ContentType } func (r *Reader) OpenFile(path string, filename string) (*File, error) { file, err := NewFile(path, filename) if err != nil { return nil, fault.Wrap(err, fmsg.With("could not init file")) } file.Etag, err = etag(file.Encodings["identity"]) if err != nil { return nil, err } 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)), ) } f, err := os.Open(filename + suffix) if err != nil { return nil, fault.Wrap( err, fmsg.With(fmt.Sprintf("could not read file %s", filename+suffix)), ) } file.Encodings[enc] = f } 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", ) }