diff options
Diffstat (limited to 'internal/resource/dir/source.go')
| -rw-r--r-- | internal/resource/dir/source.go | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/internal/resource/dir/source.go b/internal/resource/dir/source.go new file mode 100644 index 0000000..8f9b73a --- /dev/null +++ b/internal/resource/dir/source.go @@ -0,0 +1,126 @@ +package dir + +import ( + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + + "codeberg.org/snonux/gonf/internal/resource/file" + "codeberg.org/snonux/gonf/internal/resource/link" +) + +// copySourceTree mirrors d.source into d.path, dispatching each entry by +// kind. Symlink-ness is checked before the dir/file branches: fs.DirEntry +// reports a symlink's own type via Lstat semantics (never following it), so +// a symlink in the source tree is recreated as a symlink rather than read as +// file content. +func copySourceTree(d *Dir) error { + log.Printf("installing files from source %s to %s", d.source, d.path) + + return filepath.WalkDir(d.source, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(d.source, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + + target := filepath.Join(d.path, rel) + + switch { + case entry.Type()&fs.ModeSymlink != 0: + return copySourceSymlink(path, target) + case entry.IsDir(): + return copySourceDir(d, target) + default: + return copySourceFile(d, path, target) + } + }) +} + +func copySourceDir(d *Dir, target string) error { + if err := os.MkdirAll(target, d.mode); err != nil { + return fmt.Errorf("failed to create directory %s: %w", target, err) + } + return applyAttributesTo(target, d.mode, d.user, d.group) +} + +// copySourceSymlink recreates the symlink found at sourcePath as a symlink +// at target, preserving its raw (unresolved) link target string. This is +// correct as long as the destination tree mirrors the source tree 1:1; an +// absolute link target pointing back into the source tree itself is not +// remapped into the destination — a pre-existing conceptual limitation of +// copying a tree of symlinks. +func copySourceSymlink(sourcePath, target string) error { + rawTarget, err := os.Readlink(sourcePath) + if err != nil { + return fmt.Errorf("failed to read symlink %s: %w", sourcePath, err) + } + return link.Ensure(target, link.IsSymlink(rawTarget)) +} + +// copySourceFile delegates writing a single copied file to the file +// package's own primitive, using d's file-mode default (not d's directory +// mode) and passing the mechanically-derived target path verbatim — file's +// own resolve() strips a ".tmpl" suffix and computes .Param consistently, so +// dir needs no special-casing of its own. +func copySourceFile(d *Dir, sourcePath, target string) error { + return file.Ensure(target, + file.WithSource(sourcePath), + file.WithMode(d.fileMode), + file.WithUser(d.user), + file.WithGroup(d.group), + ) +} + +// pruneTree removes anything under d.path that has no counterpart in +// d.source. A destination entry also counts as having a counterpart if +// d.source has the same relative path with a ".tmpl" suffix appended, since +// copySourceFile (via file.Ensure) strips that suffix when writing — +// otherwise every templated file would be pruned immediately after being +// copied. +func pruneTree(d *Dir) error { + log.Printf("pruning destination directory %s", d.path) + + return filepath.WalkDir(d.path, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(d.path, path) + if err != nil { + return err + } + if rel == "." { + return nil // don't prune the root itself + } + + if sourceEntryExists(d.source, rel) { + return nil + } + + log.Printf("pruning %s", path) + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("failed to prune %s: %w", path, err) + } + if entry.IsDir() { + return filepath.SkipDir // already removed + } + return nil + }) +} + +func sourceEntryExists(source, rel string) bool { + if _, err := os.Lstat(filepath.Join(source, rel)); err == nil { + return true + } + _, err := os.Lstat(filepath.Join(source, rel) + ".tmpl") + return err == nil +} |
