166 lines
4.6 KiB
Go
166 lines
4.6 KiB
Go
// Package fsutil contiene operaciones de archivo seguras compartidas: escritura
|
|
// atómica y copias de seguridad.
|
|
package fsutil
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// BackupFile copia path a "path.sysagent.YYYYMMDD-HHMMSS.bak" en el mismo
|
|
// directorio, conservando permisos y, si es posible, propietario (§62).
|
|
//
|
|
// Si ya existe un backup con ese nombre (dos ediciones en el mismo segundo)
|
|
// se añade un sufijo numérico en lugar de sobrescribirlo.
|
|
// Devuelve la ruta del backup.
|
|
func BackupFile(path string, now time.Time) (string, error) {
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open %s for backup: %w", path, err)
|
|
}
|
|
defer src.Close()
|
|
st, err := src.Stat()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to stat %s: %w", path, err)
|
|
}
|
|
if !st.Mode().IsRegular() {
|
|
return "", fmt.Errorf("cannot back up %s: not a regular file", path)
|
|
}
|
|
|
|
base := fmt.Sprintf("%s.sysagent.%s.bak", path, now.Format("20060102-150405"))
|
|
dst := base
|
|
var out *os.File
|
|
for i := 1; ; i++ {
|
|
out, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, st.Mode().Perm())
|
|
if err == nil {
|
|
break
|
|
}
|
|
if !os.IsExist(err) || i > 100 {
|
|
return "", fmt.Errorf("failed to create backup %s: %w", dst, err)
|
|
}
|
|
dst = fmt.Sprintf("%s.%d", base, i)
|
|
}
|
|
if _, err := io.Copy(out, src); err != nil {
|
|
out.Close()
|
|
os.Remove(dst)
|
|
return "", fmt.Errorf("failed to write backup %s: %w", dst, err)
|
|
}
|
|
if err := out.Sync(); err != nil {
|
|
out.Close()
|
|
return "", fmt.Errorf("failed to sync backup %s: %w", dst, err)
|
|
}
|
|
out.Close()
|
|
copyOwner(dst, st)
|
|
return dst, nil
|
|
}
|
|
|
|
// WriteAtomic reemplaza path con data de forma atómica (§30):
|
|
//
|
|
// 1. crea un archivo temporal en el mismo directorio (mismo filesystem);
|
|
// 2. escribe todo el contenido y hace fsync;
|
|
// 3. aplica permisos/propietario (los del original si existía);
|
|
// 4. ejecuta validate(tmp) si se indicó: si falla, el original queda intacto;
|
|
// 5. renombra sobre el original y hace fsync del directorio.
|
|
//
|
|
// Si path es un symlink, se escribe en su destino (como hacen los editores),
|
|
// para no sustituir el enlace por un archivo normal.
|
|
func WriteAtomic(path string, data []byte, mode os.FileMode, validate func(tmp string) error) error {
|
|
if real, err := filepath.EvalSymlinks(path); err == nil {
|
|
path = real
|
|
}
|
|
dir := filepath.Dir(path)
|
|
|
|
var orig os.FileInfo
|
|
if st, err := os.Stat(path); err == nil {
|
|
if st.IsDir() {
|
|
return fmt.Errorf("%s is a directory", path)
|
|
}
|
|
orig = st
|
|
if mode == 0 {
|
|
mode = st.Mode().Perm()
|
|
}
|
|
}
|
|
if mode == 0 {
|
|
mode = 0o644
|
|
}
|
|
|
|
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".sysagent-tmp-*")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temporary file in %s: %w", dir, err)
|
|
}
|
|
tmpName := tmp.Name()
|
|
cleanup := func() { os.Remove(tmpName) }
|
|
|
|
if _, err := tmp.Write(data); err != nil {
|
|
tmp.Close()
|
|
cleanup()
|
|
return fmt.Errorf("failed to write %s: %w", tmpName, err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
cleanup()
|
|
return fmt.Errorf("failed to sync %s: %w", tmpName, err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
cleanup()
|
|
return fmt.Errorf("failed to close %s: %w", tmpName, err)
|
|
}
|
|
if err := os.Chmod(tmpName, mode); err != nil {
|
|
cleanup()
|
|
return fmt.Errorf("failed to chmod %s: %w", tmpName, err)
|
|
}
|
|
if orig != nil {
|
|
copyOwner(tmpName, orig)
|
|
}
|
|
if validate != nil {
|
|
if err := validate(tmpName); err != nil {
|
|
cleanup()
|
|
return fmt.Errorf("validation failed, %s left unchanged: %w", path, err)
|
|
}
|
|
}
|
|
if err := os.Rename(tmpName, path); err != nil {
|
|
cleanup()
|
|
return fmt.Errorf("failed to rename %s to %s: %w", tmpName, path, err)
|
|
}
|
|
_ = SyncDir(dir)
|
|
return nil
|
|
}
|
|
|
|
// copyOwner intenta replicar uid/gid; solo funciona como root y se ignora
|
|
// el error en otro caso (el archivo queda a nombre del usuario actual).
|
|
func copyOwner(path string, st os.FileInfo) {
|
|
if sys, ok := st.Sys().(*syscall.Stat_t); ok {
|
|
_ = os.Lchown(path, int(sys.Uid), int(sys.Gid))
|
|
}
|
|
}
|
|
|
|
// SyncDir hace persistente en disco un rename dentro de dir.
|
|
func SyncDir(dir string) error {
|
|
d, err := os.Open(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer d.Close()
|
|
return d.Sync()
|
|
}
|
|
|
|
// CopyFile copia src a dst de forma atómica conservando permisos.
|
|
func CopyFile(src, dst string) error {
|
|
st, err := os.Stat(src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat %s: %w", src, err)
|
|
}
|
|
if !st.Mode().IsRegular() {
|
|
return fmt.Errorf("%s is not a regular file (use exec cp -a for directories)", src)
|
|
}
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s: %w", src, err)
|
|
}
|
|
return WriteAtomic(dst, data, st.Mode().Perm(), nil)
|
|
}
|