| 🛟 Updated. 28d5985 k33g 16h ago | 1 | // Package projectfile writes the TOML files a project keeps in the editor's own |
| 2 | // directory — .turbo-go, .turbo-rust — namely its settings, its snippets and |
| 3 | // its tools. |
| 4 | // |
| 5 | // It exists because three packages were writing one identically: the same |
| 6 | // temporary file, the same rename, the same mode. A fourth would have been the |
| 7 | // point at which nobody remembered which copy was the correct one. |
| 8 | // |
| 9 | // if err := projectfile.Write(path, []byte(contents)); err != nil { |
| 10 | // return err |
| 11 | // } |
| 12 | package projectfile |
| 13 | |
| 14 | import ( |
| 15 | "fmt" |
| 16 | "os" |
| 17 | "path/filepath" |
| 18 | ) |
| 19 | |
| 20 | // fileMode is what a project's configuration file is created with: readable by |
| 21 | // everyone, writable by its owner. These files are meant to be committed and |
| 22 | // read by a team. |
| 23 | const fileMode = 0o644 |
| 24 | |
| 25 | // dirMode is what the editor's own directory is created with. |
| 26 | const dirMode = 0o755 |
| 27 | |
| 28 | // Write creates or replaces a project file, making its directory if need be. |
| 29 | // |
| 30 | // The write goes to a temporary file in the same directory which is then |
| 31 | // renamed over the target, so an interrupted write leaves the previous file |
| 32 | // intact rather than half of the new one. The temporary must share the |
| 33 | // directory because a rename is only atomic within one filesystem. |
| 34 | // |
| 35 | // This is deliberately **not** what internal/buffer does for a source file. |
| 36 | // That one preserves the mode of the file it is replacing, because it is saving |
| 37 | // over something the user already had; this one is creating a file the editor |
| 38 | // is introducing, and a fixed mode is the right answer for it. |
| 39 | // |
| 40 | // err := projectfile.Write(".turbo-go/tools.toml", []byte(template)) |
| 41 | func Write(path string, data []byte) error { |
| 42 | if err := write(path, data); err != nil { |
| 43 | return fmt.Errorf("writing %s: %w", path, err) |
| 44 | } |
| 45 | return nil |
| 46 | } |
| 47 | |
| 48 | // write does the work, leaving the error to be named by Write. |
| 49 | func write(path string, data []byte) error { |
| 50 | directory := filepath.Dir(path) |
| 51 | if err := os.MkdirAll(directory, dirMode); err != nil { |
| 52 | return err |
| 53 | } |
| 54 | |
| 55 | temporary, err := os.CreateTemp(directory, ".projectfile-*.toml") |
| 56 | if err != nil { |
| 57 | return err |
| 58 | } |
| 59 | name := temporary.Name() |
| 60 | defer os.Remove(name) // a no-op once the rename below has succeeded |
| 61 | |
| 62 | if err := writeAndClose(temporary, data); err != nil { |
| 63 | return err |
| 64 | } |
| 65 | // CreateTemp makes the file readable by its owner only, and the rename |
| 66 | // would carry that through to a file a team is meant to share. |
| 67 | if err := os.Chmod(name, fileMode); err != nil { |
| 68 | return err |
| 69 | } |
| 70 | return os.Rename(name, path) |
| 71 | } |
| 72 | |
| 73 | // writeAndClose writes a file and closes it, closing it either way. |
| 74 | // |
| 75 | // The close is what reports a write that failed on its way out of the buffer, |
| 76 | // so its error matters as much as the write's. |
| 77 | func writeAndClose(f *os.File, data []byte) error { |
| 78 | if _, err := f.Write(data); err != nil { |
| 79 | f.Close() |
| 80 | return err |
| 81 | } |
| 82 | return f.Close() |
| 83 | } |