143 lines
3.5 KiB
Go
143 lines
3.5 KiB
Go
package filecatalog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// FindYAMLFiles returns sorted full paths for .yaml and .yml files under root.
|
|
func FindYAMLFiles(ctx context.Context, root string) ([]string, error) {
|
|
var files []string
|
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if !IsYAMLFile(d.Name()) {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
})
|
|
sort.Strings(files)
|
|
return files, err
|
|
}
|
|
|
|
// FindFSYAMLFiles returns sorted paths for .yaml and .yml files under root in fsys.
|
|
func FindFSYAMLFiles(ctx context.Context, fsys fs.FS, root string) ([]string, error) {
|
|
cleanRoot := CleanFSRoot(root)
|
|
var files []string
|
|
err := fs.WalkDir(fsys, cleanRoot, func(name string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if !IsYAMLFile(d.Name()) {
|
|
return nil
|
|
}
|
|
files = append(files, name)
|
|
return nil
|
|
})
|
|
sort.Strings(files)
|
|
return files, err
|
|
}
|
|
|
|
// RelativePath computes a clean relative path from root to path.
|
|
func RelativePath(root string, filePath string) string {
|
|
rel, err := filepath.Rel(root, filePath)
|
|
if err != nil {
|
|
return filepath.Clean(filePath)
|
|
}
|
|
return filepath.Clean(rel)
|
|
}
|
|
|
|
// CleanFSRoot normalizes a root path for use with fs.FS.
|
|
func CleanFSRoot(root string) string {
|
|
root = strings.TrimSpace(root)
|
|
if root == "" || root == "." {
|
|
return "."
|
|
}
|
|
return path.Clean(root)
|
|
}
|
|
|
|
// DisplayPath returns name relative to root for messages about fs.FS paths.
|
|
func DisplayPath(root string, name string) string {
|
|
cleanRoot := CleanFSRoot(root)
|
|
cleanName := path.Clean(name)
|
|
if cleanRoot == "." {
|
|
return cleanName
|
|
}
|
|
prefix := strings.TrimSuffix(cleanRoot, "/") + "/"
|
|
if strings.HasPrefix(cleanName, prefix) {
|
|
return strings.TrimPrefix(cleanName, prefix)
|
|
}
|
|
return cleanName
|
|
}
|
|
|
|
// ResolveFSPath resolves userPath from baseDir and keeps it inside root.
|
|
func ResolveFSPath(root string, baseDir string, userPath string) (string, string, error) {
|
|
cleanRoot := CleanFSRoot(root)
|
|
cleanBase := path.Clean(strings.TrimSpace(baseDir))
|
|
if cleanBase == "" {
|
|
cleanBase = cleanRoot
|
|
}
|
|
if !containsFSPath(cleanRoot, cleanBase) {
|
|
return "", "", fmt.Errorf("base path %q is outside source root %q", cleanBase, cleanRoot)
|
|
}
|
|
|
|
cleanUserPath := strings.TrimSpace(userPath)
|
|
if cleanUserPath == "" {
|
|
return "", "", fmt.Errorf("path is required")
|
|
}
|
|
cleanUserPath = path.Clean(cleanUserPath)
|
|
if path.IsAbs(cleanUserPath) {
|
|
return "", "", fmt.Errorf("path %q must be relative", userPath)
|
|
}
|
|
|
|
resolved := path.Clean(path.Join(cleanBase, cleanUserPath))
|
|
if !containsFSPath(cleanRoot, resolved) {
|
|
return "", "", fmt.Errorf("path %q escapes source root %q", userPath, cleanRoot)
|
|
}
|
|
return resolved, DisplayPath(cleanRoot, resolved), nil
|
|
}
|
|
|
|
func containsFSPath(root string, name string) bool {
|
|
root = CleanFSRoot(root)
|
|
name = path.Clean(name)
|
|
if root == "." {
|
|
return name == "." || (name != ".." && !strings.HasPrefix(name, "../"))
|
|
}
|
|
return name == root || strings.HasPrefix(name, strings.TrimSuffix(root, "/")+"/")
|
|
}
|
|
|
|
// Stem strips .yaml or .yml from a file name.
|
|
func Stem(name string) string {
|
|
name = strings.TrimSuffix(name, ".yaml")
|
|
name = strings.TrimSuffix(name, ".yml")
|
|
return name
|
|
}
|
|
|
|
func IsYAMLFile(name string) bool {
|
|
return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")
|
|
}
|