v1.12.0
FIXED: * paths: Async searching works correctly now, and is consolidated to a single struct for searching options for async and synchronous searches.
This commit is contained in:
		
							parent
							
								
									eefe02afaf
								
							
						
					
					
						commit
						6ca22a097b
					
				
							
								
								
									
										11
									
								
								fsutils/errs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								fsutils/errs.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | package fsutils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	`syscall` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// Yes, I know. "Why ENOTTY?" I don't know, ask Linus. | ||||||
|  | 	// If you see "inappropriate ioctl for device", it's this'un. | ||||||
|  | 	ErrFsAttrsUnsupported error = syscall.ENOTTY | ||||||
|  | ) | ||||||
| @ -17,6 +17,17 @@ const ( | |||||||
| 	modeAnyExceptRegular pathMode = modeDir | modeSymlink | modePipe | modeSocket | modeDev | modeCharDev | modeIrregular | 	modeAnyExceptRegular pathMode = modeDir | modeSymlink | modePipe | modeSocket | modeDev | modeCharDev | modeIrregular | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Miss reasons | ||||||
|  | const ( | ||||||
|  | 	MissNoMiss  missReason = "" | ||||||
|  | 	MissNoMeta  missReason = "Could not determine metadata" | ||||||
|  | 	MissBadBase missReason = "Base name does not match BasePtrn" | ||||||
|  | 	MissBadPath missReason = "Path does not match PathPtrn" | ||||||
|  | 	MissBadTime missReason = "Time(s) does not/do not match Age" | ||||||
|  | 	MissFile    missReason = "Object is a file and NoFiles is set" | ||||||
|  | 	MissType    missReason = "Object does not match TargetType" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // Times | // Times | ||||||
| const TimeAny pathTimeType = 0 | const TimeAny pathTimeType = 0 | ||||||
| const ( | const ( | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								paths/errs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								paths/errs.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | package paths | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	`errors` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	ErrNilErrChan      error = errors.New("an initialized error channel is required") | ||||||
|  | 	ErrNilMatchChan    error = errors.New("an initialized matches channel is required") | ||||||
|  | 	ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required") | ||||||
|  | 	ErrNilWg           error = errors.New("a non-nil sync.WaitGroup is required") | ||||||
|  | ) | ||||||
							
								
								
									
										282
									
								
								paths/funcs.go
									
									
									
									
									
								
							
							
						
						
									
										282
									
								
								paths/funcs.go
									
									
									
									
									
								
							| @ -26,8 +26,7 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"os/user" | 	"os/user" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	`regexp` | 	`sort` | ||||||
| 	`slices` |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	`sync` | 	`sync` | ||||||
| 	`time` | 	`time` | ||||||
| @ -35,7 +34,6 @@ import ( | |||||||
| 	// "syscall" | 	// "syscall" | ||||||
| 
 | 
 | ||||||
| 	`github.com/djherbis/times` | 	`github.com/djherbis/times` | ||||||
| 	`golang.org/x/sync/semaphore` |  | ||||||
| 	`r00t2.io/goutils/bitmask` | 	`r00t2.io/goutils/bitmask` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -277,86 +275,33 @@ func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* | // SearchFsPaths gets a file/directory/etc. path list based on the provided criteria. | ||||||
| 	SearchPaths gets a file/directory path list based on the provided criteria. | func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) { | ||||||
| 
 | 
 | ||||||
| 	targetType defines what should be included in the path list. | 	var matched *FsSearchResult | ||||||
| 	It can consist of one or more (io/)fs.FileMode types OR'd together | 	var missed *FsSearchResult | ||||||
| 	(ensure they are part of (io/)fs.ModeType). |  | ||||||
| 	(You can use 0 to match regular files explicitly, and/or noFiles = true to exclude them.) |  | ||||||
| 
 | 
 | ||||||
| 	noFiles, if true, will explicitly filter out regular files from the path results. | 	if err = RealPath(&matcher.Root); err != nil { | ||||||
| 	(Normally they are *always* included regardless of targetType.) |  | ||||||
| 
 |  | ||||||
| 	basePtrn may be nil; if it isn't, it will be applied to *base names* |  | ||||||
| 	(that is, quux.txt rather than /foo/bar/baz/quux.txt). |  | ||||||
| 
 |  | ||||||
| 	pathPtrn is like basePtrn except it applies to the *entire* path, |  | ||||||
| 	not just the basename, if not nil (e.g. /foo/bar/baz/quux.txt, |  | ||||||
| 	not just quux.txt). |  | ||||||
| 
 |  | ||||||
| 	If age is not nil, it will be applied to the path object. |  | ||||||
| 	It will match older files/directories/etc. if olderThan is true, |  | ||||||
| 	otherwise it will match newer files/directories/etc. |  | ||||||
| 	(olderThan is not used otherwise.) |  | ||||||
| 
 |  | ||||||
| 	ageType is one or more Time* constants OR'd together to describe which timestamp type to check. |  | ||||||
| 	(Note that TimeCreated may not match if specified as it is only available on certain OSes, |  | ||||||
| 	kernel versions, and filesystems. This may lead to files being excluded that may have otherwise |  | ||||||
| 	been included.) |  | ||||||
| 	(You can use TimeAny to specify any supported time.) |  | ||||||
| 	*Any* matching timestamp of all specified (and supported) timestamp types matches, |  | ||||||
| 	so be judicious with your selection. They are processed in order of: |  | ||||||
| 
 |  | ||||||
| 		* btime (birth/creation time) (if supported) |  | ||||||
| 		* mtime (modification time -- contents have changed) |  | ||||||
| 		* ctime (OS-specific behavior; generally disk metadata has changed) (if supported) |  | ||||||
| 		* atime (access time) |  | ||||||
| 
 |  | ||||||
| 	olderThan (as mentioned above) will find paths *older* than age if true, otherwise *newer*. |  | ||||||
| 
 |  | ||||||
| 	now, if not nil, will be used to compare the age of files. (If nil, it will be populated at time of call.) |  | ||||||
| */ |  | ||||||
| func SearchFsPaths( |  | ||||||
| 	root string, |  | ||||||
| 	targetType fs.FileMode, noFiles bool, |  | ||||||
| 	basePtrn, pathPtrn *regexp.Regexp, |  | ||||||
| 	age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time, |  | ||||||
| ) (foundPaths []string, err error) { |  | ||||||
| 
 |  | ||||||
| 	if age != nil { |  | ||||||
| 		if now == nil { |  | ||||||
| 			now = new(time.Time) |  | ||||||
| 			*now = time.Now() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err = RealPath(&root); err != nil { |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = filepath.WalkDir( | 	if err = filepath.WalkDir( | ||||||
| 		root, | 		matcher.Root, | ||||||
| 		func(path string, d fs.DirEntry, inErr error) (outErr error) { | 		func(path string, d fs.DirEntry, inErr error) (outErr error) { | ||||||
| 
 | 
 | ||||||
| 			var include bool |  | ||||||
| 
 |  | ||||||
| 			if inErr != nil { | 			if inErr != nil { | ||||||
| 				outErr = inErr | 				outErr = inErr | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if include, outErr = filterPath( | 			if matched, missed, outErr = matcher.Match(path, d, nil); outErr != nil { | ||||||
| 				path, d, |  | ||||||
| 				targetType, noFiles, |  | ||||||
| 				basePtrn, pathPtrn, |  | ||||||
| 				age, ageType, olderThan, now, |  | ||||||
| 			); outErr != nil { |  | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 
 | 			if matched != nil && !matcher.NoMatch { | ||||||
| 			if include { | 				found = append(found, matched) | ||||||
| 				foundPaths = append(foundPaths, path) | 			} | ||||||
|  | 			if missed != nil && !matcher.NoMismatch { | ||||||
|  | 				miss = append(miss, missed) | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			return | 			return | ||||||
| @ -365,8 +310,18 @@ func SearchFsPaths( | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if found == nil || len(found) == 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// And sort them. | 	// And sort them. | ||||||
| 	slices.Sort(foundPaths) | 	sort.Slice( | ||||||
|  | 		found, | ||||||
|  | 		func(i, j int) (isLess bool) { | ||||||
|  | 			isLess = found[i].Path < found[j].Path | ||||||
|  | 			return | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
| 
 | 
 | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| @ -375,162 +330,107 @@ func SearchFsPaths( | |||||||
| 	SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent | 	SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent | ||||||
| 	workers for the filtering logic instead of performing iteratively/recursively. | 	workers for the filtering logic instead of performing iteratively/recursively. | ||||||
| 	It may, in some cases, be *slightly more* performant and *slightly less* in others. | 	It may, in some cases, be *slightly more* performant and *slightly less* in others. | ||||||
| 	Additional options are documented below. | 	Note that unlike SearchFsPaths, the results written to the | ||||||
| 	Note that unlike SearchFsPaths, the results written to foundPathsChan are not | 	FsSearchCriteriaAsync.ResChan are not guaranteed to be in any predictable order. | ||||||
| 	guaranteed to be in any predictable order. |  | ||||||
| 
 | 
 | ||||||
| 	All channels are expected to have been initialized by the caller ahead of time, | 	All channels are expected to have already been initialized by the caller. | ||||||
| 	and all provided channels will be closed upon completion (so they are only safe | 	They will not be closed by this function. | ||||||
| 	to READ from after invoking SearchFsPathsAsync). |  | ||||||
| 
 |  | ||||||
| 	foundPathsChan is a channel to which matched filepaths will be written. |  | ||||||
| 
 |  | ||||||
| 	sem/semCtx are optional; if not nil, they can be used to limit/"batch" concurrent tasks. |  | ||||||
| 	(semCtx is the context.Context used for sem when acquiring. It may be nil; |  | ||||||
| 	one will be locally created if so.) |  | ||||||
| 	The default will be to spawn all filtering logic concurrently. |  | ||||||
| 	For very large directories, you almost assuredly do not want that -- it |  | ||||||
| 	can cause a significant amount of I/O and CPU wait. |  | ||||||
| 	(See https://pkg.go.dev/golang.org/x/sync/semaphore for details.) |  | ||||||
| 
 |  | ||||||
| 	wg *must not* be nil, and must be managed by the caller. |  | ||||||
| 	SearchFsPathsAsync will exit with no errors but no-op if wg is nil. |  | ||||||
| 
 |  | ||||||
| 	errChan will receive any/all encountered errors. |  | ||||||
| */ | */ | ||||||
| func SearchFsPathsAsync( | func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) { | ||||||
| 	root string, |  | ||||||
| 	targetType fs.FileMode, noFiles bool, |  | ||||||
| 	basePtrn, pathPtrn *regexp.Regexp, |  | ||||||
| 	age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time, |  | ||||||
| 	foundPathsChan chan string, |  | ||||||
| 	sem *semaphore.Weighted, semCtx context.Context, |  | ||||||
| 	wg *sync.WaitGroup, |  | ||||||
| 	errChan chan error, |  | ||||||
| ) { |  | ||||||
| 
 | 
 | ||||||
| 	var err error | 	var err error | ||||||
| 	var localWg sync.WaitGroup | 	var wgLocal sync.WaitGroup | ||||||
|  | 	var doneChan chan bool = make(chan bool, 1) | ||||||
| 
 | 
 | ||||||
| 	if wg == nil { | 	if matcher.ErrChan == nil { | ||||||
|  | 		panic(ErrNilErrChan) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if age != nil { | 	if matcher.WG == nil { | ||||||
| 		if now == nil { | 		matcher.ErrChan <- ErrNilWg | ||||||
| 			now = new(time.Time) | 		return | ||||||
| 			*now = time.Now() |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if sem != nil && semCtx == nil { | 	defer matcher.WG.Done() | ||||||
| 		semCtx = context.Background() | 
 | ||||||
|  | 	if matcher.ResChan == nil && !matcher.NoMatch { | ||||||
|  | 		matcher.ErrChan <- ErrNilMatchChan | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if matcher.MismatchChan == nil && !matcher.NoMismatch { | ||||||
|  | 		matcher.ErrChan <- ErrNilMismatchChan | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err = RealPath(&matcher.Root); err != nil { | ||||||
|  | 		matcher.ErrChan <- err | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if matcher.Semaphore != nil && matcher.SemaphoreCtx == nil { | ||||||
|  | 		matcher.SemaphoreCtx = context.Background() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = filepath.WalkDir( | 	if err = filepath.WalkDir( | ||||||
| 		root, | 		matcher.Root, | ||||||
| 		func(path string, de fs.DirEntry, inErr error) (outErr error) { | 		func(path string, de fs.DirEntry, inErr error) (outErr error) { | ||||||
| 			localWg.Add(1) | 
 | ||||||
| 			wg.Add(1) | 			if inErr != nil { | ||||||
| 			if sem != nil { | 				inErr = filterNoFileDir(inErr) | ||||||
| 				if err = sem.Acquire(semCtx, 1); err != nil { | 				if inErr != nil { | ||||||
|  | 					outErr = inErr | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			wgLocal.Add(1) | ||||||
|  | 			if matcher.Semaphore != nil { | ||||||
|  | 				if err = matcher.Semaphore.Acquire(matcher.SemaphoreCtx, 1); err != nil { | ||||||
| 					return | 					return | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			go func(p string, d fs.DirEntry) { | 			go func(p string, d fs.DirEntry) { | ||||||
| 				var pErr error | 				var pErr error | ||||||
| 				var pInclude bool | 				var pResMatch *FsSearchResult | ||||||
|  | 				var pResMiss *FsSearchResult | ||||||
| 
 | 
 | ||||||
| 				defer localWg.Done() | 				defer wgLocal.Done() | ||||||
| 				defer wg.Done() |  | ||||||
| 
 | 
 | ||||||
| 				if sem != nil { | 				if matcher.Semaphore != nil { | ||||||
| 					defer sem.Release(1) | 					defer matcher.Semaphore.Release(1) | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if pInclude, pErr = filterPath(p, d, targetType, noFiles, basePtrn, pathPtrn, age, ageType, olderThan, now); pErr != nil { | 				if pResMatch, pResMiss, pErr = matcher.Match(p, d, nil); pErr != nil { | ||||||
| 					errChan <- pErr | 					matcher.ErrChan <- pErr | ||||||
| 					return | 					return | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if pInclude { | 				if pResMatch != nil && !matcher.NoMatch { | ||||||
| 					foundPathsChan <- p | 					matcher.ResChan <- pResMatch | ||||||
|  | 				} | ||||||
|  | 				if pResMiss != nil && !matcher.NoMismatch { | ||||||
|  | 					matcher.MismatchChan <- pResMiss | ||||||
| 				} | 				} | ||||||
| 			}(path, de) | 			}(path, de) | ||||||
| 
 | 
 | ||||||
| 			return | 			return | ||||||
| 		}, | 		}, | ||||||
| 	); err != nil { | 	); err != nil { | ||||||
| 		errChan <- err | 		err = filterNoFileDir(err) | ||||||
|  | 		if err != nil { | ||||||
|  | 			matcher.ErrChan <- err | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	go func() { | 	go func() { | ||||||
| 		localWg.Wait() | 		wgLocal.Wait() | ||||||
| 		close(foundPathsChan) | 		doneChan <- true | ||||||
| 		close(errChan) |  | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
| 	return | 	<-doneChan | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // filterPath applies the filter logic used by SearchFSPaths and SearchFsPathsAync. |  | ||||||
| func filterPath( |  | ||||||
| 	path string, d fs.DirEntry, |  | ||||||
| 	targetType fs.FileMode, noFiles bool, |  | ||||||
| 	basePtrn, pathPtrn *regexp.Regexp, |  | ||||||
| 	age *time.Duration, ageType pathTimeType, olderThan bool, now *time.Time, |  | ||||||
| ) (include bool, err error) { |  | ||||||
| 
 |  | ||||||
| 	var typeMode fs.FileMode |  | ||||||
| 	var fi fs.FileInfo |  | ||||||
| 	var tspec times.Timespec |  | ||||||
| 	var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(targetType)) |  | ||||||
| 
 |  | ||||||
| 	if age != nil { |  | ||||||
| 		if now == nil { |  | ||||||
| 			now = new(time.Time) |  | ||||||
| 			*now = time.Now() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// patterns |  | ||||||
| 	if pathPtrn != nil { |  | ||||||
| 		if !pathPtrn.MatchString(path) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if basePtrn != nil { |  | ||||||
| 		if !basePtrn.MatchString(filepath.Base(path)) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// age |  | ||||||
| 	if age != nil { |  | ||||||
| 		if tspec, err = times.Stat(path); err != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		if !filterTimes(tspec, age, &ageType, olderThan, now) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// fs object type (file, dir, etc.) |  | ||||||
| 	if fi, err = d.Info(); err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	typeMode = fi.Mode().Type() |  | ||||||
| 	if typeMode == 0 && noFiles { |  | ||||||
| 		return |  | ||||||
| 	} else if typeMode != 0 { |  | ||||||
| 		if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	include = true |  | ||||||
| 
 | 
 | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| @ -597,3 +497,13 @@ func filterTimes(tspec times.Timespec, age *time.Duration, ageType *pathTimeType | |||||||
| 
 | 
 | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func filterNoFileDir(err error) (filtered error) { | ||||||
|  | 
 | ||||||
|  | 	filtered = err | ||||||
|  | 	if errors.Is(err, fs.ErrNotExist) { | ||||||
|  | 		filtered = nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										125
									
								
								paths/funcs_fssearchcriteria.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								paths/funcs_fssearchcriteria.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | |||||||
|  | package paths | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	`io/fs` | ||||||
|  | 	`os` | ||||||
|  | 	`path/filepath` | ||||||
|  | 	`time` | ||||||
|  | 
 | ||||||
|  | 	`github.com/djherbis/times` | ||||||
|  | 	`r00t2.io/goutils/bitmask` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 	Match returns match (a  ptr to a FsSearchResult if the specified path matches, otherwise nil), | ||||||
|  | 	miss (ptr the specified path does not match, otherwise nil), and an fs.DirEntry and fs.FileInfo | ||||||
|  | 	for path. d and/or fi may be nil. | ||||||
|  | 
 | ||||||
|  | 	If err is not nil, it represents an unexpected error and as such, both match and miss should be nil. | ||||||
|  | 
 | ||||||
|  | 	Match, miss, and err will all be nil if the filesystem object/path does not exist. | ||||||
|  | */ | ||||||
|  | func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (match, miss *FsSearchResult, err error) { | ||||||
|  | 
 | ||||||
|  | 	var typeMode fs.FileMode | ||||||
|  | 	var m FsSearchResult | ||||||
|  | 	var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(f.TargetType)) | ||||||
|  | 
 | ||||||
|  | 	m = FsSearchResult{ | ||||||
|  | 		Path:     path, | ||||||
|  | 		DirEntry: d, | ||||||
|  | 		FileInfo: fi, | ||||||
|  | 		Criteria: f, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if f == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// A DirEntry can be created from a FileInfo but not vice versa. | ||||||
|  | 	if m.FileInfo == nil { | ||||||
|  | 		if m.DirEntry != nil { | ||||||
|  | 			if m.FileInfo, err = m.DirEntry.Info(); err != nil { | ||||||
|  | 				err = filterNoFileDir(err) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if f.FollowSymlinks { | ||||||
|  | 				if m.FileInfo, err = os.Stat(path); err != nil { | ||||||
|  | 					err = filterNoFileDir(err) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if m.FileInfo, err = os.Lstat(path); err != nil { | ||||||
|  | 					err = filterNoFileDir(err) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if m.DirEntry == nil { | ||||||
|  | 		m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo) | ||||||
|  | 	} | ||||||
|  | 	if m.DirEntry == nil || m.FileInfo == nil { | ||||||
|  | 		m.MissReason = MissNoMeta | ||||||
|  | 		miss = &m | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if m.Times, err = times.Stat(path); err != nil { | ||||||
|  | 		err = filterNoFileDir(err) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if f.PathPtrn != nil && !f.PathPtrn.MatchString(path) { | ||||||
|  | 		m.MissReason = MissBadPath | ||||||
|  | 		miss = &m | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if f.BasePtrn != nil && !f.BasePtrn.MatchString(filepath.Base(path)) { | ||||||
|  | 		m.MissReason = MissBadBase | ||||||
|  | 		miss = &m | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// age | ||||||
|  | 	if f.Age != nil { | ||||||
|  | 		if f.Now == nil { | ||||||
|  | 			f.Now = new(time.Time) | ||||||
|  | 			*f.Now = time.Now() | ||||||
|  | 		} | ||||||
|  | 		if !filterTimes(m.Times, f.Age, &f.AgeType, f.OlderThan, f.Now) { | ||||||
|  | 			m.MissReason = MissBadTime | ||||||
|  | 			miss = &m | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// fs object type (file, dir, etc.) | ||||||
|  | 	typeMode = m.FileInfo.Mode().Type() | ||||||
|  | 	if typeMode == 0 && f.NoFiles { | ||||||
|  | 		m.MissReason = MissFile | ||||||
|  | 		miss = &m | ||||||
|  | 		return | ||||||
|  | 	} else if typeMode != 0 { | ||||||
|  | 		if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) { | ||||||
|  | 			m.MissReason = MissType | ||||||
|  | 			miss = &m | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If it gets to here, it matches. | ||||||
|  | 	match = &m | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										127
									
								
								paths/types.go
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								paths/types.go
									
									
									
									
									
								
							| @ -1,9 +1,136 @@ | |||||||
| package paths | package paths | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	`context` | ||||||
|  | 	`io/fs` | ||||||
|  | 	`regexp` | ||||||
|  | 	`sync` | ||||||
|  | 	`time` | ||||||
|  | 
 | ||||||
|  | 	`github.com/djherbis/times` | ||||||
|  | 	`golang.org/x/sync/semaphore` | ||||||
| 	`r00t2.io/goutils/bitmask` | 	`r00t2.io/goutils/bitmask` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // FsSearchCriteria contains filter criteria for SearchFsPaths* functions. | ||||||
|  | type FsSearchCriteria struct { | ||||||
|  | 	// Root indicates the root to search. | ||||||
|  | 	Root string `json:"root" toml:"RootPath" yaml:"Root Path" xml:"root,attr" validate:"dir"` | ||||||
|  | 	// NoMatch, if true, will not return matches. If NoMatch and NoMismatch are both true, no results will be returned. | ||||||
|  | 	NoMatch bool `json:"no_match" toml:"NoMatch" yaml:"No Matches" xml:"noMatch,attr"` | ||||||
|  | 	// NoMismatch, if true, will not return mismatches. If NoMatch and NoMismatch are both true, no results will be returned. | ||||||
|  | 	NoMismatch bool `json:"no_miss" toml:"NoMismatch" yaml:"No Mismatches" xml:"noMiss,attr"` | ||||||
|  | 	/* | ||||||
|  | 		TargetType defines what types of filesystem objects should be matched. | ||||||
|  | 		It can consist of one or more (io/)fs.FileMode types OR'd together | ||||||
|  | 		(ensure they are part of (io/)fs.ModeType). | ||||||
|  | 		(You can use 0 to match regular files explicitly, and/or NoFiles = true to exclude them.) | ||||||
|  | 	*/ | ||||||
|  | 	TargetType fs.FileMode `json:"type_tgt" toml:"TargetType" yaml:"Target Type" xml:"typeTgt,attr"` | ||||||
|  | 	// NoFiles excludes files from TargetType-matching (as there isn't a way to explicitly exclude files otherwise if a non-zero mode is given). | ||||||
|  | 	NoFiles bool `json:"no_file" toml:"ExcludeFiles" yaml:"Exclude Files" xml:"noFile,attr"` | ||||||
|  | 	// FollowSymlinks, if true and a path being tested is a symlink, will use metadata (age, etc.) of the symlink itself rather than the link target. | ||||||
|  | 	FollowSymlinks bool `json:"follow_sym" toml:"FollowSymlinks" yaml:"Follow Symlinks" xml:"followSym,attr"` | ||||||
|  | 	// BasePtrn, if specified, will apply to the *base name (that is, quux.txt rather than /foo/bar/baz/quux.txt). See also PathPtrn. | ||||||
|  | 	BasePtrn *regexp.Regexp `json:"ptrn_base,omitempty" toml:"BaseNamePattern,omitempty" yaml:"Base Name Pattern,omitempty" xml:"ptrnBase,attr,omitempty"` | ||||||
|  | 	// PathPtrn, if specified, will apply to the *full path* (e.g. /foo/bar/baz/quux.txt, not just quux.txt). See also BasePtrn. | ||||||
|  | 	PathPtrn *regexp.Regexp `json:"ptrn_path,omitempty" toml:"PathPattern,omitempty" yaml:"Path Pattern,omitempty" xml:"ptrnPath,attr,omitempty"` | ||||||
|  | 	/* | ||||||
|  | 		Age, if specified, indicates the comparison of Now againt the AgeType of filesystem objects. | ||||||
|  | 		Use OlderThan to indicate if it should be older or newer. | ||||||
|  | 	*/ | ||||||
|  | 	Age *time.Duration `json:"age,omitempty" toml:"Age,omitempty" yaml:"Age,omitempty" xml:"age,attr,omitempty"` | ||||||
|  | 	/* | ||||||
|  | 		AgeType can be one (or more, OR'd together) of the Time* constants in this package (TimeAny, TimeAccessed, TimeCreated, | ||||||
|  | 		TimeChanged, TimeModified) to indicate what timestamp(s) to use for comparing Age. | ||||||
|  | 
 | ||||||
|  | 		The zero-value is TimeAny. | ||||||
|  | 
 | ||||||
|  | 		The first matching timestamp will pass all time comparisons. | ||||||
|  | 		Be mindful of timestamp type support/limitations per OS/filesystem of Root. | ||||||
|  | 
 | ||||||
|  | 		Completely unused if Age is nil. | ||||||
|  | 	*/ | ||||||
|  | 	AgeType pathTimeType `json:"type_age" toml:"AgeType" yaml:"Age Type" xml:"typeAge,attr"` | ||||||
|  | 	/* | ||||||
|  | 		OlderThan, if true (and Age is not nil), indicates that matching filesystem objects should have their | ||||||
|  | 		AgeType older than Now. If false, their AgeType should be *newer* than Now. | ||||||
|  | 
 | ||||||
|  | 		Completely unused if Age is nil. | ||||||
|  | 	*/ | ||||||
|  | 	OlderThan bool `json:"older" toml:"OlderThan" yaml:"Older Than" xml:"older,attr"` | ||||||
|  | 	/* | ||||||
|  | 		Now expresses a time to compare to Age via AgeType and OlderThan. | ||||||
|  | 		Note that it may be any valid time, not necessarily "now". | ||||||
|  | 		If Age is specified but Now is nil, it will be populated with time.Now() when the search is invoked. | ||||||
|  | 
 | ||||||
|  | 		Completely unused if Age is nil. | ||||||
|  | 	*/ | ||||||
|  | 	Now *time.Time `json:"now,omitempty" toml:"Now,omitempty" yaml:"Now,omitempty" xml:"now,attr,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FsSearchCriteriaAsync extends FsSearchCriteria for use in an asynchronous (goroutine) manner. | ||||||
|  | type FsSearchCriteriaAsync struct { | ||||||
|  | 	FsSearchCriteria | ||||||
|  | 	/* | ||||||
|  | 		WG should be a non-nil pointer to a sync.WaitGroup. | ||||||
|  | 		This is used to manage searching completion to the caller. | ||||||
|  | 
 | ||||||
|  | 		.Done() will be called once within the search function, but no .Add() will be called; | ||||||
|  | 		.Add() should be done by the caller beforehand. | ||||||
|  | 	*/ | ||||||
|  | 	WG *sync.WaitGroup | ||||||
|  | 	// ResChan must be a non-nil channel for (positive) match results to be sent to. | ||||||
|  | 	ResChan chan *FsSearchResult | ||||||
|  | 	// MismatchChan, if not nil, will have negative matches/"misses" sent to it. | ||||||
|  | 	MismatchChan chan *FsSearchResult | ||||||
|  | 	/* | ||||||
|  | 		ErrChan should be a non-nil error channel for any unexpected errors encountered. | ||||||
|  | 
 | ||||||
|  | 		If nil, a panic will be raised. | ||||||
|  | 	*/ | ||||||
|  | 	ErrChan chan error | ||||||
|  | 	/* | ||||||
|  | 		Semaphore is completely optional, but if non-nil | ||||||
|  | 		it will be used to limit concurrent filesystem | ||||||
|  | 		object processing. | ||||||
|  | 
 | ||||||
|  | 		It is generally a Very Good Idea(TM) to use this, | ||||||
|  | 		as the default is to dispatch all processing concurrently. | ||||||
|  | 		This can lead to some heavy I/O and CPU wait. | ||||||
|  | 
 | ||||||
|  | 		(See https://pkg.go.dev/golang.org/x/sync/semaphore for details.) | ||||||
|  | 	*/ | ||||||
|  | 	Semaphore *semaphore.Weighted | ||||||
|  | 	/* | ||||||
|  | 		SemaphoreCtx is the context.Context to use for Semaphore. | ||||||
|  | 		If nil (but Sempaphore is not), one will be created locally/internally. | ||||||
|  | 	*/ | ||||||
|  | 	SemaphoreCtx context.Context | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FsSearchResult contains a match/miss result for FsSearchCriteria and FsSearchCriteriaAsync. | ||||||
|  | type FsSearchResult struct { | ||||||
|  | 	/* | ||||||
|  | 		Path is the path to the object on the filesystem. | ||||||
|  | 		It may or may not exist at the time of return, | ||||||
|  | 		but will not be an empty string. | ||||||
|  | 	*/ | ||||||
|  | 	Path string `json:"path" toml:"Path" yaml:"Path" xml:"path,attr"` | ||||||
|  | 	// DirEntry is the fs.DirEntry for the Path; note that .Name() is the base name only. TODO: serialization? | ||||||
|  | 	DirEntry fs.DirEntry `json:"-" toml:"-" yaml:"-" xml:"-"` | ||||||
|  | 	// FileInfo is the fs.FileInfo for the Path; note that .Name() is the base name only. TODO: serialization? | ||||||
|  | 	FileInfo fs.FileInfo `json:"-" toml:"-" yaml:"-" xml:"-"` | ||||||
|  | 	// Criteria is the evaluated criteria specified that this FsSearchResult matched. | ||||||
|  | 	Criteria *FsSearchCriteria `json:"criteria" toml:"Criteria" yaml:"Criteria" xml:"criteria"` | ||||||
|  | 	// Times holds the mtime, ctime, etc. of the filesystem object (where supported). TODO: serialization? | ||||||
|  | 	Times times.Timespec `json:"-" toml:"-" yaml:"-" xml:"-"` | ||||||
|  | 	// MissReason contains the reason the result is a miss (MissNoMiss if a match); see the Miss* constants. | ||||||
|  | 	MissReason missReason `json:"miss_reason" toml:"MissReason" yaml:"Miss Reason" xml:"miss,attr"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type missReason string | ||||||
|  | 
 | ||||||
| type pathMode bitmask.MaskBit | type pathMode bitmask.MaskBit | ||||||
| 
 | 
 | ||||||
| type pathTimeType bitmask.MaskBit | type pathTimeType bitmask.MaskBit | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 brent saner
						brent saner