summaryrefslogtreecommitdiff
path: root/internal/resource/dir/source.go
blob: 15395907fd48bb8bc99cf3e89b81714364b362a3 (plain)
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/options"
)

// 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
}