post inicial
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user