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 }