|
| 1 | +package atomicfile |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "io" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | +) |
| 9 | + |
| 10 | +// WriteFile atomically writes the contents of r to the specified filepath with the given mode. |
| 11 | +// This is a copy of https://github.com/natefinch/atomic/blob/master/atomic.go with minor modifications allowing |
| 12 | +// to set mode of the written file. If the file already exists, its mode is preserved. |
| 13 | +func WriteFile(filename string, r io.Reader, mode os.FileMode) (err error) { |
| 14 | + // write to a temp file first, then we'll atomically replace the target file |
| 15 | + // with the temp file. |
| 16 | + dir, file := filepath.Split(filename) |
| 17 | + if dir == "" { |
| 18 | + dir = "." |
| 19 | + } |
| 20 | + |
| 21 | + f, err := os.CreateTemp(dir, file) |
| 22 | + if err != nil { |
| 23 | + return fmt.Errorf("cannot create temp file: %w", err) |
| 24 | + } |
| 25 | + defer func() { |
| 26 | + if err != nil { |
| 27 | + // Don't leave the temp file lying around on error. |
| 28 | + _ = os.Remove(f.Name()) // yes, ignore the error, not much we can do about it. |
| 29 | + } |
| 30 | + }() |
| 31 | + // ensure we always close f. Note that this does not conflict with the close below, as close is idempotent while |
| 32 | + // it returns an error for repeating close operations. |
| 33 | + defer f.Close() //nolint:errcheck |
| 34 | + name := f.Name() |
| 35 | + if _, err = io.Copy(f, r); err != nil { |
| 36 | + return fmt.Errorf("cannot write data to tempfile %q: %w", name, err) |
| 37 | + } |
| 38 | + // fsync is important, otherwise os.Rename could rename a zero-length file |
| 39 | + if err = f.Sync(); err != nil { |
| 40 | + return fmt.Errorf("can't flush tempfile %q: %w", name, err) |
| 41 | + } |
| 42 | + if err = f.Close(); err != nil { |
| 43 | + return fmt.Errorf("can't close tempfile %q: %w", name, err) |
| 44 | + } |
| 45 | + |
| 46 | + // get the file mode from the original file and use that for the replacement file, too. |
| 47 | + destInfo, err := os.Stat(filename) |
| 48 | + if os.IsNotExist(err) { |
| 49 | + // no original file |
| 50 | + if err = os.Chmod(name, mode); err != nil { |
| 51 | + return fmt.Errorf("can't set filemode on tempfile %q: %w", name, err) |
| 52 | + } |
| 53 | + } else if err != nil { |
| 54 | + return err |
| 55 | + } else { |
| 56 | + sourceInfo, err := os.Stat(name) |
| 57 | + if err != nil { |
| 58 | + return err |
| 59 | + } |
| 60 | + |
| 61 | + if sourceInfo.Mode() != destInfo.Mode() { |
| 62 | + if err = os.Chmod(name, destInfo.Mode()); err != nil { |
| 63 | + return fmt.Errorf("can't set filemode on tempfile %q: %w", name, err) |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + if err := os.Rename(name, filename); err != nil { |
| 68 | + return fmt.Errorf("cannot replace %q with tempfile %q: %w", filename, name, err) |
| 69 | + } |
| 70 | + return nil |
| 71 | +} |
0 commit comments