- 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>
98 lines
2.3 KiB
Go
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
|
|
}
|