file.go 5.16 KB
Newer Older
songlinfeng's avatar
songlinfeng committed
1
2
3
4
5
6
7
/**
# Copyright (c) 2024, HCUOpt CORPORATION.  All rights reserved.
**/

package lookup

import (
8
	"dcu-container-toolkit/internal/logger"
songlinfeng's avatar
songlinfeng committed
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
	"fmt"
	"os"
	"path/filepath"
)

// file can be used to locate file (or file-like elements) at a specified set of
// prefixes. The validity of a file is determined by a filter function.
type file struct {
	builder
	prefixes []string
}

// builder defines the builder for a file locator.
type builder struct {
	logger      logger.Interface
	root        string
	searchPaths []string
	filter      func(string) error
	count       int
	isOptional  bool
}

// Option defines a function for passing builder to the NewFileLocator() call
type Option func(*builder)

// WithRoot sets the root for the file locator
func WithRoot(root string) Option {
	return func(f *builder) {
		f.root = root
	}
}

// WithLogger sets the logger for the file locator
func WithLogger(logger logger.Interface) Option {
	return func(f *builder) {
		f.logger = logger
	}
}

// WithSearchPaths sets the search paths for the file locator.
func WithSearchPaths(paths ...string) Option {
	return func(f *builder) {
		f.searchPaths = paths
	}
}

// WithFilter sets the filter for the file locator
// The filter is called for each candidate file and candidates that return nil are considered.
func WithFilter(assert func(string) error) Option {
	return func(f *builder) {
		f.filter = assert
	}
}

// WithCount sets the maximum number of candidates to discover
func WithCount(count int) Option {
	return func(f *builder) {
		f.count = count
	}
}

// WithOptional sets the optional flag for the file locator
// If the optional flag is set, the locator will not return an error if the file is not found.
func WithOptional(optional bool) Option {
	return func(f *builder) {
		f.isOptional = optional
	}
}

func newBuilder(opts ...Option) *builder {
	o := &builder{}
	for _, opt := range opts {
		opt(o)
	}
	if o.logger == nil {
		o.logger = logger.New()
	}
	if o.filter == nil {
		o.filter = assertFile
	}
	return o
}

func (o builder) build() *file {
	f := file{
		builder: o,
		// Since the `Locate` implementations rely on the root already being specified we update
		// the prefixes to include the root.
		prefixes: getSearchPrefixes(o.root, o.searchPaths...),
	}
	return &f
}

// NewFileLocator creates a Locator that can be used to find files with the specified builder.
func NewFileLocator(opts ...Option) Locator {
	return newFileLocator(opts...)
}

func newFileLocator(opts ...Option) *file {
	return newBuilder(opts...).build()
}

// getSearchPrefixes generates a list of unique paths to be searched by a file locator.
//
// For each of the unique prefixes <p> specified, the path <root><p> is searched, where <root> is the
// specified root. If no prefixes are specified, <root> is returned as the only search prefix.
//
// Note that an empty root is equivalent to searching relative to the current working directory, and
// if the root filesystem should be searched instead, root should be specified as "/" explicitly.
//
// Also, a prefix of "" forces the root to be included in returned set of paths. This means that if
// the root in addition to another prefix must be searched the function should be called with:
//
//	getSearchPrefixes("/root", "", "another/path")
//
// and will result in the search paths []{"/root", "/root/another/path"} being returned.
func getSearchPrefixes(root string, prefixes ...string) []string {
	seen := make(map[string]bool)
	var uniquePrefixes []string
	for _, p := range prefixes {
		if seen[p] {
			continue
		}
		seen[p] = true
		uniquePrefixes = append(uniquePrefixes, filepath.Join(root, p))
	}

	if len(uniquePrefixes) == 0 {
		uniquePrefixes = append(uniquePrefixes, root)
	}

	return uniquePrefixes
}

var _ Locator = (*file)(nil)

// Locate attempts to find files with names matching the specified pattern.
// All prefixes are searched and any matching candidates are returned. If no matches are found, an error is returned.
func (p file) Locate(pattern string) ([]string, error) {
	var filenames []string

	p.logger.Debugf("Locating %q in %v", pattern, p.prefixes)
visit:
	for _, prefix := range p.prefixes {
		pathPattern := filepath.Join(prefix, pattern)
		candidates, err := filepath.Glob(pathPattern)
		if err != nil {
			p.logger.Debugf("Checking pattern '%v' failed: %v", pathPattern, err)
		}

		for _, candidate := range candidates {
			p.logger.Debugf("Checking candidate '%v'", candidate)
			err := p.filter(candidate)
			if err != nil {
				p.logger.Debugf("Candidate '%v' does not meet requirements: %v", candidate, err)
				continue
			}
			filenames = append(filenames, candidate)
			if p.count > 0 && len(filenames) == p.count {
				p.logger.Debugf("Found %d candidates; ignoring further candidates", len(filenames))
				break visit
			}
		}
	}

	if !p.isOptional && len(filenames) == 0 {
		return nil, fmt.Errorf("pattern %v %w", pattern, ErrNotFound)
	}
	return filenames, nil
}

// assertFile checks whether the specified path is a regular file
func assertFile(filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return fmt.Errorf("error getting info for %v: %v", filename, err)
	}

	if info.IsDir() {
		return fmt.Errorf("specified path '%v' is a directory", filename)
	}

	return nil
}