diff options
Diffstat (limited to 'internal/config/skel.go')
| -rw-r--r-- | internal/config/skel.go | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/config/skel.go b/internal/config/skel.go new file mode 100644 index 0000000..65d9f95 --- /dev/null +++ b/internal/config/skel.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "krino/internal/sexp" + "krino/internal/xdg" +) + +//go:embed skel/krino.conf +var skelMain []byte + +//go:embed skel/template.conf +var skelTemplate []byte + +// Reserved are subcommand names; a directory with one of them could not be +// run by name. +var Reserved = map[string]bool{"init": true, "new": true, "check": true, "explain": true, "log": true, "undo": true} + +// Init creates the directory holding mainFile with a commented krino.conf and +// template.conf, and a dirs/ directory. It never overwrites krino.conf, and +// keeps an existing template.conf. It returns the files it created. +func Init(mainFile string) ([]string, error) { + dir := filepath.Dir(mainFile) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, err + } + if err := writeNew(mainFile, skelMain); errors.Is(err, fs.ErrExist) { + return nil, fmt.Errorf("%s already exists; krino init leaves it alone", mainFile) + } else if err != nil { + return nil, err + } + created := []string{mainFile} + if err := os.MkdirAll(filepath.Join(dir, "dirs"), 0o755); err != nil { + return created, err + } + tmpl := filepath.Join(dir, "template.conf") + switch err := writeNew(tmpl, skelTemplate); { + case err == nil: + created = append(created, tmpl) + case !errors.Is(err, fs.ErrExist): + return created, err + } + return created, nil +} + +// NewDir creates dirs/<name>.conf from template.conf with the path filled +// in, and adds name to include in mainFile without changing anything else +// there. It returns the new file's path. +func NewDir(mainFile, name, path string) (string, error) { + if !nameRE.MatchString(name) { + return "", fmt.Errorf("bad directory name %q: use letters, digits, '.', '_' and '-'", name) + } + if Reserved[name] { + return "", fmt.Errorf("%q is a krino command; choose another name", name) + } + abs, err := filepath.Abs(xdg.Expand(path)) + if err != nil { + return "", err + } + if fi, err := os.Stat(abs); err != nil || !fi.IsDir() { + return "", fmt.Errorf("%s is not a directory", abs) + } + src, err := os.ReadFile(mainFile) + if errors.Is(err, fs.ErrNotExist) { + return "", fmt.Errorf("%s not found; create it with: krino init", mainFile) + } else if err != nil { + return "", err + } + m, errs := ParseMain(mainFile, src) + if len(errs) > 0 { + return "", fmt.Errorf("fix %s first: %v", mainFile, errs[0]) + } + if _, ok := m.IncludePos[name]; ok { + return "", fmt.Errorf("%q is already included", name) + } + tmplPath := filepath.Join(filepath.Dir(mainFile), "template.conf") + tmpl, err := os.ReadFile(tmplPath) + if errors.Is(err, fs.ErrNotExist) { + tmpl, err = skelTemplate, nil + } + if err != nil { + return "", err + } + file := DirFile(mainFile, name) + body := bytes.ReplaceAll(tmpl, []byte(`"@PATH@"`), []byte(sexp.Quote(xdg.Abbrev(abs)))) + newDir, derrs := ParseDir(name, file, body) + if len(derrs) > 0 { + return "", fmt.Errorf("template.conf is broken: %v", derrs[0]) + } + if newDir.Path != abs { + return "", fmt.Errorf("%s must contain (path \"@PATH@\")", tmplPath) + } + newMain := addInclude(src, m, name) + if _, errs := ParseMain(mainFile, newMain); len(errs) > 0 { + return "", fmt.Errorf("could not add %q to include: %v", name, errs[0]) + } + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + return "", err + } + if err := writeNew(file, body); errors.Is(err, fs.ErrExist) { + return "", fmt.Errorf("%s already exists", file) + } else if err != nil { + return "", err + } + if err := replaceFile(mainFile, newMain); err != nil { + os.Remove(file) + return "", err + } + return file, nil +} + +// addInclude inserts name after the last element of the (include ...) form, +// or appends an include form when there is none. +func addInclude(src []byte, m *Main, name string) []byte { + ins := " " + sexp.Quote(name) + if m.IncludeNode == nil { + out := bytes.Clone(src) + if len(out) > 0 && out[len(out)-1] != '\n' { + out = append(out, '\n') + } + return append(out, "(include"+ins+")\n"...) + } + kids := m.IncludeNode.Children + at := kids[len(kids)-1].End.Offset + out := make([]byte, 0, len(src)+len(ins)) + out = append(out, src[:at]...) + out = append(out, ins...) + return append(out, src[at:]...) +} + +// writeNew creates path holding data, failing if it already exists. +func writeNew(path string, data []byte) error { + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(path) + return err + } + return f.Close() +} + +// replaceFile atomically replaces the file at path, keeping its permissions. +// A symlink is followed and its target replaced, so dotfile links survive. +func replaceFile(path string, data []byte) error { + real, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + fi, err := os.Stat(real) + if err != nil { + return err + } + tmp, err := os.CreateTemp(filepath.Dir(real), ".krino-*") + if err != nil { + return err + } + defer os.Remove(tmp.Name()) + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Chmod(fi.Mode().Perm()); err != nil { + tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), real) +} |
