123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 | package render
import (
"bytes"
"compress/gzip"
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
)
// tempFile is written beside its destination and renamed over it, so a
// reader never sees it half written.
type tempFile struct {
*os.File
dst string
done bool
}
// createTemp starts a tempFile for dst, creating parent directories as
// needed.
func createTemp(dst string) (*tempFile, error) {
dir := filepath.Dir(dst)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
f, err := os.CreateTemp(dir, "."+filepath.Base(dst)+".tmp-*")
if err != nil {
return nil, err
}
return &tempFile{File: f, dst: dst}, nil
}
// finish makes the file mode 0644 (CreateTemp's is 0600) and closes it,
// ready for commit.
func (t *tempFile) finish() error {
if err := t.Chmod(0o644); err != nil {
return err
}
return t.Close()
}
// commit moves the finished file into place.
func (t *tempFile) commit() error {
if err := os.Rename(t.Name(), t.dst); err != nil {
return err
}
t.done = true
return nil
}
// discard closes and removes the file, unless commit has moved it into
// place.
func (t *tempFile) discard() {
if !t.done {
_ = t.Close()
_ = os.Remove(t.Name())
}
}
// writeFileAtomic writes data to path, mode 0644, via a temp file +
// rename, creating parent directories as needed.
func writeFileAtomic(path string, data []byte) error {
t, err := createTemp(path)
if err != nil {
return err
}
defer t.discard()
if _, err := t.Write(data); err != nil {
return err
}
if err := t.finish(); err != nil {
return err
}
return t.commit()
}
// gzipWriters recycles gzip writers: each holds most of a megabyte of
// compressor state, and a render compresses every page it writes.
var gzipWriters = sync.Pool{New: func() any { return gzip.NewWriter(nil) }}
// gzipBytes compresses data with the default level.
func gzipBytes(data []byte) ([]byte, error) {
var buf bytes.Buffer
buf.Grow(len(data)/4 + 64)
zw := gzipWriters.Get().(*gzip.Writer)
defer gzipWriters.Put(zw)
zw.Reset(&buf)
if _, err := zw.Write(data); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// writePair writes path and, when gz is set, path.gz. Order matters for
// httpd's gzip-static: unlink the old .gz first so a reader never gets a
// stale compressed copy next to a fresh uncompressed one.
func writePair(path string, data []byte, gz bool) error {
if err := os.Remove(path + ".gz"); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err := writeFileAtomic(path, data); err != nil {
return err
}
if !gz {
return nil
}
z, err := gzipBytes(data)
if err != nil {
return err
}
return writeFileAtomic(path+".gz", z)
}
// removeOutputs deletes each path and its .gz twin, ignoring missing files.
func removeOutputs(paths ...string) error {
for _, p := range paths {
for _, q := range []string{p, p + ".gz"} {
if err := os.Remove(q); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
}
}
return nil
}
// listDir returns the paths of dir's entries whose names match, in name
// order; a missing dir has none. Unlike filepath.Glob, it never reads dir
// itself as a pattern, so glob characters in a configured path are safe.
func listDir(dir string, match func(name string) bool) ([]string, error) {
ents, err := os.ReadDir(dir)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, err
}
var paths []string
for _, e := range ents {
if match(e.Name()) {
paths = append(paths, filepath.Join(dir, e.Name()))
}
}
return paths, nil
}
// pruneEmptyDirs removes dir and its ancestors while they are empty, never
// going above stop.
func pruneEmptyDirs(dir, stop string) {
stop = filepath.Clean(stop)
for {
dir = filepath.Clean(dir)
if dir == stop || !strings.HasPrefix(dir, stop+string(filepath.Separator)) {
return
}
if err := os.Remove(dir); err != nil {
return // not empty or gone
}
dir = filepath.Dir(dir)
}
}
|