// Package projectfile writes the TOML files a project keeps in the editor's own // directory — .turbo-go, .turbo-rust — namely its settings, its snippets and // its tools. // // It exists because three packages were writing one identically: the same // temporary file, the same rename, the same mode. A fourth would have been the // point at which nobody remembered which copy was the correct one. // // if err := projectfile.Write(path, []byte(contents)); err != nil { // return err // } package projectfile import ( "fmt" "os" "path/filepath" ) // fileMode is what a project's configuration file is created with: readable by // everyone, writable by its owner. These files are meant to be committed and // read by a team. const fileMode = 0o644 // dirMode is what the editor's own directory is created with. const dirMode = 0o755 // Write creates or replaces a project file, making its directory if need be. // // The write goes to a temporary file in the same directory which is then // renamed over the target, so an interrupted write leaves the previous file // intact rather than half of the new one. The temporary must share the // directory because a rename is only atomic within one filesystem. // // This is deliberately **not** what internal/buffer does for a source file. // That one preserves the mode of the file it is replacing, because it is saving // over something the user already had; this one is creating a file the editor // is introducing, and a fixed mode is the right answer for it. // // err := projectfile.Write(".turbo-go/tools.toml", []byte(template)) func Write(path string, data []byte) error { if err := write(path, data); err != nil { return fmt.Errorf("writing %s: %w", path, err) } return nil } // write does the work, leaving the error to be named by Write. func write(path string, data []byte) error { directory := filepath.Dir(path) if err := os.MkdirAll(directory, dirMode); err != nil { return err } temporary, err := os.CreateTemp(directory, ".projectfile-*.toml") if err != nil { return err } name := temporary.Name() defer os.Remove(name) // a no-op once the rename below has succeeded if err := writeAndClose(temporary, data); err != nil { return err } // CreateTemp makes the file readable by its owner only, and the rename // would carry that through to a file a team is meant to share. if err := os.Chmod(name, fileMode); err != nil { return err } return os.Rename(name, path) } // writeAndClose writes a file and closes it, closing it either way. // // The close is what reports a write that failed on its way out of the buffer, // so its error matters as much as the write's. func writeAndClose(f *os.File, data []byte) error { if _, err := f.Write(data); err != nil { f.Close() return err } return f.Close() }