package vfs import ( "bytes" "encoding/hex" "errors" "fmt" "io" "io/fs" "path" "pkg.jfrech.com/brief/internal/crand" ) func ReadFile(fsys FS, name string) ([]byte, error) { f, err := fsys.OpenFile(name, O_RDONLY, 0) if err != nil { return nil, err } data, err := io.ReadAll(f) if err != nil { f.Close() return nil, err } err = f.Close() if err != nil { return nil, err } return data, err } // WriteFileTRUNC is a Rename-based atomic wrapper. // WriteFileTRUNC simulates an os.O_TRUNC write call by func WriteFileTRUNC(fsys FS, name string, data []byte, perm fs.FileMode) error { dir, _ := path.Split(name) tmp, err := func() (string, error) { return path.Join(dir, ".tmp-" + hex.EncodeToString(crand.ReadN(24))), nil }() if err != nil { return err } if true { err := fsys.MkdirAll(dir, 0755) if err != nil { return err } f, err := fsys.OpenFile(tmp, O_WRONLY|O_CREATE|O_EXCL, perm) if err != nil { return err } // [2024-01-14, jfrech] TODO Think about dropping the bytes.Clone call. // (How likely is the scenario that fsys' files lie in their io.Writer // implementation?) n, err := f.Write(bytes.Clone(data)) if err == nil && len(data) != n { err = io.ErrShortWrite } if err != nil { f.Close() fsys.Remove(tmp) return err } err = f.Sync() if err != nil { f.Close() fsys.Remove(tmp) return err } err = f.Close() if err != nil { fsys.Remove(tmp) return err } } err = fsys.Rename(tmp, name) if err != nil { fsys.Remove(tmp) return err } return nil } // WriteFile is a Rename-based atomic wrapper. func WriteFile(fsys FS, name string, data []byte, perm fs.FileMode) error { if true { f, err := fsys.OpenFile(name, O_WRONLY|O_CREATE|O_EXCL, perm) if errors.Is(err, fs.ErrExist) { return fmt.Errorf("WriteFile would truncate data: %w", err) } else if err != nil { return err } // warm up the file (e.g. signal a faulty network filesystem that the file truly exists) n, err := f.Write(make([]byte, 0)) if err == nil && 0 != n { err = io.ErrShortWrite } if err != nil { f.Close() return err } err = f.Sync() if err != nil { f.Close() return err } err = f.Close() if err != nil { return err } } /* [2024-01-14, jfrech] TODO Think about this old code? err = fsys.Rename(tmpname, name) if errors.Is(err, fs.ErrPermission) { err2 := fsys.Chmod(name, 0200) if err2 != nil { // TODO vfs.ErrNonAtomicOperation enao := fmt.Errorf("pkg.jfrech.com/brief/internal/vfs: non-atomic operation") //return fmt.Errorf("%w: no chmod: %v", err, err2) return fmt.Errorf("%w: no rename, no chmod: %v, %v", enao, err, err2) } err = fsys.Rename(tmpname, name) } return err } // ... // [2023-12-17, jfrech] TODO ugly var _ = fsys.Chmod(name, 0200) var _ = fsys.Remove(name) */ return WriteFileTRUNC(fsys, name, data, perm) }