homelab/services/ha-sync/internal/sync/walker.go
Dan V deb6c38d7b chore: commit homelab setup — deployment, services, orchestration, skill
- Add .gitignore: exclude compiled binaries, build artifacts, and Helm
  values files containing real secrets (authentik, prometheus)
- Add all Kubernetes deployment manifests (deployment/)
- Add services source code: ha-sync, device-inventory, games-console,
  paperclip, parts-inventory
- Add Ansible orchestration: playbooks, roles, inventory, cloud-init
- Add hardware specs, execution plans, scripts, HOMELAB.md
- Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill
- Remove previously-tracked inventory-cli binary from git index

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 08:10:32 +02:00

98 lines
2.3 KiB
Go

package sync
import (
"io/fs"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"time"
)
// FileInfo holds metadata for a single file or directory found during a walk.
type FileInfo struct {
RelPath string
AbsPath string
Size int64
ModTime time.Time
IsDir bool
Owner string // username of the file owner (best-effort; empty if not resolvable)
}
// Walk returns all files and directories under root with paths relative to root.
// Non-regular files (sockets, devices, pipes) are silently skipped.
// Any entry whose relative path or base name matches an exclude glob pattern is
// also skipped (directories matching a pattern are pruned entirely).
func Walk(root string, excludes []string) ([]FileInfo, error) {
var files []FileInfo
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
// Skip entries matching any exclude pattern.
base := filepath.Base(rel)
for _, pat := range excludes {
if matchGlob(pat, rel) || matchGlob(pat, base) {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
}
info, err := d.Info()
if err != nil {
return err
}
// Skip non-regular, non-directory entries (sockets, devices, pipes, etc.).
if !d.IsDir() && !info.Mode().IsRegular() {
return nil
}
files = append(files, FileInfo{
RelPath: rel,
AbsPath: path,
Size: info.Size(),
ModTime: info.ModTime(),
IsDir: d.IsDir(),
Owner: resolveOwner(path),
})
return nil
})
return files, err
}
// resolveOwner returns the username of the file owner on Linux via syscall.
// Returns an empty string if it cannot be determined.
func resolveOwner(path string) string {
info, err := os.Lstat(path)
if err != nil {
return ""
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return ""
}
u, err := user.LookupId(strconv.Itoa(int(stat.Uid)))
if err != nil {
return strconv.Itoa(int(stat.Uid))
}
return u.Username
}
// matchGlob returns true if name matches the given glob pattern.
// Any error from filepath.Match is treated as no-match.
func matchGlob(pattern, name string) bool {
matched, _ := filepath.Match(pattern, name)
return matched
}