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 }