From 127a675fc7cd9cdb65e4b4caac21e0f259102ee8 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Mon, 27 Jan 2025 21:42:41 +0100 Subject: serve files from Storage implementation --- cmd/server/main.go | 1 + internal/storage/file.go | 23 +++++++++ internal/storage/files/file.go | 102 +++++++++++++++++++++++++++++++++++++++ internal/storage/files/reader.go | 84 ++++++-------------------------- internal/storage/files/writer.go | 6 +-- internal/website/mux.go | 4 +- 6 files changed, 143 insertions(+), 77 deletions(-) create mode 100644 internal/storage/file.go create mode 100644 internal/storage/files/file.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 652ffa3..f77c0a2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -30,6 +30,7 @@ func main() { if err != nil { log.Fatal("could not create temporary directory", "error", err) } + log.Info("using temporary directory", "dir", tmpdir) defer os.RemoveAll(tmpdir) runtimeConfig.Root = tmpdir } diff --git a/internal/storage/file.go b/internal/storage/file.go new file mode 100644 index 0000000..f588bf3 --- /dev/null +++ b/internal/storage/file.go @@ -0,0 +1,23 @@ +package storage + +import ( + "io" + "time" +) + +type File struct { + Path string + ContentType string + LastModified time.Time + Etag string + Encodings map[string]io.ReadSeekCloser +} + +func (f *File) AvailableEncodings() []string { + encs := make([]string, 0, len(f.Encodings)) + for enc := range f.Encodings { + encs = append(encs, enc) + } + + return encs +} diff --git a/internal/storage/files/file.go b/internal/storage/files/file.go new file mode 100644 index 0000000..a71811c --- /dev/null +++ b/internal/storage/files/file.go @@ -0,0 +1,102 @@ +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 +} diff --git a/internal/storage/files/reader.go b/internal/storage/files/reader.go index 0bad9ef..425436b 100644 --- a/internal/storage/files/reader.go +++ b/internal/storage/files/reader.go @@ -1,12 +1,7 @@ package files import ( - "fmt" - "hash/fnv" - "io" "io/fs" - "mime" - "os" "path/filepath" "strings" @@ -15,21 +10,6 @@ import ( "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 @@ -49,49 +29,13 @@ func NewReader(path string, log *log.Logger) (*Reader, error) { return r, nil } -func hashFile(filename string) (string, error) { - f, err := os.Open(filename) +func (r *Reader) registerFile(urlpath string, filepath string) error { + file, err := r.OpenFile(urlpath, filepath) 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 errors.WithMessagef(err, "could not register file %s", filepath) } - 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 + r.files[urlpath] = file return nil } @@ -101,22 +45,22 @@ func (r *Reader) registerContentFiles() error { if err != nil { return errors.WithMessagef(err, "failed to access path %s", filePath) } + if f.IsDir() { + return nil + } + 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) + urlPath := fileNameToPathName("/" + relPath) + + switch filepath.Ext(relPath) { + case ".br", ".gz": + return nil } - return nil + return r.registerFile(urlPath, filePath) }) if err != nil { return errors.WithMessage(err, "could not walk directory") diff --git a/internal/storage/files/writer.go b/internal/storage/files/writer.go index cd227c0..ce498e7 100644 --- a/internal/storage/files/writer.go +++ b/internal/storage/files/writer.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "strings" "go.alanpearce.eu/x/log" @@ -177,8 +176,5 @@ func (f *Files) Mkdirp(dir string) error { } func (f *Files) join(filename string) string { - if strings.HasSuffix(filename, "/") { - filename = filename + "index.html" - } - return filepath.Join(f.outputDirectory, filename) + return filepath.Join(f.outputDirectory, pathNameToFileName(filename)) } diff --git a/internal/website/mux.go b/internal/website/mux.go index 5f571f8..7b1db36 100644 --- a/internal/website/mux.go +++ b/internal/website/mux.go @@ -83,9 +83,9 @@ func NewMux( switch enc { case "br", "gzip": w.Header().Add("Content-Encoding", enc) - w.Header().Add("Content-Type", file.ContentType) } - http.ServeFile(w, r, file.Alternatives[enc]) + w.Header().Add("Content-Type", file.ContentType) + http.ServeContent(w, r, file.Path, file.LastModified, file.Encodings[enc]) return nil }, log)) -- cgit 1.4.1