isle/go/toolkit/os.go

85 lines
1.9 KiB
Go

package toolkit
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"io/fs"
"os"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/sergi/go-diff/diffmatchpatch"
)
var differ = diffmatchpatch.New()
// WriteFileCheckChanged uses the given callback to collect data to be written
// to the given filepath with the given permission bits set. If the file's
// contents have been modified (or if the file didn't previously exist) then
// true is returned.
func WriteFileCheckChanged(
ctx context.Context,
logger *mlog.Logger,
path string,
mode os.FileMode,
fn func(io.Writer) error,
) (
bool, error,
) {
var (
buf = new(bytes.Buffer)
h = sha256.New()
w = io.MultiWriter(buf, h)
changed bool
)
if err := fn(w); err != nil {
return false, fmt.Errorf("callback returned: %w", err)
}
f, err := os.Open(path)
if errors.Is(err, fs.ErrNotExist) {
changed = true
// fine, we'll just write the file
} else if err != nil {
return false, fmt.Errorf("reading contents of existing file: %w", err)
} else {
var (
existingBuf = new(bytes.Buffer)
existingH = sha256.New()
existingW = io.MultiWriter(existingBuf, existingH)
_, copyErr = io.Copy(existingW, f)
closeErr = f.Close()
)
if err := errors.Join(closeErr, copyErr); err != nil {
return false, fmt.Errorf("hashing existing file: %w", err)
}
changed = !bytes.Equal(h.Sum(nil), existingH.Sum(nil))
if changed && logger.MaxLevel() >= mlog.LevelDebug.Int() {
var (
ctx = mctx.Annotate(ctx, "path", path)
diff = differ.DiffPrettyText(
differ.DiffMain(existingBuf.String(), buf.String(), false),
)
)
logger.Debug(ctx, "WriteFileCheckChanged diff:\n"+diff)
}
}
if changed {
if err := os.WriteFile(path, buf.Bytes(), mode); err != nil {
return false, fmt.Errorf("writing file: %w", err)
}
}
return changed, nil
}