github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/internal/renameio/renameio.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package renameio writes files atomically by renaming temporary files. 6 package renameio 7 8 import ( 9 "bytes" 10 "io" 11 "math/rand" 12 "os" 13 "path/filepath" 14 "strconv" 15 16 "github.com/amarpal/go-tools/internal/robustio" 17 ) 18 19 const patternSuffix = ".tmp" 20 21 // Pattern returns a glob pattern that matches the unrenamed temporary files 22 // created when writing to filename. 23 func Pattern(filename string) string { 24 return filepath.Join(filepath.Dir(filename), filepath.Base(filename)+patternSuffix) 25 } 26 27 // WriteFile is like ioutil.WriteFile, but first writes data to an arbitrary 28 // file in the same directory as filename, then renames it atomically to the 29 // final name. 30 // 31 // That ensures that the final location, if it exists, is always a complete file. 32 func WriteFile(filename string, data []byte, perm os.FileMode) (err error) { 33 return WriteToFile(filename, bytes.NewReader(data), perm) 34 } 35 36 // WriteToFile is a variant of WriteFile that accepts the data as an io.Reader 37 // instead of a slice. 38 func WriteToFile(filename string, data io.Reader, perm os.FileMode) (err error) { 39 f, err := tempFile(filepath.Dir(filename), filepath.Base(filename), perm) 40 if err != nil { 41 return err 42 } 43 defer func() { 44 // Only call os.Remove on f.Name() if we failed to rename it: otherwise, 45 // some other process may have created a new file with the same name after 46 // that. 47 if err != nil { 48 f.Close() 49 os.Remove(f.Name()) 50 } 51 }() 52 53 if _, err := io.Copy(f, data); err != nil { 54 return err 55 } 56 // Sync the file before renaming it: otherwise, after a crash the reader may 57 // observe a 0-length file instead of the actual contents. 58 // See https://golang.org/issue/22397#issuecomment-380831736. 59 if err := f.Sync(); err != nil { 60 return err 61 } 62 if err := f.Close(); err != nil { 63 return err 64 } 65 66 return robustio.Rename(f.Name(), filename) 67 } 68 69 // ReadFile is like ioutil.ReadFile, but on Windows retries spurious errors that 70 // may occur if the file is concurrently replaced. 71 // 72 // Errors are classified heuristically and retries are bounded, so even this 73 // function may occasionally return a spurious error on Windows. 74 // If so, the error will likely wrap one of: 75 // - syscall.ERROR_ACCESS_DENIED 76 // - syscall.ERROR_FILE_NOT_FOUND 77 // - internal/syscall/windows.ERROR_SHARING_VIOLATION 78 func ReadFile(filename string) ([]byte, error) { 79 return robustio.ReadFile(filename) 80 } 81 82 // tempFile creates a new temporary file with given permission bits. 83 func tempFile(dir, prefix string, perm os.FileMode) (f *os.File, err error) { 84 for i := 0; i < 10000; i++ { 85 name := filepath.Join(dir, prefix+strconv.Itoa(rand.Intn(1000000000))+patternSuffix) 86 f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm) 87 if os.IsExist(err) { 88 continue 89 } 90 break 91 } 92 return 93 }