Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c6efc2d83c | ||
![]() |
eefe02afaf | ||
![]() |
b82f0c02ed | ||
![]() |
903dd00c81 | ||
![]() |
70a88ca8b4 | ||
![]() |
9dbc3a00fe |
3
.gitignore
vendored
3
.gitignore
vendored
@ -29,6 +29,9 @@
|
|||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
# Test file
|
||||||
|
fsutils/testfile
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
1
auger/TODO
Normal file
1
auger/TODO
Normal file
@ -0,0 +1 @@
|
|||||||
|
This module is still under work.
|
@ -7,3 +7,35 @@ const (
|
|||||||
augInclTfm string = "incl" // The transformer keyword for Augeas includes.
|
augInclTfm string = "incl" // The transformer keyword for Augeas includes.
|
||||||
augAppendSuffix string = "[last()+1]"
|
augAppendSuffix string = "[last()+1]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dstPtrTrue bool = true
|
||||||
|
dstPtrFalse bool = false
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// PtrTrue and PtrFalse are convenience references for constructing an AugFlags if needed. It is recommended you do not change these values if you do not like being confused.
|
||||||
|
PtrTrue *bool = &dstPtrTrue
|
||||||
|
PtrFalse *bool = &dstPtrFalse
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
IncludeOptNone is the default include recursion option for Aug.RecursiveInclude.
|
||||||
|
* No special behavior is defined
|
||||||
|
* All include directives are assumed to refer:
|
||||||
|
* Explicitly/exclusively to file paths
|
||||||
|
* That must exist
|
||||||
|
*/
|
||||||
|
const IncludeOptNone includeOpt = 0
|
||||||
|
const (
|
||||||
|
// IncludeOptNoExist specifies that inclusions are allowed to not exist, otherwise an error will be raised while attempting to parse them.
|
||||||
|
IncludeOptNoExist includeOpt = 1 << iota
|
||||||
|
// IncludeOptGlobbing indicates that the inclusion system supports globbing (as supported by (github.com/gobwas/glob).Match).
|
||||||
|
IncludeOptGlobbing
|
||||||
|
// IncludeOptRegex indicates that the inclusion system supports matching by regex (as supported by regexp).
|
||||||
|
IncludeOptRegex
|
||||||
|
// IncludeOptDirs indicates that the inclusion system supports matching by directory.
|
||||||
|
IncludeOptDirs
|
||||||
|
// IncludeOptDirsRecursive indicates that the inclusion system also recurses into subdirectories of matched directories. Only used if IncludeOptDirs is also set.
|
||||||
|
IncludeOptDirsRecursive
|
||||||
|
)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
`strings`
|
`strings`
|
||||||
|
|
||||||
`honnef.co/go/augeas`
|
`honnef.co/go/augeas`
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -41,12 +42,17 @@ func NewAugerFromAugeas(orig augeas.Augeas) (aug *Aug) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
AugpathToFspath returns the filesystem path from an Augeas path.
|
AugpathToFspath returns the filesystem path (i.e. an existing file) from an Augeas path.
|
||||||
|
|
||||||
It is *required* and expected that the Augeas standard /files prefix be removed first;
|
It is *required* and expected that the Augeas standard /files prefix be removed first;
|
||||||
if not, it is assumed to be part of the filesystem path.
|
if not, it is assumed to be part of the filesystem path.
|
||||||
|
|
||||||
If a valid path cannot be determined, fsPath will be empty.
|
If a valid path cannot be determined, fsPath will be empty.
|
||||||
|
|
||||||
|
To be clear, a file must exist for fsPath to not be empty;
|
||||||
|
the way AugpathToFsPath works is it recurses bottom-up a
|
||||||
|
given path and checks for the existence of a file,
|
||||||
|
continuing upwards if not found.
|
||||||
*/
|
*/
|
||||||
func AugpathToFspath(augPath string) (fsPath string, err error) {
|
func AugpathToFspath(augPath string) (fsPath string, err error) {
|
||||||
|
|
||||||
@ -95,3 +101,11 @@ func dedupePaths(new, existing []string) (missing []string) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInclPaths applies path options to inclusions.
|
||||||
|
func getInclPaths(pathSpec string, inclFlags *bitmask.MaskBit) (fpaths []string, err error) {
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
`github.com/davecgh/go-spew/spew`
|
`github.com/davecgh/go-spew/spew`
|
||||||
`github.com/google/shlex`
|
`github.com/google/shlex`
|
||||||
`honnef.co/go/augeas`
|
`honnef.co/go/augeas`
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
`r00t2.io/sysutils/paths`
|
`r00t2.io/sysutils/paths`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -146,10 +147,21 @@ breakCmd:
|
|||||||
An error will be returned if augLens is a nonexistent or not-loaded Augeas lens module.
|
An error will be returned if augLens is a nonexistent or not-loaded Augeas lens module.
|
||||||
|
|
||||||
Depending on how many files there are and whether globs vs. explicit filepaths are included, this may take a while.
|
Depending on how many files there are and whether globs vs. explicit filepaths are included, this may take a while.
|
||||||
*/
|
|
||||||
func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string) (err error) {
|
|
||||||
|
|
||||||
if err = a.addIncl(includeDirective, augLens, fsRoot, nil); err != nil {
|
optFlags may be nil, multiple includeOpt (see the IncludeOpt* constants) as variadic parameters/expanded slice,
|
||||||
|
bitwise-OR'd together, or multiple non-OR'd and OR'd together (all will be combined to a single value).
|
||||||
|
*/
|
||||||
|
func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string, optFlags ...includeOpt) (err error) {
|
||||||
|
|
||||||
|
var flags *bitmask.MaskBit = bitmask.NewMaskBit()
|
||||||
|
|
||||||
|
if optFlags != nil && len(optFlags) > 0 {
|
||||||
|
for _, f := range optFlags {
|
||||||
|
flags.AddFlag(f.toMb())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = a.addIncl(includeDirective, augLens, fsRoot, nil, flags); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,14 +176,16 @@ func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string) (err er
|
|||||||
newInclPaths are new filesystem paths/Augeas-compatible glob patterns to load into the filetree and recurse into.
|
newInclPaths are new filesystem paths/Augeas-compatible glob patterns to load into the filetree and recurse into.
|
||||||
They may be nil, especially if the first run.
|
They may be nil, especially if the first run.
|
||||||
*/
|
*/
|
||||||
func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPaths []string) (err error) {
|
func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPaths []string, inclFlags *bitmask.MaskBit) (err error) {
|
||||||
|
|
||||||
var matches []string // Passed around set of Augeas matches.
|
var matches []string // Passed around set of Augeas matches.
|
||||||
|
var exists bool // Used to indicate if the include path exists.
|
||||||
var includes []string // Filepath(s)/glob(s) from fetching includeDirective in lensInclPath. These are internal to the application but are recursed.
|
var includes []string // Filepath(s)/glob(s) from fetching includeDirective in lensInclPath. These are internal to the application but are recursed.
|
||||||
var lensInclPath string // The path of the included paths in the tree. These are internal to Augeas, not the application.
|
var lensInclPath string // The path of the included paths in the tree. These are internal to Augeas, not the application.
|
||||||
var appendPath string // The path for new Augeas includes.
|
var appendPath string // The path for new Augeas includes.
|
||||||
var match []string // A placeholder for iterating when populating includes.
|
var match []string // A placeholder for iterating when populating includes.
|
||||||
var fpath string // A placeholder for finding the path of a conf file that contains an includeDirective.
|
var fpath string // A placeholder for finding the path of a conf file that contains an includeDirective.
|
||||||
|
var normalizedIncludes []string // A temporary slice to hold normalization operations and other dynamic building.
|
||||||
var lensPath string = fmt.Sprintf(augLensTpl, augLens) // The path of the lens (augLens) itself.
|
var lensPath string = fmt.Sprintf(augLensTpl, augLens) // The path of the lens (augLens) itself.
|
||||||
var augErr *augeas.Error = new(augeas.Error) // We use this to skip "nonexistent" lens.
|
var augErr *augeas.Error = new(augeas.Error) // We use this to skip "nonexistent" lens.
|
||||||
|
|
||||||
@ -193,7 +207,7 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa
|
|||||||
|
|
||||||
// First canonize paths.
|
// First canonize paths.
|
||||||
if newInclPaths != nil && len(newInclPaths) > 0 {
|
if newInclPaths != nil && len(newInclPaths) > 0 {
|
||||||
// Existing includes. We don't return on an empty lensInclPath because
|
// Existing includes. We don't return on an empty lensInclPath.
|
||||||
if matches, err = a.aug.Match(lensInclPath); err != nil {
|
if matches, err = a.aug.Match(lensInclPath); err != nil {
|
||||||
if errors.As(err, augErr) && augErr.Code == augeas.NoMatch {
|
if errors.As(err, augErr) && augErr.Code == augeas.NoMatch {
|
||||||
err = nil
|
err = nil
|
||||||
@ -221,6 +235,17 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa
|
|||||||
// We don't want to bother adding multiple incl's for the same path(s); it can negatively affect Augeas loads.
|
// We don't want to bother adding multiple incl's for the same path(s); it can negatively affect Augeas loads.
|
||||||
newInclPaths = dedupePaths(newInclPaths, matches)
|
newInclPaths = dedupePaths(newInclPaths, matches)
|
||||||
|
|
||||||
|
// And then apply things like recursion, globbing, etc.
|
||||||
|
normalizedIncludes = make([]string, 0, len(newInclPaths))
|
||||||
|
if inclFlags.HasFlag(IncludeOptGlobbing.toMb()) {
|
||||||
|
// TODO
|
||||||
|
/*
|
||||||
|
if strings.Contains(newInclPaths[idx], "*") {
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
// Add the new path(s) as Augeas include entries.
|
// Add the new path(s) as Augeas include entries.
|
||||||
if newInclPaths != nil {
|
if newInclPaths != nil {
|
||||||
for _, fsPath := range newInclPaths {
|
for _, fsPath := range newInclPaths {
|
||||||
@ -285,10 +310,13 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matches != nil && len(matches) != 0 {
|
if matches != nil && len(matches) != 0 {
|
||||||
if err = a.addIncl(includeDirective, augLens, fsRoot, matches); err != nil {
|
if err = a.addIncl(includeDirective, augLens, fsRoot, matches, inclFlags); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
_, _ = exists, normalizedIncludes
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
13
auger/funcs_includeopt.go
Normal file
13
auger/funcs_includeopt.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package auger
|
||||||
|
|
||||||
|
import (
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
|
)
|
||||||
|
|
||||||
|
// toMb returns a bitmask.MaskBit of this includeOpt.
|
||||||
|
func (i includeOpt) toMb() (mb bitmask.MaskBit) {
|
||||||
|
|
||||||
|
mb = bitmask.MaskBit(i)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -22,3 +22,18 @@ func TestNewAuger(t *testing.T) {
|
|||||||
|
|
||||||
_ = aug
|
_ = aug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRecursiveInclude(t *testing.T) {
|
||||||
|
|
||||||
|
var aug *Aug
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if aug, err = NewAuger("/", "", &AugFlags{DryRun: PtrTrue}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This requires Nginx to be installed and with a particularly complex nested include system.
|
||||||
|
if err = aug.RecursiveInclude("Nginx", "include", "/etc/nginx"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,8 +2,11 @@ package auger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
`honnef.co/go/augeas`
|
`honnef.co/go/augeas`
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type includeOpt bitmask.MaskBit
|
||||||
|
|
||||||
// Aug is a wrapper around (honnef.co/go/)augeas.Augeas. Remember to call Aug.Close().
|
// Aug is a wrapper around (honnef.co/go/)augeas.Augeas. Remember to call Aug.Close().
|
||||||
type Aug struct {
|
type Aug struct {
|
||||||
aug augeas.Augeas
|
aug augeas.Augeas
|
||||||
|
@ -3,3 +3,4 @@
|
|||||||
|
|
||||||
It is now its own module: r00t2.io/cryptparse
|
It is now its own module: r00t2.io/cryptparse
|
||||||
*/
|
*/
|
||||||
|
package cryptparse
|
||||||
|
@ -159,7 +159,7 @@ func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
|
|||||||
var procPath string
|
var procPath string
|
||||||
var exists bool
|
var exists bool
|
||||||
|
|
||||||
envMap = make(map[string]string, 0)
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
procPath = fmt.Sprintf("/proc/%v/environ", pid)
|
procPath = fmt.Sprintf("/proc/%v/environ", pid)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ func envListToMap(envs []string) (envMap map[string]string) {
|
|||||||
var kv []string
|
var kv []string
|
||||||
var k, v string
|
var k, v string
|
||||||
|
|
||||||
envMap = make(map[string]string, 0)
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
for _, ev := range envs {
|
for _, ev := range envs {
|
||||||
kv = strings.SplitN(ev, "=", 2)
|
kv = strings.SplitN(ev, "=", 2)
|
||||||
|
3
fsutils/TODO
Normal file
3
fsutils/TODO
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- XATTRS
|
||||||
|
(see FS_XFLAG_* in fs.h, FS_IOC_FSGETXATTR/FS_IOC_FSSETXATTR)
|
||||||
|
- fs label, UUID? (fs.h)
|
@ -1,101 +1,36 @@
|
|||||||
package fsutils
|
package fsutils
|
||||||
|
|
||||||
import (
|
|
||||||
`github.com/g0rbe/go-chattr`
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h
|
|
||||||
const (
|
|
||||||
SecureDelete uint32 = chattr.FS_SECRM_FL // Secure deletion
|
|
||||||
UnDelete = chattr.FS_UNRM_FL // Undelete
|
|
||||||
CompressFile = chattr.FS_COMPR_FL // Compress file
|
|
||||||
SyncUpdatechattr = chattr.FS_SYNC_FL // Synchronous updates
|
|
||||||
Immutable = chattr.FS_IMMUTABLE_FL // Immutable file
|
|
||||||
AppendOnly = chattr.FS_APPEND_FL // Writes to file may only append
|
|
||||||
NoDumpFile = chattr.FS_NODUMP_FL // Do not dump file
|
|
||||||
NoUpdateAtime = chattr.FS_NOATIME_FL // Do not update atime
|
|
||||||
IsDirty = chattr.FS_DIRTY_FL // Nobody knows what this does, lol.
|
|
||||||
CompressedClusters = chattr.FS_COMPRBLK_FL // One or more compressed clusters
|
|
||||||
NoCompress = chattr.FS_NOCOMP_FL // Don't compress
|
|
||||||
EncFile = chattr.FS_ENCRYPT_FL // Encrypted file
|
|
||||||
BtreeFmt = chattr.FS_BTREE_FL // Btree format dir
|
|
||||||
HashIdxDir = chattr.FS_INDEX_FL // Hash-indexed directory
|
|
||||||
AfsDir = chattr.FS_IMAGIC_FL // AFS directory
|
|
||||||
ReservedExt3 = chattr.FS_JOURNAL_DATA_FL // Reserved for ext3
|
|
||||||
NoMergeTail = chattr.FS_NOTAIL_FL // File tail should not be merged
|
|
||||||
DirSync = chattr.FS_DIRSYNC_FL // dirsync behaviour (directories only)
|
|
||||||
DirTop = chattr.FS_TOPDIR_FL // Top of directory hierarchies
|
|
||||||
ReservedExt4a = chattr.FS_HUGE_FILE_FL // Reserved for ext4
|
|
||||||
Extents = chattr.FS_EXTENT_FL // Extents
|
|
||||||
LargeEaInode = chattr.FS_EA_INODE_FL // Inode used for large EA
|
|
||||||
ReservedExt4b = chattr.FS_EOFBLOCKS_FL // Reserved for ext4
|
|
||||||
NoCOWFile = chattr.FS_NOCOW_FL // Do not cow file
|
|
||||||
ReservedExt4c = chattr.FS_INLINE_DATA_FL // Reserved for ext4
|
|
||||||
UseParentProjId = chattr.FS_PROJINHERIT_FL // Create with parents projid
|
|
||||||
ReservedExt2 = chattr.FS_RESERVED_FL // Reserved for ext2 lib
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// AttrNameValueMap contains a mapping of attribute names (as designated by this package) to their flag values.
|
|
||||||
AttrNameValueMap map[string]uint32 = map[string]uint32{
|
|
||||||
"SecureDelete": SecureDelete,
|
|
||||||
"UnDelete": UnDelete,
|
|
||||||
"CompressFile": CompressFile,
|
|
||||||
"SyncUpdatechattr": SyncUpdatechattr,
|
|
||||||
"Immutable": Immutable,
|
|
||||||
"AppendOnly": AppendOnly,
|
|
||||||
"NoDumpFile": NoDumpFile,
|
|
||||||
"NoUpdateAtime": NoUpdateAtime,
|
|
||||||
"IsDirty": IsDirty,
|
|
||||||
"CompressedClusters": CompressedClusters,
|
|
||||||
"NoCompress": NoCompress,
|
|
||||||
"EncFile": EncFile,
|
|
||||||
"BtreeFmt": BtreeFmt,
|
|
||||||
"HashIdxDir": HashIdxDir,
|
|
||||||
"AfsDir": AfsDir,
|
|
||||||
"ReservedExt3": ReservedExt3,
|
|
||||||
"NoMergeTail": NoMergeTail,
|
|
||||||
"DirSync": DirSync,
|
|
||||||
"DirTop": DirTop,
|
|
||||||
"ReservedExt4a": ReservedExt4a,
|
|
||||||
"Extents": Extents,
|
|
||||||
"LargeEaInode": LargeEaInode,
|
|
||||||
"ReservedExt4b": ReservedExt4b,
|
|
||||||
"NoCOWFile": NoCOWFile,
|
|
||||||
"ReservedExt4c": ReservedExt4c,
|
|
||||||
"UseParentProjId": UseParentProjId,
|
|
||||||
"ReservedExt2": ReservedExt2,
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
AttrValueNameMap contains a mapping of attribute flags to their names (as designated by this package).
|
linuxFsAttrsListOrder defines the order the attributes are printed in per e2fsprogs.
|
||||||
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so be forewarned.
|
|
||||||
|
See flags_name at https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c for order.
|
||||||
|
Up to date as of e2fsprogs v1.47.1, Linux 6.12-rc7.
|
||||||
|
|
||||||
|
The below are the struct field names for easier reflection.
|
||||||
*/
|
*/
|
||||||
AttrValueNameMap map[uint32]string = map[uint32]string{
|
linuxFsAttrsListOrder []string = []string{
|
||||||
SecureDelete: "SecureDelete",
|
"SecureDelete",
|
||||||
UnDelete: "UnDelete",
|
"UnDelete",
|
||||||
CompressFile: "CompressFile",
|
"SyncUpdate",
|
||||||
SyncUpdatechattr: "SyncUpdatechattr",
|
"DirSync",
|
||||||
Immutable: "Immutable",
|
"Immutable",
|
||||||
AppendOnly: "AppendOnly",
|
"AppendOnly",
|
||||||
NoDumpFile: "NoDumpFile",
|
"NoDumpFile",
|
||||||
NoUpdateAtime: "NoUpdateAtime",
|
"NoUpdateAtime",
|
||||||
IsDirty: "IsDirty",
|
"CompressFile",
|
||||||
CompressedClusters: "CompressedClusters",
|
"EncFile",
|
||||||
NoCompress: "NoCompress",
|
"ReservedExt3",
|
||||||
EncFile: "EncFile",
|
"HashIdxDir",
|
||||||
BtreeFmt: "BtreeFmt|HashIdxDir", // Well THIS is silly and seems like an oversight. Both FS_BTREE_FL and FS_INDEX_FL have the same flag. Confirmed in kernel source.
|
"NoMergeTail",
|
||||||
AfsDir: "AfsDir",
|
"DirTop",
|
||||||
ReservedExt3: "ReservedExt3",
|
"Extents",
|
||||||
NoMergeTail: "NoMergeTail",
|
"NoCOWFile",
|
||||||
DirSync: "DirSync",
|
"DAX",
|
||||||
DirTop: "DirTop",
|
"CaseInsensitive",
|
||||||
ReservedExt4a: "ReservedExt4a",
|
"ReservedExt4c",
|
||||||
Extents: "Extents",
|
"UseParentProjId",
|
||||||
LargeEaInode: "LargeEaInode",
|
"VerityProtected",
|
||||||
ReservedExt4b: "ReservedExt4b",
|
"NoCompress",
|
||||||
NoCOWFile: "NoCOWFile",
|
|
||||||
ReservedExt4c: "ReservedExt4c",
|
|
||||||
UseParentProjId: "UseParentProjId",
|
|
||||||
ReservedExt2: "ReservedExt2",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
127
fsutils/consts_lin.go
Normal file
127
fsutils/consts_lin.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package fsutils
|
||||||
|
|
||||||
|
/*
|
||||||
|
https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h "Inode flags (FS_IOC_GETFLAGS / FS_IOC_SETFLAGS)"
|
||||||
|
Up to date as of Linux 6.12-rc7.
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
SecureDelete fsAttr = 1 << iota // Secure deletion
|
||||||
|
UnDelete // Undelete
|
||||||
|
CompressFile // Compress file
|
||||||
|
SyncUpdate // Synchronous updates
|
||||||
|
Immutable // Immutable file
|
||||||
|
AppendOnly // Writes to file may only append
|
||||||
|
NoDumpFile // Do not dump file
|
||||||
|
NoUpdateAtime // Do not update atime
|
||||||
|
IsDirty // Nobody knows what this does, lol.
|
||||||
|
CompressedClusters // One or more compressed clusters
|
||||||
|
NoCompress // Don't compress
|
||||||
|
EncFile // Encrypted file
|
||||||
|
BtreeFmt // Btree format dir
|
||||||
|
AfsDir // AFS directory
|
||||||
|
ReservedExt3 // Reserved for ext3
|
||||||
|
NoMergeTail // File tail should not be merged
|
||||||
|
DirSync // dirsync behaviour (directories only)
|
||||||
|
DirTop // Top of directory hierarchies
|
||||||
|
ReservedExt4a // Reserved for ext4
|
||||||
|
Extents // Extents
|
||||||
|
VerityProtected // Verity-protected inode
|
||||||
|
LargeEaInode // Inode used for large EA
|
||||||
|
ReservedExt4b // Reserved for ext4
|
||||||
|
NoCOWFile // Do not cow file
|
||||||
|
_ // (Unused)
|
||||||
|
DAX // Inode is DAX
|
||||||
|
_ // (Unused)
|
||||||
|
_ // (Unused)
|
||||||
|
ReservedExt4c // Reserved for ext4
|
||||||
|
UseParentProjId // Create with parents projid
|
||||||
|
CaseInsensitive // Folder is case-insensitive
|
||||||
|
ReservedExt2 // Reserved for ext2 lib
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are the same value. For some reason.
|
||||||
|
const (
|
||||||
|
HashIdxDir fsAttr = BtreeFmt // Hash-indexed directory
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// AttrNameValueMap contains a mapping of attribute names (as designated by this package) to their flag values.
|
||||||
|
AttrNameValueMap map[string]fsAttr = map[string]fsAttr{
|
||||||
|
"SecureDelete": SecureDelete,
|
||||||
|
"UnDelete": UnDelete,
|
||||||
|
"CompressFile": CompressFile,
|
||||||
|
"SyncUpdate": SyncUpdate,
|
||||||
|
"Immutable": Immutable,
|
||||||
|
"AppendOnly": AppendOnly,
|
||||||
|
"NoDumpFile": NoDumpFile,
|
||||||
|
"NoUpdateAtime": NoUpdateAtime,
|
||||||
|
"IsDirty": IsDirty,
|
||||||
|
"CompressedClusters": CompressedClusters,
|
||||||
|
"NoCompress": NoCompress,
|
||||||
|
"EncFile": EncFile,
|
||||||
|
"BtreeFmt": BtreeFmt,
|
||||||
|
"HashIdxDir": HashIdxDir,
|
||||||
|
"AfsDir": AfsDir,
|
||||||
|
"ReservedExt3": ReservedExt3,
|
||||||
|
"NoMergeTail": NoMergeTail,
|
||||||
|
"DirSync": DirSync,
|
||||||
|
"DirTop": DirTop,
|
||||||
|
"ReservedExt4a": ReservedExt4a,
|
||||||
|
"Extents": Extents,
|
||||||
|
"VerityProtected": VerityProtected,
|
||||||
|
"LargeEaInode": LargeEaInode,
|
||||||
|
"ReservedExt4b": ReservedExt4b,
|
||||||
|
"NoCOWFile": NoCOWFile,
|
||||||
|
"DAX": DAX,
|
||||||
|
"ReservedExt4c": ReservedExt4c,
|
||||||
|
"UseParentProjId": UseParentProjId,
|
||||||
|
"CaseInsensitive": CaseInsensitive,
|
||||||
|
"ReservedExt2": ReservedExt2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
AttrValueNameMap contains a mapping of attribute flags to their names (as designated by this package).
|
||||||
|
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so their string value is unpredictable.
|
||||||
|
*/
|
||||||
|
AttrValueNameMap map[fsAttr]string = invertMap(AttrNameValueMap)
|
||||||
|
|
||||||
|
// KernelNameValueMap allows lookups using the symbol name as used in the Linux kernel source.
|
||||||
|
KernelNameValueMap map[string]fsAttr = map[string]fsAttr{
|
||||||
|
"FS_SECRM_FL": SecureDelete,
|
||||||
|
"FS_UNRM_FL": UnDelete,
|
||||||
|
"FS_COMPR_FL": CompressFile,
|
||||||
|
"FS_SYNC_FL": SyncUpdate,
|
||||||
|
"FS_IMMUTABLE_FL": Immutable,
|
||||||
|
"FS_APPEND_FL": AppendOnly,
|
||||||
|
"FS_NODUMP_FL": NoDumpFile,
|
||||||
|
"FS_NOATIME_FL": NoUpdateAtime,
|
||||||
|
"FS_DIRTY_FL": IsDirty,
|
||||||
|
"FS_COMPRBLK_FL": CompressedClusters,
|
||||||
|
"FS_NOCOMP_FL": NoCompress,
|
||||||
|
"FS_ENCRYPT_FL": EncFile,
|
||||||
|
"FS_BTREE_FL": BtreeFmt,
|
||||||
|
"FS_INDEX_FL": HashIdxDir,
|
||||||
|
"FS_IMAGIC_FL": AfsDir,
|
||||||
|
"FS_JOURNAL_DATA_FL": ReservedExt3,
|
||||||
|
"FS_NOTAIL_FL": NoMergeTail,
|
||||||
|
"FS_DIRSYNC_FL": DirSync,
|
||||||
|
"FS_TOPDIR_FL": DirTop,
|
||||||
|
"FS_HUGE_FILE_FL": ReservedExt4a,
|
||||||
|
"FS_EXTENT_FL": Extents,
|
||||||
|
"FS_VERITY_FL": VerityProtected,
|
||||||
|
"FS_EA_INODE_FL": LargeEaInode,
|
||||||
|
"FS_EOFBLOCKS_FL": ReservedExt4b,
|
||||||
|
"FS_NOCOW_FL": NoCOWFile,
|
||||||
|
"FS_DAX_FL": DAX,
|
||||||
|
"FS_INLINE_DATA_FL": ReservedExt4c,
|
||||||
|
"FS_PROJINHERIT_FL": UseParentProjId,
|
||||||
|
"FS_CASEFOLD_FL": CaseInsensitive,
|
||||||
|
"FS_RESERVED_FL": ReservedExt2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
KernelValueNameMap contains a mapping of attribute flags to their kernel source symbol name.
|
||||||
|
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so their string value is unpredictable.
|
||||||
|
*/
|
||||||
|
KernelValueNameMap map[fsAttr]string = invertMap(KernelNameValueMap)
|
||||||
|
)
|
7
fsutils/doc.go
Normal file
7
fsutils/doc.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
fsutils is a collection of filesystem-related functions, types, etc.
|
||||||
|
|
||||||
|
Currently it's only a (fixed/actually working) reimplementation of github.com/g0rbe/go-chattr.
|
||||||
|
(Note to library maintainers, if someone reports an integer overflow and even tells you how to fix it, you should probably fix it.)
|
||||||
|
*/
|
||||||
|
package fsutils
|
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
|
||||||
|
)
|
@ -1,44 +1,16 @@
|
|||||||
package fsutils
|
package fsutils
|
||||||
|
|
||||||
import (
|
// invertMap returns some handy consts remapping for easier lookups.
|
||||||
`os`
|
func invertMap(origMap map[string]fsAttr) (newMap map[fsAttr]string) {
|
||||||
`reflect`
|
|
||||||
|
|
||||||
`github.com/g0rbe/go-chattr`
|
if origMap == nil {
|
||||||
`r00t2.io/sysutils/paths`
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetAttrs(path string) (attrs *FsAttrs, err error) {
|
|
||||||
|
|
||||||
var f *os.File
|
|
||||||
var evalAttrs FsAttrs
|
|
||||||
var attrVal uint32
|
|
||||||
var reflectVal reflect.Value
|
|
||||||
var field reflect.Value
|
|
||||||
var myPath string = path
|
|
||||||
|
|
||||||
if err = paths.RealPath(&myPath); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
newMap = make(map[fsAttr]string)
|
||||||
|
|
||||||
if f, err = os.Open(myPath); err != nil {
|
for k, v := range origMap {
|
||||||
return
|
newMap[v] = k
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
reflectVal = reflect.ValueOf(&evalAttrs).Elem()
|
|
||||||
|
|
||||||
if attrVal, err = chattr.GetAttrs(f); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for attrNm, attrInt := range AttrNameValueMap {
|
|
||||||
field = reflectVal.FieldByName(attrNm)
|
|
||||||
field.SetBool((attrVal & attrInt) != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = new(FsAttrs)
|
|
||||||
*attrs = evalAttrs
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,96 @@
|
|||||||
package fsutils
|
package fsutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`os`
|
|
||||||
`reflect`
|
`reflect`
|
||||||
|
`strings`
|
||||||
`github.com/g0rbe/go-chattr`
|
|
||||||
`r00t2.io/sysutils/paths`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *FsAttrs) Apply(path string) (err error) {
|
/*
|
||||||
|
String returns a string representation (comparable to lsattr(1)) of an FsAttrs.
|
||||||
|
|
||||||
var file *os.File
|
Not all flags are represented, as this aims for compatibility with e2fsprogs/lsattr output.
|
||||||
var reflectVal reflect.Value
|
*/
|
||||||
|
func (f *FsAttrs) String() (s string) {
|
||||||
|
|
||||||
|
// Flags have their short name printed if set, otherwise a '-' placeholder is used.
|
||||||
|
// https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c
|
||||||
|
|
||||||
|
var refType reflect.Type
|
||||||
|
var refVal reflect.Value
|
||||||
|
var refField reflect.StructField
|
||||||
var fieldVal reflect.Value
|
var fieldVal reflect.Value
|
||||||
|
var tagVal string
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
var myPath string = path
|
if f == nil {
|
||||||
|
s = strings.Repeat("-", len(linuxFsAttrsListOrder))
|
||||||
if err = paths.RealPath(&myPath); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if file, err = os.Open(myPath); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
reflectVal = reflect.ValueOf(*f)
|
refVal = reflect.ValueOf(*f)
|
||||||
|
refType = refVal.Type()
|
||||||
for attrNm, attrVal := range AttrNameValueMap {
|
for _, fn := range linuxFsAttrsListOrder {
|
||||||
fieldVal = reflectVal.FieldByName(attrNm)
|
refField, _ = refType.FieldByName(fn)
|
||||||
|
tagVal = refField.Tag.Get("fsAttrShort")
|
||||||
|
if tagVal == "" || tagVal == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldVal = refVal.FieldByName(fn)
|
||||||
if fieldVal.Bool() {
|
if fieldVal.Bool() {
|
||||||
if err = chattr.SetAttr(file, attrVal); err != nil {
|
sb.WriteString(tagVal)
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if err = chattr.UnsetAttr(file, attrVal); err != nil {
|
sb.WriteString("-")
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringLong returns a more extensive/"human-friendly" representation (comparable to lsattr(1) wiih -l) of an Fsattrs.
|
||||||
|
|
||||||
|
Not all flags are represented, as this aims for compatibility with e2fsprogs/lsattr output.
|
||||||
|
*/
|
||||||
|
func (f *FsAttrs) StringLong() (s string) {
|
||||||
|
|
||||||
|
// The long names are separated via a commma then a space.
|
||||||
|
// If no attrs are set, the string "---" is used.
|
||||||
|
// https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c
|
||||||
|
|
||||||
|
var refType reflect.Type
|
||||||
|
var refVal reflect.Value
|
||||||
|
var refField reflect.StructField
|
||||||
|
var fieldVal reflect.Value
|
||||||
|
var tagVal string
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
s = strings.Repeat("-", 3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refVal = reflect.ValueOf(*f)
|
||||||
|
refType = refVal.Type()
|
||||||
|
for _, fn := range linuxFsAttrsListOrder {
|
||||||
|
refField, _ = refType.FieldByName(fn)
|
||||||
|
tagVal = refField.Tag.Get("fsAttrLong")
|
||||||
|
if tagVal == "" || tagVal == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldVal = refVal.FieldByName(fn)
|
||||||
|
if fieldVal.Bool() {
|
||||||
|
out = append(out, tagVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == nil || len(out) == 0 {
|
||||||
|
s = strings.Repeat("-", 3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.Join(out, ", ")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
46
fsutils/funcs_fsattrs_linux.go
Normal file
46
fsutils/funcs_fsattrs_linux.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package fsutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
`os`
|
||||||
|
`reflect`
|
||||||
|
|
||||||
|
`r00t2.io/sysutils/paths`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *FsAttrs) Apply(path string) (err error) {
|
||||||
|
|
||||||
|
var file *os.File
|
||||||
|
var reflectVal reflect.Value
|
||||||
|
var fieldVal reflect.Value
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = paths.RealPath(&path); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if file, err = os.Open(path); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reflectVal = reflect.ValueOf(*f)
|
||||||
|
|
||||||
|
for attrNm, attrVal := range AttrNameValueMap {
|
||||||
|
fieldVal = reflectVal.FieldByName(attrNm)
|
||||||
|
if fieldVal.Bool() {
|
||||||
|
if err = setAttrs(file, attrVal); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = unsetAttrs(file, attrVal); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
134
fsutils/funcs_linux.go
Normal file
134
fsutils/funcs_linux.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package fsutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
`os`
|
||||||
|
`reflect`
|
||||||
|
`unsafe`
|
||||||
|
|
||||||
|
`golang.org/x/sys/unix`
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
|
`r00t2.io/sysutils/paths`
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAttrs(path string) (attrs *FsAttrs, err error) {
|
||||||
|
|
||||||
|
var f *os.File
|
||||||
|
var evalAttrs FsAttrs
|
||||||
|
var attrVal fsAttr
|
||||||
|
var attrValBit bitmask.MaskBit
|
||||||
|
var reflectVal reflect.Value
|
||||||
|
var field reflect.Value
|
||||||
|
var myPath string = path
|
||||||
|
|
||||||
|
if err = paths.RealPath(&myPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err = os.Open(myPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
reflectVal = reflect.ValueOf(&evalAttrs).Elem()
|
||||||
|
|
||||||
|
if attrVal, err = getAttrs(f); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
attrValBit = bitmask.MaskBit(attrVal)
|
||||||
|
|
||||||
|
for attrNm, attrInt := range AttrNameValueMap {
|
||||||
|
field = reflectVal.FieldByName(attrNm)
|
||||||
|
field.SetBool(attrValBit.HasFlag(bitmask.MaskBit(attrInt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = new(FsAttrs)
|
||||||
|
*attrs = evalAttrs
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAttrs is the unexported low-level syscall to get attributes.
|
||||||
|
func getAttrs(f *os.File) (attrVal fsAttr, err error) {
|
||||||
|
|
||||||
|
var u uint
|
||||||
|
var curFlags int
|
||||||
|
// var errNo syscall.Errno
|
||||||
|
|
||||||
|
/*
|
||||||
|
if _, _, errNo = unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.FS_IOC_GETFLAGS, uintptr(unsafe.Pointer(&curFlags))); errNo != 0 {
|
||||||
|
err = os.NewSyscallError("ioctl: FS_IOC_GETFLAGS", errNo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if curFlags, err = unix.IoctlGetInt(int(f.Fd()), unix.FS_IOC_GETFLAGS); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u = uint(curFlags)
|
||||||
|
|
||||||
|
attrVal = fsAttr(u)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAttrs is the unexported low-level syscall to set attributes. attrs may be OR'd.
|
||||||
|
func setAttrs(f *os.File, attrs fsAttr) (err error) {
|
||||||
|
|
||||||
|
var curAttrs fsAttr
|
||||||
|
var ab bitmask.MaskBit
|
||||||
|
var errNo unix.Errno
|
||||||
|
var val uint
|
||||||
|
|
||||||
|
if curAttrs, err = getAttrs(f); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ab = bitmask.MaskBit(curAttrs)
|
||||||
|
|
||||||
|
if ab.HasFlag(bitmask.MaskBit(attrs)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ab.AddFlag(bitmask.MaskBit(attrs))
|
||||||
|
|
||||||
|
val = ab.Value()
|
||||||
|
|
||||||
|
/*
|
||||||
|
if err = unix.IoctlSetInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, int(ab.Value())); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if _, _, errNo = unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.FS_IOC_SETFLAGS, uintptr(unsafe.Pointer(&val))); errNo != 0 {
|
||||||
|
err = os.NewSyscallError("ioctl: SYS_IOCTL", errNo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsetAttrs is the unexported low-level syscall to remove attributes. attrs may be OR'd.
|
||||||
|
func unsetAttrs(f *os.File, attrs fsAttr) (err error) {
|
||||||
|
|
||||||
|
var curAttrs fsAttr
|
||||||
|
var ab bitmask.MaskBit
|
||||||
|
|
||||||
|
if curAttrs, err = getAttrs(f); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ab = bitmask.MaskBit(curAttrs)
|
||||||
|
|
||||||
|
if !ab.HasFlag(bitmask.MaskBit(attrs)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ab.ClearFlag(bitmask.MaskBit(attrs))
|
||||||
|
|
||||||
|
/*
|
||||||
|
if err = unix.IoctlSetInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, int(ab.Value())); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
package fsutils
|
package fsutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -7,12 +9,13 @@ import (
|
|||||||
`os/user`
|
`os/user`
|
||||||
`testing`
|
`testing`
|
||||||
|
|
||||||
|
`github.com/davecgh/go-spew/spew`
|
||||||
`r00t2.io/sysutils/paths`
|
`r00t2.io/sysutils/paths`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testFilename string = "testfile"
|
testFilename string = "testfile"
|
||||||
testErrBadUser error = errors.New("test must be run as root, on Linux")
|
testErrBadUser error = errors.New("test must be run as root")
|
||||||
)
|
)
|
||||||
|
|
||||||
func testChkUser() (err error) {
|
func testChkUser() (err error) {
|
||||||
@ -36,12 +39,18 @@ func TestSetAttrs(t *testing.T) {
|
|||||||
if attrs, err = GetAttrs(testFilename); err != nil {
|
if attrs, err = GetAttrs(testFilename); err != nil {
|
||||||
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
|
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
|
||||||
}
|
}
|
||||||
t.Logf("Attrs for %v:\n%#v", testFilename, attrs)
|
t.Logf("Attrs for %v (before):\n%s", testFilename, spew.Sdump(attrs))
|
||||||
attrs.CompressFile = true
|
attrs.CompressFile = true
|
||||||
|
attrs.SyncUpdate = true
|
||||||
|
attrs.SecureDelete = true
|
||||||
if err = attrs.Apply(testFilename); err != nil {
|
if err = attrs.Apply(testFilename); err != nil {
|
||||||
t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err)
|
t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err)
|
||||||
}
|
}
|
||||||
t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs)
|
t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs)
|
||||||
|
if attrs, err = GetAttrs(testFilename); err != nil {
|
||||||
|
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
|
||||||
|
}
|
||||||
|
t.Logf("Attrs for %v (after):\n%s", testFilename, spew.Sdump(attrs))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(t *testing.M) {
|
func TestMain(t *testing.M) {
|
@ -1,32 +1,44 @@
|
|||||||
package fsutils
|
package fsutils
|
||||||
|
|
||||||
// FsAttrs is a convenience struct around github.com/g0rbe/go-chattr.
|
import (
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
|
)
|
||||||
|
|
||||||
|
type fsAttr bitmask.MaskBit
|
||||||
|
|
||||||
|
/*
|
||||||
|
FsAttrs is a struct representation of filesystem attributes on Linux.
|
||||||
|
Up to date as of Linux 6.12-rc7.
|
||||||
|
*/
|
||||||
type FsAttrs struct {
|
type FsAttrs struct {
|
||||||
SecureDelete bool
|
SecureDelete bool `fsAttrShort:"s" fsAttrLong:"Secure_Deletion" fsAttrKern:"FS_SECRM_FL" json:"secure_delete" toml:"SecureDelete" yaml:"Secure Delete" xml:"secureDelete,attr"`
|
||||||
UnDelete bool
|
UnDelete bool `fsAttrShort:"u" fsAttrLong:"Undelete" fsAttrKern:"FS_UNRM_FL" json:"undelete" toml:"Undelete" yaml:"Undelete" xml:"undelete,attr"`
|
||||||
CompressFile bool
|
CompressFile bool `fsAttrShort:"c" fsAttrLong:"Compression_Requested" fsAttrKern:"FS_COMPR_FL" json:"compress" toml:"Compress" yaml:"Compress" xml:"compress,attr"`
|
||||||
SyncUpdatechattr bool
|
SyncUpdate bool `fsAttrShort:"S" fsAttrLong:"Synchronous_Updates" fsAttrKern:"FS_SYNC_FL" json:"sync" toml:"SyncUpdate" yaml:"Synchronized Update" xml:"syncUpdate,attr"`
|
||||||
Immutable bool
|
Immutable bool `fsAttrShort:"i" fsAttrLong:"Immutable" fsAttrKern:"FS_IMMUTABLE_FL" json:"immutable" toml:"Immutable" yaml:"Immutable" xml:"immutable,attr"`
|
||||||
AppendOnly bool
|
AppendOnly bool `fsAttrShort:"a" fsAttrLong:"Append_Only" fsAttrKern:"FS_APPEND_FL" json:"append_only" toml:"AppendOnly" yaml:"Append Only" xml:"appendOnly,attr"`
|
||||||
NoDumpFile bool
|
NoDumpFile bool `fsAttrShort:"d" fsAttrLong:"No_Dump" fsAttrKern:"FS_NODUMP_FL" json:"no_dump" toml:"NoDump" yaml:"Disable Dumping" xml:"noDump,attr"`
|
||||||
NoUpdateAtime bool
|
NoUpdateAtime bool `fsAttrShort:"A" fsAttrLong:"No_Atime" fsAttrKern:"FS_NOATIME_FL" json:"no_atime" toml:"DisableAtime" yaml:"Disable Atime Updating" xml:"noAtime,attr"`
|
||||||
IsDirty bool
|
IsDirty bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_DIRTY_FL" json:"dirty" toml:"Dirty" yaml:"Dirty" xml:"dirty,attr"`
|
||||||
CompressedClusters bool
|
CompressedClusters bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_COMPRBLK_FL" json:"compress_clst" toml:"CompressedClusters" yaml:"Compressed Clusters" xml:"compressClst,attr"`
|
||||||
NoCompress bool
|
NoCompress bool `fsAttrShort:"m" fsAttrLong:"Dont_Compress" fsAttrKern:"FS_NOCOMP_FL" json:"no_compress" toml:"DisableCompression" yaml:"Disable Compression" xml:"noCompress,attr"`
|
||||||
EncFile bool
|
EncFile bool `fsAttrShort:"E" fsAttrLong:"Encrypted" fsAttrKern:"FS_ENCRYPT_FL" json:"enc" toml:"Encrypted" yaml:"Encrypted" xml:"enc,attr"`
|
||||||
BtreeFmt bool
|
BtreeFmt bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_BTREE_FL" json:"btree" toml:"Btree" yaml:"Btree" xml:"btree,attr"`
|
||||||
HashIdxDir bool
|
HashIdxDir bool `fsAttrShort:"I" fsAttrLong:"Indexed_directory" fsAttrKern:"FS_INDEX_FL" json:"idx_dir" toml:"IdxDir" yaml:"Indexed Directory" xml:"idxDir,attr"`
|
||||||
AfsDir bool
|
AfsDir bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_IMAGIC_FL" json:"afs" toml:"AFS" yaml:"AFS" xml:"afs,attr"`
|
||||||
ReservedExt3 bool
|
ReservedExt3 bool `fsAttrShort:"j" fsAttrLong:"Journaled_Data" fsAttrKern:"FS_JOURNAL_DATA_FL" json:"res_ext3" toml:"ReservedExt3" yaml:"Reserved Ext3" xml:"resExt3,attr"`
|
||||||
NoMergeTail bool
|
NoMergeTail bool `fsAttrShort:"t" fsAttrLong:"No_Tailmerging" fsAttrKern:"FS_NOTAIL_FL" json:"no_merge_tail" toml:"DisableTailmerging" yaml:"Disable Tailmerging" xml:"noMergeTail,attr"`
|
||||||
DirSync bool
|
DirSync bool `fsAttrShort:"D" fsAttrLong:"Synchronous_Directory_Updates" fsAttrKern:"FS_DIRSYNC_FL" json:"dir_sync" toml:"DirSync" yaml:"Synchronized Directory Updates" xml:"dirSync,attr"`
|
||||||
DirTop bool
|
DirTop bool `fsAttrShort:"T" fsAttrLong:"Top_of_Directory_Hierarchies" fsAttrKern:"FS_TOPDIR_FL" json:"dir_top" toml:"DirTop" yaml:"Top of Directory Hierarchies" xml:"dirTop,attr"`
|
||||||
ReservedExt4a bool
|
ReservedExt4a bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_HUGE_FILE_FL" json:"res_ext4a" toml:"ReservedExt4A" yaml:"Reserved Ext4 A" xml:"resExt4a,attr"`
|
||||||
Extents bool
|
Extents bool `fsAttrShort:"e" fsAttrLong:"Extents" fsAttrKern:"FS_EXTENT_FL" json:"extents" toml:"Extents" yaml:"Extents" xml:"extents,attr"`
|
||||||
LargeEaInode bool
|
VerityProtected bool `fsAttrShort:"V" fsAttrLong:"Verity" fsAttrKern:"FS_VERITY_FL" json:"verity" toml:"Verity" yaml:"Verity Protected" xml:"verity,attr"`
|
||||||
ReservedExt4b bool
|
LargeEaInode bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_EA_INODE_FL" json:"ea" toml:"EAInode" yaml:"EA Inode" xml:"ea,attr"`
|
||||||
NoCOWFile bool
|
ReservedExt4b bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_EOFBLOCKS_FL" json:"res_ext4b" toml:"ReservedExt4B" yaml:"Reserved Ext4 B" xml:"resExt4b,attr"`
|
||||||
ReservedExt4c bool
|
NoCOWFile bool `fsAttrShort:"C" fsAttrLong:"No_COW" fsAttrKern:"FS_NOCOW_FL" json:"no_cow" toml:"NoCOW" yaml:"Disable COW" xml:"noCOW,attr"`
|
||||||
UseParentProjId bool
|
DAX bool `fsAttrShort:"x" fsAttrLong:"DAX" fsAttrKern:"FS_DAX_FL" json:"dax" toml:"DAX" yaml:"DAX" xml:"DAX,attr"`
|
||||||
ReservedExt2 bool
|
ReservedExt4c bool `fsAttrShort:"N" fsAttrLong:"Inline_Data" fsAttrKern:"FS_INLINE_DATA_FL" json:"res_ext4c" toml:"ReservedExt4C" yaml:"Reserved Ext4 C" xml:"resExt4c,attr"`
|
||||||
|
UseParentProjId bool `fsAttrShort:"P" fsAttrLong:"Project_Hierarchy" fsAttrKern:"FS_PROJINHERIT_FL" json:"parent_proj_id" toml:"ParentProjId" yaml:"Use Parent Project ID" xml:"parentProjId,attr"`
|
||||||
|
CaseInsensitive bool `fsAttrShort:"F" fsAttrLong:"Casefold" fsAttrKern:"FS_CASEFOLD_FL" json:"case_ins" toml:"CaseInsensitive" yaml:"Case Insensitive" xml:"caseIns,attr"`
|
||||||
|
ReservedExt2 bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_RESERVED_FL" json:"res_ext2" toml:"ReservedExt2" yaml:"Reserved Ext2" xml:"resExt2,attr"`
|
||||||
}
|
}
|
||||||
|
9
go.mod
9
go.mod
@ -4,13 +4,10 @@ go 1.23.2
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/g0rbe/go-chattr v1.0.1
|
github.com/djherbis/times v1.6.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
|
golang.org/x/sync v0.9.0
|
||||||
golang.org/x/sys v0.26.0
|
golang.org/x/sys v0.26.0
|
||||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
||||||
r00t2.io/goutils v1.7.0
|
r00t2.io/goutils v1.7.1
|
||||||
r00t2.io/sysutils v1.8.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pending https://github.com/g0rbe/go-chattr/pull/3
|
|
||||||
replace github.com/g0rbe/go-chattr => github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13
|
|
||||||
|
13
go.sum
13
go.sum
@ -1,18 +1,19 @@
|
|||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
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/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=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
|
||||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
|
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.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
|
||||||
r00t2.io/goutils v1.7.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
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.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=
|
|
||||||
|
42
paths/consts.go
Normal file
42
paths/consts.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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)
|
||||||
|
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
|
||||||
|
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
|
||||||
|
)
|
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")
|
||||||
|
)
|
241
paths/funcs.go
241
paths/funcs.go
@ -19,14 +19,22 @@
|
|||||||
package paths
|
package paths
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
`context`
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
`sort`
|
||||||
"strings"
|
"strings"
|
||||||
|
`sync`
|
||||||
|
`time`
|
||||||
|
|
||||||
// "syscall"
|
// "syscall"
|
||||||
|
|
||||||
|
`github.com/djherbis/times`
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -266,3 +274,236 @@ 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.
|
||||||
|
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {
|
||||||
|
|
||||||
|
var matched *FsSearchResult
|
||||||
|
var missed *FsSearchResult
|
||||||
|
|
||||||
|
if err = RealPath(&matcher.Root); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = filepath.WalkDir(
|
||||||
|
matcher.Root,
|
||||||
|
func(path string, d fs.DirEntry, inErr error) (outErr error) {
|
||||||
|
|
||||||
|
if inErr != nil {
|
||||||
|
outErr = inErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched, missed, outErr = matcher.Match(path, d, nil); outErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if matched != nil && !matcher.NoMatch {
|
||||||
|
found = append(found, matched)
|
||||||
|
}
|
||||||
|
if missed != nil && !matcher.NoMismatch {
|
||||||
|
miss = append(miss, missed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if found == nil || len(found) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// And sort them.
|
||||||
|
sort.Slice(
|
||||||
|
found,
|
||||||
|
func(i, j int) (isLess bool) {
|
||||||
|
isLess = found[i].Path < found[j].Path
|
||||||
|
return
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent
|
||||||
|
workers for the filtering logic instead of performing iteratively/recursively.
|
||||||
|
It may, in some cases, be *slightly more* performant and *slightly less* in others.
|
||||||
|
Note that unlike SearchFsPaths, the results written to the
|
||||||
|
FsSearchCriteriaAsync.ResChan are not guaranteed to be in any predictable order.
|
||||||
|
|
||||||
|
All channels are expected to have already been initialized by the caller.
|
||||||
|
They will not be closed by this function.
|
||||||
|
*/
|
||||||
|
func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var wgLocal sync.WaitGroup
|
||||||
|
var doneChan chan bool = make(chan bool, 1)
|
||||||
|
|
||||||
|
if matcher.ErrChan == nil {
|
||||||
|
panic(ErrNilErrChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if matcher.WG == nil {
|
||||||
|
matcher.ErrChan <- ErrNilWg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer matcher.WG.Done()
|
||||||
|
|
||||||
|
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(
|
||||||
|
matcher.Root,
|
||||||
|
func(path string, de fs.DirEntry, inErr error) (outErr error) {
|
||||||
|
|
||||||
|
if inErr != nil {
|
||||||
|
inErr = filterNoFileDir(inErr)
|
||||||
|
if inErr != nil {
|
||||||
|
outErr = inErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgLocal.Add(1)
|
||||||
|
if matcher.Semaphore != nil {
|
||||||
|
if err = matcher.Semaphore.Acquire(matcher.SemaphoreCtx, 1); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(p string, d fs.DirEntry) {
|
||||||
|
var pErr error
|
||||||
|
var pResMatch *FsSearchResult
|
||||||
|
var pResMiss *FsSearchResult
|
||||||
|
|
||||||
|
defer wgLocal.Done()
|
||||||
|
|
||||||
|
if matcher.Semaphore != nil {
|
||||||
|
defer matcher.Semaphore.Release(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pResMatch, pResMiss, pErr = matcher.Match(p, d, nil); pErr != nil {
|
||||||
|
matcher.ErrChan <- pErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pResMatch != nil && !matcher.NoMatch {
|
||||||
|
matcher.ResChan <- pResMatch
|
||||||
|
}
|
||||||
|
if pResMiss != nil && !matcher.NoMismatch {
|
||||||
|
matcher.MismatchChan <- pResMiss
|
||||||
|
}
|
||||||
|
}(path, de)
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
err = filterNoFileDir(err)
|
||||||
|
if err != nil {
|
||||||
|
matcher.ErrChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wgLocal.Wait()
|
||||||
|
doneChan <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-doneChan
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ATIME
|
||||||
|
if mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeAccessed)) {
|
||||||
|
curAge = now.Sub(tspec.AccessTime())
|
||||||
|
if include = tfunc(&curAge); include {
|
||||||
|
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
|
||||||
|
}
|
13
paths/funcs_pathtimetype.go
Normal file
13
paths/funcs_pathtimetype.go
Normal file
@ -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
|
||||||
|
}
|
136
paths/types.go
Normal file
136
paths/types.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package paths
|
||||||
|
|
||||||
|
import (
|
||||||
|
`context`
|
||||||
|
`io/fs`
|
||||||
|
`regexp`
|
||||||
|
`sync`
|
||||||
|
`time`
|
||||||
|
|
||||||
|
`github.com/djherbis/times`
|
||||||
|
`golang.org/x/sync/semaphore`
|
||||||
|
`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 pathTimeType bitmask.MaskBit
|
Loading…
x
Reference in New Issue
Block a user