github.com/opentofu/opentofu@v1.7.1/internal/replacefile/writefile.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package replacefile
     7  
     8  import (
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  )
    13  
    14  // AtomicWriteFile uses a temporary file along with this package's AtomicRename
    15  // function in order to provide a replacement for ioutil.WriteFile that
    16  // writes the given file into place as atomically as the underlying operating
    17  // system can support.
    18  //
    19  // The sense of "atomic" meant by this function is that the file at the
    20  // given filename will either contain the entirety of the previous contents
    21  // or the entirety of the given data array if opened and read at any point
    22  // during the execution of the function.
    23  //
    24  // On some platforms attempting to overwrite a file that has at least one
    25  // open filehandle will produce an error. On other platforms, the overwriting
    26  // will succeed but existing open handles will still refer to the old file,
    27  // even though its directory entry is no longer present.
    28  //
    29  // Although AtomicWriteFile tries its best to avoid leaving behind its
    30  // temporary file on error, some particularly messy error cases may result
    31  // in a leftover temporary file.
    32  func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
    33  	dir, file := filepath.Split(filename)
    34  	if dir == "" {
    35  		// If the file is in the current working directory then dir will
    36  		// end up being "", but that's not right here because TempFile
    37  		// treats an empty dir as meaning "use the TMPDIR environment variable".
    38  		dir = "."
    39  	}
    40  	f, err := os.CreateTemp(dir, file) // alongside target file and with a similar name
    41  	if err != nil {
    42  		return fmt.Errorf("cannot create temporary file to update %s: %w", filename, err)
    43  	}
    44  	tmpName := f.Name()
    45  	moved := false
    46  	defer func(f *os.File, name string) {
    47  		// Remove the temporary file if it hasn't been moved yet. We're
    48  		// ignoring errors here because there's nothing we can do about
    49  		// them anyway.
    50  		if !moved {
    51  			os.Remove(name)
    52  		}
    53  	}(f, tmpName)
    54  
    55  	// We'll try to apply the requested permissions. This may
    56  	// not be effective on all platforms, but should at least work on
    57  	// Unix-like targets and should be harmless elsewhere.
    58  	if err := os.Chmod(tmpName, perm); err != nil {
    59  		return fmt.Errorf("cannot set mode for temporary file %s: %w", tmpName, err)
    60  	}
    61  
    62  	// Write the credentials to the temporary file, then immediately close
    63  	// it, whether or not the write succeeds. Note that closing the file here
    64  	// is required because on Windows we can't move a file while it's open.
    65  	_, err = f.Write(data)
    66  	f.Close()
    67  	if err != nil {
    68  		return fmt.Errorf("cannot write to temporary file %s: %w", tmpName, err)
    69  	}
    70  
    71  	// Temporary file now replaces the original file, as atomically as
    72  	// possible. (At the very least, we should not end up with a file
    73  	// containing only a partial JSON object.)
    74  	err = AtomicRename(tmpName, filename)
    75  	if err != nil {
    76  		return fmt.Errorf("failed to replace %s with temporary file %s: %w", filename, tmpName, err)
    77  	}
    78  
    79  	moved = true
    80  	return nil
    81  }