1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
package dir
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"codeberg.org/snonux/gonf/internal/resource/file"
"codeberg.org/snonux/gonf/internal/resource/link"
opt "codeberg.org/snonux/gonf/api/option"
)
// 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, opt.WithSymlink(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,
opt.WithSource(sourcePath),
opt.WithMode(d.fileMode),
opt.WithOwner(d.user),
opt.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
}
|