github.com/safing/portbase@v0.19.5/utils/atomic.go (about)

     1  package utils
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  
    10  	"github.com/safing/portbase/utils/renameio"
    11  )
    12  
    13  // AtomicFileOptions holds additional options for manipulating
    14  // the behavior of CreateAtomic and friends.
    15  type AtomicFileOptions struct {
    16  	// Mode is the file mode for the new file. If
    17  	// 0, the file mode will be set to 0600.
    18  	Mode os.FileMode
    19  
    20  	// TempDir is the path to the temp-directory
    21  	// that should be used. If empty, it defaults
    22  	// to the system temp.
    23  	TempDir string
    24  }
    25  
    26  // CreateAtomic creates or overwrites a file at dest atomically using
    27  // data from r. Atomic means that even in case of a power outage,
    28  // dest will never be a zero-length file. It will always either contain
    29  // the previous data (or not exist) or the new data but never anything
    30  // in between.
    31  func CreateAtomic(dest string, r io.Reader, opts *AtomicFileOptions) error {
    32  	if opts == nil {
    33  		opts = &AtomicFileOptions{}
    34  	}
    35  
    36  	tmpFile, err := renameio.TempFile(opts.TempDir, dest)
    37  	if err != nil {
    38  		return fmt.Errorf("failed to create temp file: %w", err)
    39  	}
    40  	defer tmpFile.Cleanup() //nolint:errcheck
    41  
    42  	if opts.Mode != 0 {
    43  		if err := tmpFile.Chmod(opts.Mode); err != nil {
    44  			return fmt.Errorf("failed to update mode bits of temp file: %w", err)
    45  		}
    46  	}
    47  
    48  	if _, err := io.Copy(tmpFile, r); err != nil {
    49  		return fmt.Errorf("failed to copy source file: %w", err)
    50  	}
    51  
    52  	if err := tmpFile.CloseAtomicallyReplace(); err != nil {
    53  		return fmt.Errorf("failed to rename temp file to %q", dest)
    54  	}
    55  
    56  	return nil
    57  }
    58  
    59  // CopyFileAtomic is like CreateAtomic but copies content from
    60  // src to dest. If opts.Mode is 0 CopyFileAtomic tries to set
    61  // the file mode of src to dest.
    62  func CopyFileAtomic(dest string, src string, opts *AtomicFileOptions) error {
    63  	if opts == nil {
    64  		opts = &AtomicFileOptions{}
    65  	}
    66  
    67  	if opts.Mode == 0 {
    68  		stat, err := os.Stat(src)
    69  		if err != nil {
    70  			return err
    71  		}
    72  		opts.Mode = stat.Mode()
    73  	}
    74  
    75  	f, err := os.Open(src)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	defer func() {
    80  		_ = f.Close()
    81  	}()
    82  
    83  	return CreateAtomic(dest, f, opts)
    84  }
    85  
    86  // ReplaceFileAtomic replaces the file at dest with the content from src.
    87  // If dest exists it's file mode copied and used for the replacement. If
    88  // not, dest will get the same file mode as src. See CopyFileAtomic and
    89  // CreateAtomic for more information.
    90  func ReplaceFileAtomic(dest string, src string, opts *AtomicFileOptions) error {
    91  	if opts == nil {
    92  		opts = &AtomicFileOptions{}
    93  	}
    94  
    95  	if opts.Mode == 0 {
    96  		stat, err := os.Stat(dest)
    97  		if err == nil {
    98  			opts.Mode = stat.Mode()
    99  		} else if !errors.Is(err, fs.ErrNotExist) {
   100  			return err
   101  		}
   102  	}
   103  
   104  	return CopyFileAtomic(dest, src, opts)
   105  }