summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go54
1 files changed, 50 insertions, 4 deletions
diff --git a/main.go b/main.go
index ec081ba..c7fb8ce 100644
--- a/main.go
+++ b/main.go
@@ -17,6 +17,7 @@ import (
"slices"
"sort"
"strings"
+ "syscall"
"time"
"github.com/yuin/goldmark"
@@ -707,7 +708,7 @@ func (s *site) write(outputs map[string][]byte, manageGitignore bool) error {
from := filepath.Join(backup, filepath.FromSlash(movedOld[i]))
to := filepath.Join(s.root, filepath.FromSlash(movedOld[i]))
_ = os.MkdirAll(filepath.Dir(to), 0o755)
- _ = os.Rename(from, to)
+ _ = moveFile(from, to)
}
}
moveOld := func(rel string) error {
@@ -721,7 +722,7 @@ func (s *site) write(outputs map[string][]byte, manageGitignore bool) error {
if err := os.MkdirAll(filepath.Dir(to), 0o755); err != nil {
return err
}
- if err := os.Rename(from, to); err != nil {
+ if err := moveFile(from, to); err != nil {
return err
}
movedOld = append(movedOld, rel)
@@ -740,16 +741,28 @@ func (s *site) write(outputs map[string][]byte, manageGitignore bool) error {
install = append(install, ".gitignore")
}
for _, rel := range install {
+ to := filepath.Join(s.root, filepath.FromSlash(rel))
+ // Leave byte-identical outputs untouched so their mtimes (and thus
+ // Last-Modified/ETag headers) only change when content does.
+ if existing, err := os.ReadFile(to); err == nil {
+ staged, err := os.ReadFile(filepath.Join(stage, filepath.FromSlash(rel)))
+ if err != nil {
+ rollback()
+ return err
+ }
+ if bytes.Equal(existing, staged) {
+ continue
+ }
+ }
if err := moveOld(rel); err != nil {
rollback()
return err
}
- to := filepath.Join(s.root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(to), 0o755); err != nil {
rollback()
return err
}
- if err := os.Rename(filepath.Join(stage, filepath.FromSlash(rel)), to); err != nil {
+ if err := moveFile(filepath.Join(stage, filepath.FromSlash(rel)), to); err != nil {
rollback()
return err
}
@@ -758,6 +771,39 @@ func (s *site) write(outputs map[string][]byte, manageGitignore bool) error {
return nil
}
+// moveFile is os.Rename with a fallback for cross-device moves (EXDEV), which
+// happen when part of the site tree is a symlink onto another filesystem: the
+// contents are staged next to the destination and renamed into place there.
+func moveFile(from, to string) error {
+ err := os.Rename(from, to)
+ if err == nil || !errors.Is(err, syscall.EXDEV) {
+ return err
+ }
+ contents, err := os.ReadFile(from)
+ if err != nil {
+ return err
+ }
+ tmp, err := os.CreateTemp(filepath.Dir(to), ".weft-move-")
+ if err != nil {
+ return err
+ }
+ _, werr := tmp.Write(contents)
+ if cerr := tmp.Close(); werr == nil {
+ werr = cerr
+ }
+ if werr == nil {
+ werr = os.Chmod(tmp.Name(), 0o644)
+ }
+ if werr == nil {
+ werr = os.Rename(tmp.Name(), to)
+ }
+ if werr != nil {
+ _ = os.Remove(tmp.Name())
+ return werr
+ }
+ return os.Remove(from)
+}
+
func (s *site) gitignore(outputs []string) ([]byte, error) {
raw, err := os.ReadFile(filepath.Join(s.root, ".gitignore"))
if err != nil && !errors.Is(err, os.ErrNotExist) {