From 187ad868db788ebaab95f8bd53f11cfc4faa0518 Mon Sep 17 00:00:00 2001 From: brent saner Date: Fri, 26 Jan 2024 09:54:46 -0500 Subject: [PATCH] add filesystem attr (on linux) support --- fsutils/consts.go | 101 +++++++++++++++++++++++++++++++++++++++ fsutils/funcs.go | 44 +++++++++++++++++ fsutils/funcs_fsattrs.go | 43 +++++++++++++++++ fsutils/funcs_test.go | 71 +++++++++++++++++++++++++++ fsutils/types.go | 32 +++++++++++++ go.mod | 7 ++- go.sum | 2 + 7 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 fsutils/consts.go create mode 100644 fsutils/funcs.go create mode 100644 fsutils/funcs_fsattrs.go create mode 100644 fsutils/funcs_test.go create mode 100644 fsutils/types.go diff --git a/fsutils/consts.go b/fsutils/consts.go new file mode 100644 index 0000000..d03f3e0 --- /dev/null +++ b/fsutils/consts.go @@ -0,0 +1,101 @@ +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 ( + // 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). + Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so be forewarned. + */ + AttrValueNameMap map[uint32]string = map[uint32]string{ + 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", // 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. + 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", + } +) diff --git a/fsutils/funcs.go b/fsutils/funcs.go new file mode 100644 index 0000000..d153565 --- /dev/null +++ b/fsutils/funcs.go @@ -0,0 +1,44 @@ +package fsutils + +import ( + `os` + `reflect` + + `github.com/g0rbe/go-chattr` + `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 + } + + if f, err = os.Open(myPath); err != nil { + return + } + 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 +} diff --git a/fsutils/funcs_fsattrs.go b/fsutils/funcs_fsattrs.go new file mode 100644 index 0000000..30b5b89 --- /dev/null +++ b/fsutils/funcs_fsattrs.go @@ -0,0 +1,43 @@ +package fsutils + +import ( + `os` + `reflect` + + `github.com/g0rbe/go-chattr` + `r00t2.io/sysutils/paths` +) + +func (f *FsAttrs) Apply(path string) (err error) { + + var file *os.File + var reflectVal reflect.Value + var fieldVal reflect.Value + + var myPath string = path + + if err = paths.RealPath(&myPath); err != nil { + return + } + if file, err = os.Open(myPath); err != nil { + return + } + defer file.Close() + + reflectVal = reflect.ValueOf(*f) + + for attrNm, attrVal := range AttrNameValueMap { + fieldVal = reflectVal.FieldByName(attrNm) + if fieldVal.Bool() { + if err = chattr.SetAttr(file, attrVal); err != nil { + return + } + } else { + if err = chattr.UnsetAttr(file, attrVal); err != nil { + return + } + } + } + + return +} diff --git a/fsutils/funcs_test.go b/fsutils/funcs_test.go new file mode 100644 index 0000000..5cdb2d6 --- /dev/null +++ b/fsutils/funcs_test.go @@ -0,0 +1,71 @@ +package fsutils + +import ( + `errors` + `fmt` + `os` + `os/user` + `testing` + + `r00t2.io/sysutils/paths` +) + +var ( + testFilename string = "testfile" + testErrBadUser error = errors.New("test must be run as root, on Linux") +) + +func testChkUser() (err error) { + var u *user.User + + if u, err = user.Current(); err != nil { + return + } + if u.Uid != "0" { + err = testErrBadUser + return + } + return +} + +func TestSetAttrs(t *testing.T) { + + var err error + var attrs *FsAttrs + + if attrs, err = GetAttrs(testFilename); err != nil { + t.Fatalf("Failed to get attrs for %v: %v", testFilename, err) + } + t.Logf("Attrs for %v:\n%#v", testFilename, attrs) + attrs.CompressFile = true + if err = attrs.Apply(testFilename); err != nil { + t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err) + } + t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs) +} + +func TestMain(t *testing.M) { + + var err error + var rslt int + + if err = testChkUser(); err != nil { + fmt.Println(err) + os.Exit(1) + } + if err = paths.RealPath(&testFilename); err != nil { + fmt.Println(err) + os.Exit(2) + } + if err = os.WriteFile(testFilename, []byte("This is a test file."), 0o0644); err != nil { + fmt.Println(err) + os.Exit(3) + } + + rslt = t.Run() + + if err = os.Remove(testFilename); err != nil { + fmt.Printf("Failed to remove test file %v: %v", testFilename, err) + } + os.Exit(rslt) +} diff --git a/fsutils/types.go b/fsutils/types.go new file mode 100644 index 0000000..778f7d8 --- /dev/null +++ b/fsutils/types.go @@ -0,0 +1,32 @@ +package fsutils + +// FsAttrs is a convenience struct around github.com/g0rbe/go-chattr. +type FsAttrs struct { + SecureDelete bool + UnDelete bool + CompressFile bool + SyncUpdatechattr bool + Immutable bool + AppendOnly bool + NoDumpFile bool + NoUpdateAtime bool + IsDirty bool + CompressedClusters bool + NoCompress bool + EncFile bool + BtreeFmt bool + HashIdxDir bool + AfsDir bool + ReservedExt3 bool + NoMergeTail bool + DirSync bool + DirTop bool + ReservedExt4a bool + Extents bool + LargeEaInode bool + ReservedExt4b bool + NoCOWFile bool + ReservedExt4c bool + UseParentProjId bool + ReservedExt2 bool +} diff --git a/go.mod b/go.mod index 0b962a7..4e6077b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module r00t2.io/sysutils -go 1.16 +go 1.21 + +require github.com/g0rbe/go-chattr v1.0.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 diff --git a/go.sum b/go.sum index e69de29..a347158 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +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=