diff --git a/go.mod b/go.mod index b8c202c..873e078 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.2 require ( github.com/davecgh/go-spew v1.1.1 + github.com/djherbis/times v1.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 golang.org/x/sys v0.26.0 honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 diff --git a/go.sum b/go.sum index 97ca174..8d693a1 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,17 @@ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg= -github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE= -r00t2.io/goutils v1.7.0 h1:iQluWlkOyBwOKaK94D5QSnSMYpGKtMb/5WjefmdfHgI= -r00t2.io/goutils v1.7.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc= r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= -r00t2.io/sysutils v1.7.0 h1:zk5IbcbZvq11FoXI/fLvcgyq36lBhPDY6fvC9CunfWE= -r00t2.io/sysutils v1.7.0/go.mod h1:Sk/7riJp9fteeW9STkdQ/k22huL1J6r05n6wLh5byHY= diff --git a/paths/consts.go b/paths/consts.go new file mode 100644 index 0000000..8a91bbc --- /dev/null +++ b/paths/consts.go @@ -0,0 +1,31 @@ +package paths + +import ( + `io/fs` +) + +// Mostly just for reference. +const ( + // ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular + modeDir pathMode = pathMode(fs.ModeDir) + modeSymlink pathMode = pathMode(fs.ModeSymlink) + modePipe pathMode = pathMode(fs.ModeNamedPipe) + modeSocket pathMode = pathMode(fs.ModeSocket) + modeDev pathMode = pathMode(fs.ModeDevice) + modeCharDev pathMode = pathMode(fs.ModeCharDevice) + modeIrregular pathMode = pathMode(fs.ModeIrregular) + modeAny pathMode = modeDir | modeSymlink | modePipe | modeSocket | modeDev | modeCharDev | modeIrregular +) + +// Times +const TimeAny pathTimeType = 0 +const ( + // TimeAccessed == atime + TimeAccessed pathTimeType = 1 << iota + // TimeCreated == "birth" time (*NOT* ctime! See TimeChanged) + TimeCreated + // TimeChanged == ctime + TimeChanged + // TimeModified == mtime + TimeModified +) diff --git a/paths/funcs.go b/paths/funcs.go index 3857cf9..fea1181 100644 --- a/paths/funcs.go +++ b/paths/funcs.go @@ -25,8 +25,15 @@ import ( "os" "os/user" "path/filepath" + `regexp` + `slices` "strings" + `time` + // "syscall" + + `github.com/djherbis/times` + `r00t2.io/goutils/bitmask` ) /* @@ -266,3 +273,172 @@ func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) return } + +/* + SearchPaths gets a file/directory path list based on the provided criteria. + + targetType defines what should be included in the path list. + 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 as a shortcut to match anything/all paths. + You can also use (io/)fs.ModeType itself to match anything/all paths.) + + 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 ptrn 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 would 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. + + olderThan (as mentioned above) will find paths *older* than age if true, otherwise *newer*. +*/ +func SearchFsPaths( + root string, + targetType fs.FileMode, + basePtrn, pathPtrn *regexp.Regexp, + age *time.Duration, ageType pathTimeType, olderThan bool, +) (foundPaths []string, err error) { + + var now time.Time = time.Now() + + if err = RealPath(&root); err != nil { + return + } + + if err = filepath.WalkDir( + root, + func(path string, d fs.DirEntry, inErr error) (outErr error) { + + var typeMode fs.FileMode + var fi fs.FileInfo + var tspec times.Timespec + var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(targetType)) + + if inErr != nil { + outErr = inErr + return + } + + // 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, outErr = times.Stat(path); outErr != nil { + return + } + if !filterTimes(tspec, age, &ageType, olderThan, &now) { + return + } + } + + // fs object type (file, dir, etc.) + if targetType != 0 && uint(targetType) != uint(modeAny) { + if fi, outErr = d.Info(); outErr != nil { + return + } + typeMode = fi.Mode().Type() + if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) { + return + } + } + + // All filters passed at this point. + foundPaths = append(foundPaths, path) + + return + }, + ); err != nil { + return + } + + // And sort them. + slices.Sort(foundPaths) + + return +} + +/* + filterTimes checks a times.Timespec of a file using: + * an age specified by the caller + * an ageType bitmask for types of times to compare + * an olderThan bool (if false, the file must be younger than) + * an optional "now" timestamp for the age derivation. +*/ +func filterTimes(tspec times.Timespec, age *time.Duration, ageType *pathTimeType, olderThan bool, now *time.Time) (include bool) { + + var curAge time.Duration + var mask *bitmask.MaskBit + var tfunc func(t *time.Duration) (match bool) = func(t *time.Duration) (match bool) { + if olderThan { + match = *t > *age + } else { + match = *t < *age + } + return + } + + if tspec == nil || age == nil || ageType == nil { + return + } + + mask = ageType.Mask() + + if now == nil { + now = new(time.Time) + *now = time.Now() + } + + // ATIME + if mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeAccessed)) { + curAge = now.Sub(tspec.AccessTime()) + if include = tfunc(&curAge); include { + return + } + } + // MTIME + if mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeModified)) { + curAge = now.Sub(tspec.ModTime()) + if include = tfunc(&curAge); include { + return + } + } + // CTIME (if supported) + if tspec.HasChangeTime() && (mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeChanged))) { + curAge = now.Sub(tspec.ChangeTime()) + if include = tfunc(&curAge); include { + return + } + } + // BTIME (if supported) + if tspec.HasBirthTime() && (mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeCreated))) { + curAge = now.Sub(tspec.BirthTime()) + if include = tfunc(&curAge); include { + return + } + } + + return +} diff --git a/paths/funcs_pathtimetype.go b/paths/funcs_pathtimetype.go new file mode 100644 index 0000000..08205b9 --- /dev/null +++ b/paths/funcs_pathtimetype.go @@ -0,0 +1,13 @@ +package paths + +import ( + `r00t2.io/goutils/bitmask` +) + +// Mask returns a bitmask.MaskBit from a pathTimeType. +func (p *pathTimeType) Mask() (mask *bitmask.MaskBit) { + + mask = bitmask.NewMaskBitExplicit(uint(*p)) + + return +} diff --git a/paths/types.go b/paths/types.go new file mode 100644 index 0000000..8e6dc58 --- /dev/null +++ b/paths/types.go @@ -0,0 +1,9 @@ +package paths + +import ( + `r00t2.io/goutils/bitmask` +) + +type pathMode bitmask.MaskBit + +type pathTimeType bitmask.MaskBit