mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
2332a7ab47
ioutil.TempFile has a special case where an empty string for its dir argument is interpreted as a request to automatically look up the system temporary directory, which is commonly /tmp . We don't want that behavior here because we're specifically trying to create the temporary file in the same directory as the file we're hoping to replace. If the file gets created in /tmp then it might be on a different device and thus the later atomic rename won't work. Instead, we'll add our own special case to explicitly use "." when the given filename is in the current working directory. That overrides the special automatic behavior of ioutil.TempFile and thus forces the behavior we need. This hadn't previously mattered for earlier callers of this code because they were creating files in subdirectories, but this codepath was failing for the dependency lock file due to it always being created directly in the current working directory. Unfortunately since this is a picky implementation detail I couldn't find a good way to write a unit test for it without considerable refactoring. Instead, I verified manually that the temporary filename wasn't in /tmp on my Linux system, and hope that the comment inline will explain this situation well enough to avoid an accidental regression in future maintenence.
78 lines
2.8 KiB
Go
78 lines
2.8 KiB
Go
package replacefile
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// AtomicWriteFile uses a temporary file along with this package's AtomicRename
|
|
// function in order to provide a replacement for ioutil.WriteFile that
|
|
// writes the given file into place as atomically as the underlying operating
|
|
// system can support.
|
|
//
|
|
// The sense of "atomic" meant by this function is that the file at the
|
|
// given filename will either contain the entirety of the previous contents
|
|
// or the entirety of the given data array if opened and read at any point
|
|
// during the execution of the function.
|
|
//
|
|
// On some platforms attempting to overwrite a file that has at least one
|
|
// open filehandle will produce an error. On other platforms, the overwriting
|
|
// will succeed but existing open handles will still refer to the old file,
|
|
// even though its directory entry is no longer present.
|
|
//
|
|
// Although AtomicWriteFile tries its best to avoid leaving behind its
|
|
// temporary file on error, some particularly messy error cases may result
|
|
// in a leftover temporary file.
|
|
func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
|
|
dir, file := filepath.Split(filename)
|
|
if dir == "" {
|
|
// If the file is in the current working directory then dir will
|
|
// end up being "", but that's not right here because TempFile
|
|
// treats an empty dir as meaning "use the TMPDIR environment variable".
|
|
dir = "."
|
|
}
|
|
f, err := ioutil.TempFile(dir, file) // alongside target file and with a similar name
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create temporary file to update %s: %s", filename, err)
|
|
}
|
|
tmpName := f.Name()
|
|
moved := false
|
|
defer func(f *os.File, name string) {
|
|
// Remove the temporary file if it hasn't been moved yet. We're
|
|
// ignoring errors here because there's nothing we can do about
|
|
// them anyway.
|
|
if !moved {
|
|
os.Remove(name)
|
|
}
|
|
}(f, tmpName)
|
|
|
|
// We'll try to apply the requested permissions. This may
|
|
// not be effective on all platforms, but should at least work on
|
|
// Unix-like targets and should be harmless elsewhere.
|
|
if err := os.Chmod(tmpName, perm); err != nil {
|
|
return fmt.Errorf("cannot set mode for temporary file %s: %s", tmpName, err)
|
|
}
|
|
|
|
// Write the credentials to the temporary file, then immediately close
|
|
// it, whether or not the write succeeds. Note that closing the file here
|
|
// is required because on Windows we can't move a file while it's open.
|
|
_, err = f.Write(data)
|
|
f.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot write to temporary file %s: %s", tmpName, err)
|
|
}
|
|
|
|
// Temporary file now replaces the original file, as atomically as
|
|
// possible. (At the very least, we should not end up with a file
|
|
// containing only a partial JSON object.)
|
|
err = AtomicRename(tmpName, filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to replace %s with temporary file %s: %s", filename, tmpName, err)
|
|
}
|
|
|
|
moved = true
|
|
return nil
|
|
}
|