summaryrefslogtreecommitdiff
path: root/internal/resource/dir/source.go
blob: 8f9b73a0d57a33b35262219fb3cad12b4543b56e (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
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
}