set mtime of output based on input content
7 files changed, 186 insertions(+), 23 deletions(-)
M internal/builder/builder.go → internal/builder/builder.go
@@ -99,7 +99,7 @@ if err := templates.PostPage(config, post).Render(ctx, buf); err != nil { return nil, errors.WithMessage(err, "could not render post") } - if err := storage.Write(post.URL, buf); err != nil { + if err := storage.WritePost(&post, buf); err != nil { return nil, err } }@@ -221,7 +221,7 @@ if err := templates.Page(config, post).Render(ctx, buf); err != nil { return nil, err } } - if err := storage.Write(post.URL, buf); err != nil { + if err := storage.WritePost(post, buf); err != nil { return nil, err } }
M internal/content/posts.go → internal/content/posts.go
@@ -92,16 +92,25 @@ func (cc *Collection) GetPage(filename string) (*Post, error) { fp := filepath.Join(cc.config.Root, filename) url := path.Join("/", pageURLReplacer.Replace(filename)) + cs, err := cc.config.Repo.GetFileLog(filename) + if err != nil { + return nil, errors.WithMessagef(err, "could not get commit log for file %s", filename) + } post := &Post{ Input: fp, Basename: filepath.Base(url), URL: url, PostMatter: &PostMatter{}, + Commits: cs, } - err := parse(fp, post) + err = parse(fp, post) if err != nil { return nil, err + } + + if post.Date.IsZero() { + post.Date = cs[0].Date } return post, nil
A internal/multifile/compress.go
@@ -0,0 +1,27 @@ +package multifile + +import ( + "errors" + "io" + "os" +) + +type CompressWriter struct { + *os.File + Writer io.WriteCloser +} + +func NewCompressWriter(file *os.File, writer io.WriteCloser) *CompressWriter { + return &CompressWriter{ + File: file, + Writer: writer, + } +} + +func (cw *CompressWriter) Write(p []byte) (n int, err error) { + return cw.Writer.Write(p) +} + +func (cw *CompressWriter) Close() error { + return errors.Join(cw.Writer.Close(), cw.File.Close()) +}
A internal/multifile/multifile.go
@@ -0,0 +1,59 @@ +package multifile + +import ( + "io" + "os" + "time" + + "gitlab.com/tozd/go/errors" +) + +type MultiFile struct { + files []FileLike + io.Writer +} + +type FileLike interface { + io.WriteCloser + Name() string +} + +func NewMultiFile(files ...FileLike) *MultiFile { + writers := make([]io.Writer, len(files)) + for i, w := range files { + writers[i] = w + } + + return &MultiFile{ + files: files, + Writer: io.MultiWriter(writers...), + } +} + +func (mf *MultiFile) Close() error { + var lastErr error + for _, w := range mf.files { + err := w.Close() + if err != nil { + lastErr = err + } + } + + return lastErr +} + +func (mf *MultiFile) Name() string { + return mf.files[0].Name() +} + +func (mf *MultiFile) Chtimes(mtime time.Time) error { + var lastErr error + for _, f := range mf.files { + err := os.Chtimes(f.Name(), mtime, mtime) + if err != nil { + lastErr = errors.Join(lastErr, err) + } + } + + return lastErr +}
M internal/storage/files/writer.go → internal/storage/files/writer.go
@@ -7,7 +7,8 @@ "os" "path/filepath" "go.alanpearce.eu/website/internal/buffer" - "go.alanpearce.eu/website/internal/multibuf" + "go.alanpearce.eu/website/internal/content" + "go.alanpearce.eu/website/internal/multifile" "go.alanpearce.eu/x/log" "github.com/andybalholm/brotli"@@ -46,61 +47,101 @@ func (f *Files) OpenWrite(filename string) (io.WriteCloser, error) { return openFileWrite(f.join(filename)) } +func (f *Files) WritePost(post *content.Post, content *buffer.Buffer) error { + fd, err := f.write(post.URL, content) + if err != nil { + return err + } + err = fd.Close() + if err != nil { + return err + } + + if mf, isMultifile := fd.(*multifile.MultiFile); isMultifile { + err = mf.Chtimes(post.Date) + } else { + err = os.Chtimes(fd.Name(), post.Date, post.Date) + } + if err != nil { + return errors.WithMessage(err, "could not set file times") + } + + return nil +} + func (f *Files) Write(pathname string, content *buffer.Buffer) error { + fd, err := f.write(pathname, content) + if err != nil { + return err + } + fd.Close() + + return nil +} + +func (f *Files) write(pathname string, content *buffer.Buffer) (multifile.FileLike, error) { filename := pathNameToFileName(pathname) err := f.Mkdirp(filepath.Dir(filename)) if err != nil { - return errors.WithMessage(err, "could not create directory") + return nil, errors.WithMessage(err, "could not create directory") } fd, err := f.OpenFileAndVariants(filename) if err != nil { - return errors.WithMessagef(err, "could not open output file") + return nil, errors.WithMessagef(err, "could not open output file") } - defer fd.Close() _, err = fd.Write(content.Bytes()) if err != nil { - return errors.WithMessage(err, "could not write output file") + return nil, errors.WithMessage(err, "could not write output file") } - return nil + return fd, nil } func openFileWrite(filename string) (*os.File, error) { return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) } -func openFileGz(filename string) (*gzip.Writer, error) { +func openFileGz(filename string) (*multifile.CompressWriter, error) { filenameGz := filename + ".gz" f, err := openFileWrite(filenameGz) + if err != nil { + return nil, err + } + w, err := gzip.NewWriterLevel(f, gzipLevel) if err != nil { return nil, err } - return gzip.NewWriterLevel(f, gzipLevel) + return multifile.NewCompressWriter(f, w), err } -func openFileBrotli(filename string) (*brotli.Writer, error) { +func openFileBrotli(filename string) (*multifile.CompressWriter, error) { filenameBrotli := filename + ".br" f, err := openFileWrite(filenameBrotli) if err != nil { return nil, err } - return brotli.NewWriterLevel(f, brotliLevel), nil + return multifile.NewCompressWriter(f, brotli.NewWriterLevel(f, brotliLevel)), nil } -func openFileZstd(filename string) (*zstd.Encoder, error) { +func openFileZstd(filename string) (*multifile.CompressWriter, error) { f, err := openFileWrite(filename + ".zstd") if err != nil { return nil, err } - return zstd.NewWriter(f) + w, err := zstd.NewWriter(f) + if err != nil { + return nil, err + } + + return multifile.NewCompressWriter(f, w), nil } -func multiOpenFile(filename string) (*multibuf.WriteCloser, error) { +func multiOpenFile(filename string) (*multifile.MultiFile, error) { r, err := openFileWrite(filename) if err != nil { return nil, err@@ -118,10 +159,10 @@ if err != nil { return nil, err } - return multibuf.NewWriteCloser(r, gz, br, zst), nil + return multifile.NewMultiFile(r, gz, br, zst), nil } -func (f *Files) OpenFileAndVariants(filename string) (io.WriteCloser, error) { +func (f *Files) OpenFileAndVariants(filename string) (multifile.FileLike, error) { if f.options.Compress { return multiOpenFile(f.join(filename)) }
M internal/storage/interface.go → internal/storage/interface.go
@@ -2,6 +2,7 @@ package storage import ( "go.alanpearce.eu/website/internal/buffer" + "go.alanpearce.eu/website/internal/content" ) type Reader interface {@@ -13,4 +14,5 @@ type Writer interface { Mkdirp(path string) error Write(pathname string, content *buffer.Buffer) error + WritePost(post *content.Post, content *buffer.Buffer) error }
M internal/storage/sqlite/writer.go → internal/storage/sqlite/writer.go
@@ -13,6 +13,7 @@ "github.com/andybalholm/brotli" "github.com/klauspost/compress/gzip" "github.com/klauspost/compress/zstd" "go.alanpearce.eu/website/internal/buffer" + "go.alanpearce.eu/website/internal/content" "go.alanpearce.eu/website/internal/storage" "go.alanpearce.eu/x/log"@@ -170,15 +171,27 @@ func contentType(pathname string) string { return mime.TypeByExtension(filepath.Ext(pathNameToFileName(pathname))) } -func (s *Writer) Write(pathname string, content *buffer.Buffer) error { - s.log.Debug("storing content", "pathname", pathname) +func (s *Writer) WritePost(post *content.Post, content *buffer.Buffer) error { + s.log.Debug("storing post", "title", post.Title) bytes := content.Bytes() + etag, err := etag(bytes) + if err != nil { + return errors.WithMessage(err, "calculating etag") + } - urlID, err := s.storeURL(pathname) - if err != nil { - return errors.WithMessage(err, "storing URL") + file := &storage.File{ + Path: post.URL, + ContentType: contentType(post.URL), + LastModified: post.Date, + Etag: etag, } + return s.write(file, content) +} + +func (s *Writer) Write(pathname string, content *buffer.Buffer) error { + bytes := content.Bytes() + etag, err := etag(bytes) if err != nil { return errors.WithMessage(err, "calculating etag")@@ -189,6 +202,18 @@ Path: pathname, ContentType: contentType(pathname), LastModified: time.Now(), Etag: etag, + } + + return s.write(file, content) +} + +func (s *Writer) write(file *storage.File, content *buffer.Buffer) error { + s.log.Debug("storing content", "pathname", file.Path) + bytes := content.Bytes() + + urlID, err := s.storeURL(file.Path) + if err != nil { + return errors.WithMessage(err, "storing URL") } fileID, err := s.storeFile(urlID, file)