package main import ( "errors" "fmt" "os" "path/filepath" ) // writeFileAtomic replaces the file at path with data through a temporary file // and a rename. A reader or a crash of the process sees the old file or the new // file, never a part of one. func writeFileAtomic(path string, data []byte) error { file, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*") if err != nil { return fmt.Errorf("create temporary file: %w", err) } _, err = file.Write(data) err = errors.Join(err, file.Close()) if err == nil { err = os.Rename(file.Name(), path) } if err != nil { return errors.Join(fmt.Errorf("replace file: %w", err), os.Remove(file.Name())) } return nil } // writeFileDurable replaces path only after syncing its private temporary file. // It syncs the directory after rename before it reports success. An error after // rename has an uncertain outcome, so callers must stop writes until recovery. func writeFileDurable(path string, data []byte) (err error) { file, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*") if err != nil { return fmt.Errorf("create durable temporary file: %w", err) } defer func() { removeErr := os.Remove(file.Name()) if removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) { err = errors.Join(err, fmt.Errorf("remove durable temporary file: %w", removeErr)) } }() _, err = file.Write(data) if err == nil { err = file.Sync() } err = errors.Join(err, file.Close()) if err != nil { return fmt.Errorf("write durable temporary file: %w", err) } err = os.Rename(file.Name(), path) if err != nil { return fmt.Errorf("publish durable file: %w", err) } directory, err := os.Open(filepath.Dir(path)) if err != nil { return fmt.Errorf("open durable directory: %w", err) } err = errors.Join(directory.Sync(), directory.Close()) if err != nil { return fmt.Errorf("sync durable directory: %w", err) } return nil }