github.com/safing/portbase@v0.19.5/utils/renameio/tempfile.go (about) 1 package renameio 2 3 import ( 4 "errors" 5 "io/fs" 6 "os" 7 "path/filepath" 8 ) 9 10 // TempDir checks whether os.TempDir() can be used as a temporary directory for 11 // later atomically replacing files within dest. If no (os.TempDir() resides on 12 // a different mount point), dest is returned. 13 // 14 // Note that the returned value ceases to be valid once either os.TempDir() 15 // changes (e.g. on Linux, once the TMPDIR environment variable changes) or the 16 // file system is unmounted. 17 func TempDir(dest string) string { 18 return tempDir("", filepath.Join(dest, "renameio-TempDir")) 19 } 20 21 func tempDir(dir, dest string) string { 22 if dir != "" { 23 return dir // caller-specified directory always wins 24 } 25 26 // Chose the destination directory as temporary directory so that we 27 // definitely can rename the file, for which both temporary and destination 28 // file need to point to the same mount point. 29 fallback := filepath.Dir(dest) 30 31 // The user might have overridden the os.TempDir() return value by setting 32 // the TMPDIR environment variable. 33 tmpdir := os.TempDir() 34 35 testsrc, err := os.CreateTemp(tmpdir, "."+filepath.Base(dest)) 36 if err != nil { 37 return fallback 38 } 39 cleanup := true 40 defer func() { 41 if cleanup { 42 _ = os.Remove(testsrc.Name()) 43 } 44 }() 45 _ = testsrc.Close() 46 47 testdest, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest)) 48 if err != nil { 49 return fallback 50 } 51 defer func() { 52 _ = os.Remove(testdest.Name()) 53 }() 54 _ = testdest.Close() 55 56 if err := os.Rename(testsrc.Name(), testdest.Name()); err != nil { 57 return fallback 58 } 59 cleanup = false // testsrc no longer exists 60 return tmpdir 61 } 62 63 // PendingFile is a pending temporary file, waiting to replace the destination 64 // path in a call to CloseAtomicallyReplace. 65 type PendingFile struct { 66 *os.File 67 68 path string 69 done bool 70 closed bool 71 } 72 73 // Cleanup is a no-op if CloseAtomicallyReplace succeeded, and otherwise closes 74 // and removes the temporary file. 75 func (t *PendingFile) Cleanup() error { 76 if t.done { 77 return nil 78 } 79 // An error occurred. Close and remove the tempfile. Errors are returned for 80 // reporting, there is nothing the caller can recover here. 81 var closeErr error 82 if !t.closed { 83 closeErr = t.Close() 84 } 85 if err := os.Remove(t.Name()); err != nil { 86 return err 87 } 88 return closeErr 89 } 90 91 // CloseAtomicallyReplace closes the temporary file and atomically replaces 92 // the destination file with it, i.e., a concurrent open(2) call will either 93 // open the file previously located at the destination path (if any), or the 94 // just written file, but the file will always be present. 95 func (t *PendingFile) CloseAtomicallyReplace() error { 96 // Even on an ordered file system (e.g. ext4 with data=ordered) or file 97 // systems with write barriers, we cannot skip the fsync(2) call as per 98 // Theodore Ts'o (ext2/3/4 lead developer): 99 // 100 // > data=ordered only guarantees the avoidance of stale data (e.g., the previous 101 // > contents of a data block showing up after a crash, where the previous data 102 // > could be someone's love letters, medical records, etc.). Without the fsync(2) 103 // > a zero-length file is a valid and possible outcome after the rename. 104 if err := t.Sync(); err != nil { 105 return err 106 } 107 t.closed = true 108 if err := t.Close(); err != nil { 109 return err 110 } 111 if err := os.Rename(t.Name(), t.path); err != nil { 112 return err 113 } 114 t.done = true 115 return nil 116 } 117 118 // TempFile wraps os.CreateTemp for the use case of atomically creating or 119 // replacing the destination file at path. 120 // 121 // If dir is the empty string, TempDir(filepath.Base(path)) is used. If you are 122 // going to write a large number of files to the same file system, store the 123 // result of TempDir(filepath.Base(path)) and pass it instead of the empty 124 // string. 125 // 126 // The file's permissions will be 0600 by default. You can change these by 127 // explicitly calling Chmod on the returned PendingFile. 128 func TempFile(dir, path string) (*PendingFile, error) { 129 f, err := os.CreateTemp(tempDir(dir, path), "."+filepath.Base(path)) 130 if err != nil { 131 return nil, err 132 } 133 134 return &PendingFile{File: f, path: path}, nil 135 } 136 137 // Symlink wraps os.Symlink, replacing an existing symlink with the same name 138 // atomically (os.Symlink fails when newname already exists, at least on Linux). 139 func Symlink(oldname, newname string) error { 140 // Fast path: if newname does not exist yet, we can skip the whole dance 141 // below. 142 if err := os.Symlink(oldname, newname); err == nil || !errors.Is(err, fs.ErrExist) { 143 return err 144 } 145 146 // We need to use os.MkdirTemp, as we cannot overwrite a os.CreateTemp, 147 // and removing+symlinking creates a TOCTOU race. 148 d, err := os.MkdirTemp(filepath.Dir(newname), "."+filepath.Base(newname)) 149 if err != nil { 150 return err 151 } 152 cleanup := true 153 defer func() { 154 if cleanup { 155 _ = os.RemoveAll(d) 156 } 157 }() 158 159 symlink := filepath.Join(d, "tmp.symlink") 160 if err := os.Symlink(oldname, symlink); err != nil { 161 return err 162 } 163 164 if err := os.Rename(symlink, newname); err != nil { 165 return err 166 } 167 168 cleanup = false 169 return os.RemoveAll(d) 170 }