From e1a9a68b29fc80888cae96ac900445ba55b128f2 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 3 Mar 2025 11:24:30 +0100 Subject: [PATCH] FS: Add /pkg/fs/duf to determine mount points and disk usage #4266 Signed-off-by: Michael Mayer --- go.mod | 7 +- go.sum | 10 +- pkg/fs/bytes.go | 8 +- pkg/fs/duf/const.go | 18 ++ pkg/fs/duf/duf.go | 40 ++++ pkg/fs/duf/duf_test.go | 40 ++++ pkg/fs/duf/filesystems.go | 68 ++++++ pkg/fs/duf/filesystems_darwin.go | 21 ++ pkg/fs/duf/filesystems_freebsd.go | 36 +++ pkg/fs/duf/filesystems_linux.go | 300 ++++++++++++++++++++++++ pkg/fs/duf/filesystems_openbsd.go | 21 ++ pkg/fs/duf/filesystems_windows.go | 55 +++++ pkg/fs/duf/filters.go | 23 ++ pkg/fs/duf/groups.go | 113 +++++++++ pkg/fs/duf/groups_test.go | 46 ++++ pkg/fs/duf/mounts.go | 92 ++++++++ pkg/fs/duf/mounts_darwin.go | 95 ++++++++ pkg/fs/duf/mounts_freebsd.go | 107 +++++++++ pkg/fs/duf/mounts_linux.go | 171 ++++++++++++++ pkg/fs/duf/mounts_linux_test.go | 207 ++++++++++++++++ pkg/fs/duf/mounts_openbsd.go | 86 +++++++ pkg/fs/duf/mounts_windows.go | 377 ++++++++++++++++++++++++++++++ pkg/fs/duf/util.go | 53 +++++ 23 files changed, 1983 insertions(+), 11 deletions(-) create mode 100644 pkg/fs/duf/const.go create mode 100644 pkg/fs/duf/duf.go create mode 100644 pkg/fs/duf/duf_test.go create mode 100644 pkg/fs/duf/filesystems.go create mode 100644 pkg/fs/duf/filesystems_darwin.go create mode 100644 pkg/fs/duf/filesystems_freebsd.go create mode 100644 pkg/fs/duf/filesystems_linux.go create mode 100644 pkg/fs/duf/filesystems_openbsd.go create mode 100644 pkg/fs/duf/filesystems_windows.go create mode 100644 pkg/fs/duf/filters.go create mode 100644 pkg/fs/duf/groups.go create mode 100644 pkg/fs/duf/groups_test.go create mode 100644 pkg/fs/duf/mounts.go create mode 100644 pkg/fs/duf/mounts_darwin.go create mode 100644 pkg/fs/duf/mounts_freebsd.go create mode 100644 pkg/fs/duf/mounts_linux.go create mode 100644 pkg/fs/duf/mounts_linux_test.go create mode 100644 pkg/fs/duf/mounts_openbsd.go create mode 100644 pkg/fs/duf/mounts_windows.go create mode 100644 pkg/fs/duf/util.go diff --git a/go.mod b/go.mod index fb85823b8..6dd1af86e 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082 require golang.org/x/text v0.22.0 require ( + github.com/IGLOU-EU/go-wildcard v1.0.3 github.com/davidbyttow/govips/v2 v2.16.0 github.com/go-co-op/gocron/v2 v2.16.0 github.com/pquerna/otp v1.4.0 @@ -86,6 +87,7 @@ require ( github.com/swaggo/gin-swagger v1.6.0 github.com/urfave/cli/v2 v2.27.5 github.com/zitadel/oidc/v3 v3.35.0 + golang.org/x/sys v0.30.0 ) require ( @@ -130,7 +132,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.4.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/swaggo/swag v1.16.3 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -144,7 +146,6 @@ require ( go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sys v0.30.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/protobuf v1.36.4 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -162,7 +163,7 @@ require ( require ( github.com/emersion/go-webdav v0.6.0 - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/tidwall/pretty v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index fc0a0503f..34f2136a0 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= +github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= @@ -304,8 +306,8 @@ github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GW github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -349,8 +351,8 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= -github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/pkg/fs/bytes.go b/pkg/fs/bytes.go index 1af222b1f..4307c7637 100644 --- a/pkg/fs/bytes.go +++ b/pkg/fs/bytes.go @@ -1,8 +1,8 @@ package fs -// KiloByte, MegaByte, and GigaByte size constants. +// Size constants for KByte, MByte, and GByte. const ( - KiloByte = 1024 - MegaByte = KiloByte * 1024 - GigaByte = MegaByte * 1024 + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 ) diff --git a/pkg/fs/duf/const.go b/pkg/fs/duf/const.go new file mode 100644 index 000000000..37bda4eaf --- /dev/null +++ b/pkg/fs/duf/const.go @@ -0,0 +1,18 @@ +package duf + +// Supported device types. +const ( + LocalDevice = "local" + NetworkDevice = "network" + FuseDevice = "fuse" + SpecialDevice = "special" + LoopsDevice = "loops" + BindsMount = "binds" +) + +// Size constants for KByte, MByte, and GByte. +const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 +) diff --git a/pkg/fs/duf/duf.go b/pkg/fs/duf/duf.go new file mode 100644 index 000000000..4f5110a74 --- /dev/null +++ b/pkg/fs/duf/duf.go @@ -0,0 +1,40 @@ +/* +Package duf provides file system usage information. + +Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under Version 3 of the GNU Affero General Public License (the "AGPL"): + + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + The AGPL is supplemented by our Trademark and Brand Guidelines, + which describe how our Brand Assets may be used: + + +This code is copied and modified in part from: + + - https://github.com/muesli/duf + MIT License, Copyright (c) 2020 Christian Muehlhaeuser + see https://github.com/muesli/duf?tab=License-1-ov-file#readme + + - https://github.com/shirou/gopsutil + BSD License, Copyright (c) 2014, WAKAYAMA Shirou + see https://github.com/shirou/gopsutil?tab=License-1-ov-file#readme + +Feel free to send an email to hello@photoprism.app if you have questions, +want to support our work, or just want to say hello. + +Additional information can be found in our Developer Guide: + +*/ +package duf + +// Mounts returns the active file system mounts, along with any warnings or errors that have occurred. +func Mounts() (m []Mount, warnings []string, err error) { + return mounts() +} diff --git a/pkg/fs/duf/duf_test.go b/pkg/fs/duf/duf_test.go new file mode 100644 index 000000000..05bc8cb1f --- /dev/null +++ b/pkg/fs/duf/duf_test.go @@ -0,0 +1,40 @@ +package duf + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMounts(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Get slice of mounted file systems. + results, warnings, err := Mounts() + + // No warnings or errors are expected. + assert.NoError(t, err) + assert.Empty(t, warnings) + + // At least one mount returned? + if len(results) < 1 { + t.Error("at least one result expected") + } else { + // If so, check the first mount for plausibility. + result := results[0] + assert.NotEmpty(t, result.Device) + assert.Equal(t, "local", result.DeviceType) + assert.Equal(t, "/", result.Mountpoint) + assert.NotEmpty(t, result.Fstype) + assert.NotEmpty(t, result.Opts) + assert.NotEmpty(t, result.Total) + assert.NotEmpty(t, result.Used) + assert.NotEmpty(t, result.Free) + assert.NotEmpty(t, result.Inodes) + assert.NotEmpty(t, result.InodesFree) + assert.NotEmpty(t, result.InodesUsed) + assert.NotEmpty(t, result.Blocks) + assert.NotEmpty(t, result.BlockSize) + assert.NotEmpty(t, result.Metadata) + } + }) +} diff --git a/pkg/fs/duf/filesystems.go b/pkg/fs/duf/filesystems.go new file mode 100644 index 000000000..8dc028090 --- /dev/null +++ b/pkg/fs/duf/filesystems.go @@ -0,0 +1,68 @@ +package duf + +import ( + "os" + "path/filepath" + "strings" +) + +func findMounts(mounts []Mount, path string) ([]Mount, error) { + var err error + path, err = filepath.Abs(path) + if err != nil { + return nil, err + } + + path, err = filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + + _, err = os.Stat(path) + if err != nil { + return nil, err + } + + var m []Mount + for _, v := range mounts { + if path == v.Device { + return []Mount{v}, nil + } + + if strings.HasPrefix(path, v.Mountpoint) { + var nm []Mount + + // keep all entries that are as close or closer to the target + for _, mv := range m { + if len(mv.Mountpoint) >= len(v.Mountpoint) { + nm = append(nm, mv) + } + } + m = nm + + // add entry only if we didn't already find something closer + if len(nm) == 0 || len(v.Mountpoint) >= len(nm[0].Mountpoint) { + m = append(m, v) + } + } + } + + return m, nil +} + +func deviceType(m Mount) string { + if isNetworkFs(m) { + return NetworkDevice + } + if isSpecialFs(m) { + return SpecialDevice + } + if isFuseFs(m) { + return FuseDevice + } + + return LocalDevice +} + +// remote: [ "nfs", "smbfs", "cifs", "ncpfs", "afs", "coda", "ftpfs", "mfs", "sshfs", "fuse.sshfs", "nfs4" ] +// special: [ "tmpfs", "devpts", "devtmpfs", "proc", "sysfs", "usbfs", "devfs", "fdescfs", "linprocfs" ] diff --git a/pkg/fs/duf/filesystems_darwin.go b/pkg/fs/duf/filesystems_darwin.go new file mode 100644 index 000000000..776652390 --- /dev/null +++ b/pkg/fs/duf/filesystems_darwin.go @@ -0,0 +1,21 @@ +//go:build darwin + +package duf + +func isFuseFs(m Mount) bool { + //FIXME: implement + return false +} + +func isNetworkFs(m Mount) bool { + //FIXME: implement + return false +} + +func isSpecialFs(m Mount) bool { + return m.Fstype == "devfs" +} + +func isHiddenFs(m Mount) bool { + return false +} diff --git a/pkg/fs/duf/filesystems_freebsd.go b/pkg/fs/duf/filesystems_freebsd.go new file mode 100644 index 000000000..d131f54b5 --- /dev/null +++ b/pkg/fs/duf/filesystems_freebsd.go @@ -0,0 +1,36 @@ +//go:build freebsd + +package duf + +func isFuseFs(m Mount) bool { + //FIXME: implement + return false +} + +func isNetworkFs(m Mount) bool { + fs := []string{"nfs", "smbfs"} + + for _, v := range fs { + if m.Fstype == v { + return true + } + } + + return false +} + +func isSpecialFs(m Mount) bool { + fs := []string{"devfs", "tmpfs", "linprocfs", "linsysfs", "fdescfs", "procfs"} + + for _, v := range fs { + if m.Fstype == v { + return true + } + } + + return false +} + +func isHiddenFs(m Mount) bool { + return false +} diff --git a/pkg/fs/duf/filesystems_linux.go b/pkg/fs/duf/filesystems_linux.go new file mode 100644 index 000000000..cc12b45cb --- /dev/null +++ b/pkg/fs/duf/filesystems_linux.go @@ -0,0 +1,300 @@ +//go:build linux + +package duf + +import "strings" + +//nolint:revive,deadcode +const ( + // man statfs + ADFS_SUPER_MAGIC = 0xadf5 + AFFS_SUPER_MAGIC = 0xADFF + AUTOFS_SUPER_MAGIC = 0x0187 + BDEVFS_MAGIC = 0x62646576 + BEFS_SUPER_MAGIC = 0x42465331 + BFS_MAGIC = 0x1BADFACE + BINFMTFS_MAGIC = 0x42494e4d + BPF_FS_MAGIC = 0xcafe4a11 + BTRFS_SUPER_MAGIC = 0x9123683E + CGROUP_SUPER_MAGIC = 0x27e0eb + CGROUP2_SUPER_MAGIC = 0x63677270 + CIFS_MAGIC_NUMBER = 0xFF534D42 + CODA_SUPER_MAGIC = 0x73757245 + COH_SUPER_MAGIC = 0x012FF7B7 + CONFIGFS_MAGIC = 0x62656570 + CRAMFS_MAGIC = 0x28cd3d45 + DEBUGFS_MAGIC = 0x64626720 + DEVFS_SUPER_MAGIC = 0x1373 + DEVPTS_SUPER_MAGIC = 0x1cd1 + EFIVARFS_MAGIC = 0xde5e81e4 + EFS_SUPER_MAGIC = 0x00414A53 + EXT_SUPER_MAGIC = 0x137D + EXT2_OLD_SUPER_MAGIC = 0xEF51 + EXT2_SUPER_MAGIC = 0xEF53 + EXT3_SUPER_MAGIC = 0xEF53 + EXT4_SUPER_MAGIC = 0xEF53 + FUSE_SUPER_MAGIC = 0x65735546 + FUTEXFS_SUPER_MAGIC = 0xBAD1DEA + HFS_SUPER_MAGIC = 0x4244 + HFSPLUS_SUPER_MAGIC = 0x482b + HOSTFS_SUPER_MAGIC = 0x00c0ffee + HPFS_SUPER_MAGIC = 0xF995E849 + HUGETLBFS_MAGIC = 0x958458f6 + ISOFS_SUPER_MAGIC = 0x9660 + JFFS2_SUPER_MAGIC = 0x72b6 + JFS_SUPER_MAGIC = 0x3153464a + MINIX_SUPER_MAGIC = 0x137F /* orig. minix */ + MINIX_SUPER_MAGIC2 = 0x138F /* 30 char minix */ + MINIX2_SUPER_MAGIC = 0x2468 /* minix V2 */ + MINIX2_SUPER_MAGIC2 = 0x2478 /* minix V2, 30 char names */ + MINIX3_SUPER_MAGIC = 0x4d5a /* minix V3 fs, 60 char names */ + MQUEUE_MAGIC = 0x19800202 + MSDOS_SUPER_MAGIC = 0x4d44 + NCP_SUPER_MAGIC = 0x564c + NFS_SUPER_MAGIC = 0x6969 + NILFS_SUPER_MAGIC = 0x3434 + NTFS_SB_MAGIC = 0x5346544e + OCFS2_SUPER_MAGIC = 0x7461636f + OPENPROM_SUPER_MAGIC = 0x9fa1 + PIPEFS_MAGIC = 0x50495045 + PROC_SUPER_MAGIC = 0x9fa0 + PSTOREFS_MAGIC = 0x6165676C + QNX4_SUPER_MAGIC = 0x002f + QNX6_SUPER_MAGIC = 0x68191122 + RAMFS_MAGIC = 0x858458f6 + REISERFS_SUPER_MAGIC = 0x52654973 + ROMFS_MAGIC = 0x7275 + SELINUX_MAGIC = 0xf97cff8c + SMACK_MAGIC = 0x43415d53 + SMB_SUPER_MAGIC = 0x517B + SMB2_MAGIC_NUMBER = 0xfe534d42 + SOCKFS_MAGIC = 0x534F434B + SQUASHFS_MAGIC = 0x73717368 + SYSFS_MAGIC = 0x62656572 + SYSV2_SUPER_MAGIC = 0x012FF7B6 + SYSV4_SUPER_MAGIC = 0x012FF7B5 + TMPFS_MAGIC = 0x01021994 + TRACEFS_MAGIC = 0x74726163 + UDF_SUPER_MAGIC = 0x15013346 + UFS_MAGIC = 0x00011954 + USBDEVICE_SUPER_MAGIC = 0x9fa2 + V9FS_MAGIC = 0x01021997 + VXFS_SUPER_MAGIC = 0xa501FCF5 + XENFS_SUPER_MAGIC = 0xabba1974 + XENIX_SUPER_MAGIC = 0x012FF7B4 + XFS_SUPER_MAGIC = 0x58465342 + _XIAFS_SUPER_MAGIC = 0x012FD16D + + AFS_SUPER_MAGIC = 0x5346414F + AUFS_SUPER_MAGIC = 0x61756673 + ANON_INODE_FS_SUPER_MAGIC = 0x09041934 + CEPH_SUPER_MAGIC = 0x00C36400 + ECRYPTFS_SUPER_MAGIC = 0xF15F + FAT_SUPER_MAGIC = 0x4006 + FHGFS_SUPER_MAGIC = 0x19830326 + FUSEBLK_SUPER_MAGIC = 0x65735546 + FUSECTL_SUPER_MAGIC = 0x65735543 + GFS_SUPER_MAGIC = 0x1161970 + GPFS_SUPER_MAGIC = 0x47504653 + MTD_INODE_FS_SUPER_MAGIC = 0x11307854 + INOTIFYFS_SUPER_MAGIC = 0x2BAD1DEA + ISOFS_R_WIN_SUPER_MAGIC = 0x4004 + ISOFS_WIN_SUPER_MAGIC = 0x4000 + JFFS_SUPER_MAGIC = 0x07C0 + KAFS_SUPER_MAGIC = 0x6B414653 + LUSTRE_SUPER_MAGIC = 0x0BD00BD0 + NFSD_SUPER_MAGIC = 0x6E667364 + PANFS_SUPER_MAGIC = 0xAAD7AAEA + RPC_PIPEFS_SUPER_MAGIC = 0x67596969 + SECURITYFS_SUPER_MAGIC = 0x73636673 + UFS_BYTESWAPPED_SUPER_MAGIC = 0x54190100 + VMHGFS_SUPER_MAGIC = 0xBACBACBC + VZFS_SUPER_MAGIC = 0x565A4653 + ZFS_SUPER_MAGIC = 0x2FC12FC1 +) + +// coreutils/src/stat.c +var fsTypeMap = map[int64]string{ + ADFS_SUPER_MAGIC: "adfs", /* 0xADF5 local */ + AFFS_SUPER_MAGIC: "affs", /* 0xADFF local */ + AFS_SUPER_MAGIC: "afs", /* 0x5346414F remote */ + ANON_INODE_FS_SUPER_MAGIC: "anon-inode FS", /* 0x09041934 local */ + AUFS_SUPER_MAGIC: "aufs", /* 0x61756673 remote */ + AUTOFS_SUPER_MAGIC: "autofs", /* 0x0187 local */ + BEFS_SUPER_MAGIC: "befs", /* 0x42465331 local */ + BDEVFS_MAGIC: "bdevfs", /* 0x62646576 local */ + BFS_MAGIC: "bfs", /* 0x1BADFACE local */ + BINFMTFS_MAGIC: "binfmt_misc", /* 0x42494E4D local */ + BTRFS_SUPER_MAGIC: "btrfs", /* 0x9123683E local */ + CEPH_SUPER_MAGIC: "ceph", /* 0x00C36400 remote */ + CGROUP_SUPER_MAGIC: "cgroupfs", /* 0x0027E0EB local */ + CIFS_MAGIC_NUMBER: "cifs", /* 0xFF534D42 remote */ + CODA_SUPER_MAGIC: "coda", /* 0x73757245 remote */ + COH_SUPER_MAGIC: "coh", /* 0x012FF7B7 local */ + CRAMFS_MAGIC: "cramfs", /* 0x28CD3D45 local */ + DEBUGFS_MAGIC: "debugfs", /* 0x64626720 local */ + DEVFS_SUPER_MAGIC: "devfs", /* 0x1373 local */ + DEVPTS_SUPER_MAGIC: "devpts", /* 0x1CD1 local */ + ECRYPTFS_SUPER_MAGIC: "ecryptfs", /* 0xF15F local */ + EFS_SUPER_MAGIC: "efs", /* 0x00414A53 local */ + EXT_SUPER_MAGIC: "ext", /* 0x137D local */ + EXT2_SUPER_MAGIC: "ext2/ext3", /* 0xEF53 local */ + EXT2_OLD_SUPER_MAGIC: "ext2", /* 0xEF51 local */ + FAT_SUPER_MAGIC: "fat", /* 0x4006 local */ + FHGFS_SUPER_MAGIC: "fhgfs", /* 0x19830326 remote */ + FUSEBLK_SUPER_MAGIC: "fuseblk", /* 0x65735546 remote */ + FUSECTL_SUPER_MAGIC: "fusectl", /* 0x65735543 remote */ + FUTEXFS_SUPER_MAGIC: "futexfs", /* 0x0BAD1DEA local */ + GFS_SUPER_MAGIC: "gfs/gfs2", /* 0x1161970 remote */ + GPFS_SUPER_MAGIC: "gpfs", /* 0x47504653 remote */ + HFS_SUPER_MAGIC: "hfs", /* 0x4244 local */ + HFSPLUS_SUPER_MAGIC: "hfsplus", /* 0x482b local */ + HPFS_SUPER_MAGIC: "hpfs", /* 0xF995E849 local */ + HUGETLBFS_MAGIC: "hugetlbfs", /* 0x958458F6 local */ + MTD_INODE_FS_SUPER_MAGIC: "inodefs", /* 0x11307854 local */ + INOTIFYFS_SUPER_MAGIC: "inotifyfs", /* 0x2BAD1DEA local */ + ISOFS_SUPER_MAGIC: "isofs", /* 0x9660 local */ + ISOFS_R_WIN_SUPER_MAGIC: "isofs", /* 0x4004 local */ + ISOFS_WIN_SUPER_MAGIC: "isofs", /* 0x4000 local */ + JFFS_SUPER_MAGIC: "jffs", /* 0x07C0 local */ + JFFS2_SUPER_MAGIC: "jffs2", /* 0x72B6 local */ + JFS_SUPER_MAGIC: "jfs", /* 0x3153464A local */ + KAFS_SUPER_MAGIC: "k-afs", /* 0x6B414653 remote */ + LUSTRE_SUPER_MAGIC: "lustre", /* 0x0BD00BD0 remote */ + MINIX_SUPER_MAGIC: "minix", /* 0x137F local */ + MINIX_SUPER_MAGIC2: "minix (30 char.)", /* 0x138F local */ + MINIX2_SUPER_MAGIC: "minix v2", /* 0x2468 local */ + MINIX2_SUPER_MAGIC2: "minix v2 (30 char.)", /* 0x2478 local */ + MINIX3_SUPER_MAGIC: "minix3", /* 0x4D5A local */ + MQUEUE_MAGIC: "mqueue", /* 0x19800202 local */ + MSDOS_SUPER_MAGIC: "msdos", /* 0x4D44 local */ + NCP_SUPER_MAGIC: "novell", /* 0x564C remote */ + NFS_SUPER_MAGIC: "nfs", /* 0x6969 remote */ + NFSD_SUPER_MAGIC: "nfsd", /* 0x6E667364 remote */ + NILFS_SUPER_MAGIC: "nilfs", /* 0x3434 local */ + NTFS_SB_MAGIC: "ntfs", /* 0x5346544E local */ + OPENPROM_SUPER_MAGIC: "openprom", /* 0x9FA1 local */ + OCFS2_SUPER_MAGIC: "ocfs2", /* 0x7461636f remote */ + PANFS_SUPER_MAGIC: "panfs", /* 0xAAD7AAEA remote */ + PIPEFS_MAGIC: "pipefs", /* 0x50495045 remote */ + PROC_SUPER_MAGIC: "proc", /* 0x9FA0 local */ + PSTOREFS_MAGIC: "pstorefs", /* 0x6165676C local */ + QNX4_SUPER_MAGIC: "qnx4", /* 0x002F local */ + QNX6_SUPER_MAGIC: "qnx6", /* 0x68191122 local */ + RAMFS_MAGIC: "ramfs", /* 0x858458F6 local */ + REISERFS_SUPER_MAGIC: "reiserfs", /* 0x52654973 local */ + ROMFS_MAGIC: "romfs", /* 0x7275 local */ + RPC_PIPEFS_SUPER_MAGIC: "rpc_pipefs", /* 0x67596969 local */ + SECURITYFS_SUPER_MAGIC: "securityfs", /* 0x73636673 local */ + SELINUX_MAGIC: "selinux", /* 0xF97CFF8C local */ + SMB_SUPER_MAGIC: "smb", /* 0x517B remote */ + SMB2_MAGIC_NUMBER: "smb2", /* 0xfe534d42 remote */ + SOCKFS_MAGIC: "sockfs", /* 0x534F434B local */ + SQUASHFS_MAGIC: "squashfs", /* 0x73717368 local */ + SYSFS_MAGIC: "sysfs", /* 0x62656572 local */ + SYSV2_SUPER_MAGIC: "sysv2", /* 0x012FF7B6 local */ + SYSV4_SUPER_MAGIC: "sysv4", /* 0x012FF7B5 local */ + TMPFS_MAGIC: "tmpfs", /* 0x01021994 local */ + UDF_SUPER_MAGIC: "udf", /* 0x15013346 local */ + UFS_MAGIC: "ufs", /* 0x00011954 local */ + UFS_BYTESWAPPED_SUPER_MAGIC: "ufs", /* 0x54190100 local */ + USBDEVICE_SUPER_MAGIC: "usbdevfs", /* 0x9FA2 local */ + V9FS_MAGIC: "v9fs", /* 0x01021997 local */ + VMHGFS_SUPER_MAGIC: "vmhgfs", /* 0xBACBACBC remote */ + VXFS_SUPER_MAGIC: "vxfs", /* 0xA501FCF5 local */ + VZFS_SUPER_MAGIC: "vzfs", /* 0x565A4653 local */ + XENFS_SUPER_MAGIC: "xenfs", /* 0xABBA1974 local */ + XENIX_SUPER_MAGIC: "xenix", /* 0x012FF7B4 local */ + XFS_SUPER_MAGIC: "xfs", /* 0x58465342 local */ + _XIAFS_SUPER_MAGIC: "xia", /* 0x012FD16D local */ + ZFS_SUPER_MAGIC: "zfs", /* 0x2FC12FC1 local */ +} + +/* +var localMap = map[int64]bool{ + AFS_SUPER_MAGIC: true, + BTRFS_SUPER_MAGIC: true, + EXT_SUPER_MAGIC: true, + EXT2_OLD_SUPER_MAGIC: true, + EXT2_SUPER_MAGIC: true, + FAT_SUPER_MAGIC: true, + HPFS_SUPER_MAGIC: true, + MSDOS_SUPER_MAGIC: true, + NTFS_SB_MAGIC: true, + REISERFS_SUPER_MAGIC: true, + UDF_SUPER_MAGIC: true, + XFS_SUPER_MAGIC: true, + ZFS_SUPER_MAGIC: true, +} +*/ + +var networkMap = map[int64]bool{ + CIFS_MAGIC_NUMBER: true, + NFS_SUPER_MAGIC: true, + SMB_SUPER_MAGIC: true, + SMB2_MAGIC_NUMBER: true, +} + +var specialMap = map[int64]bool{ + AUTOFS_SUPER_MAGIC: true, + BINFMTFS_MAGIC: true, + BPF_FS_MAGIC: true, + CGROUP_SUPER_MAGIC: true, + CGROUP2_SUPER_MAGIC: true, + CONFIGFS_MAGIC: true, + DEBUGFS_MAGIC: true, + DEVPTS_SUPER_MAGIC: true, + EFIVARFS_MAGIC: true, + FUSECTL_SUPER_MAGIC: true, + HUGETLBFS_MAGIC: true, + MQUEUE_MAGIC: true, + PROC_SUPER_MAGIC: true, + PSTOREFS_MAGIC: true, + SECURITYFS_SUPER_MAGIC: true, + SYSFS_MAGIC: true, + TMPFS_MAGIC: true, + TRACEFS_MAGIC: true, +} + +/* +func isLocalFs(m Mount) bool { + return localMap[int64(m.Stat().Type)] //nolint:unconvert +} +*/ + +func isFuseFs(m Mount) bool { + return m.Stat().Type == FUSEBLK_SUPER_MAGIC || + m.Stat().Type == FUSE_SUPER_MAGIC +} + +func isNetworkFs(m Mount) bool { + return networkMap[int64(m.Stat().Type)] //nolint:unconvert +} + +func isSpecialFs(m Mount) bool { + if m.Device == "nsfs" { + return true + } + + return specialMap[int64(m.Stat().Type)] //nolint:unconvert +} + +func isHiddenFs(m Mount) bool { + switch m.Device { + case "shm": + return true + case "overlay": + return true + } + + switch m.Fstype { + case "autofs": + return true + case "squashfs": + if strings.HasPrefix(m.Mountpoint, "/snap") { + return true + } + } + + return false +} diff --git a/pkg/fs/duf/filesystems_openbsd.go b/pkg/fs/duf/filesystems_openbsd.go new file mode 100644 index 000000000..2440cf43a --- /dev/null +++ b/pkg/fs/duf/filesystems_openbsd.go @@ -0,0 +1,21 @@ +//go:build openbsd + +package duf + +func isFuseFs(m Mount) bool { + //FIXME: implement + return false +} + +func isNetworkFs(m Mount) bool { + //FIXME: implement + return false +} + +func isSpecialFs(m Mount) bool { + return m.Fstype == "devfs" +} + +func isHiddenFs(m Mount) bool { + return false +} diff --git a/pkg/fs/duf/filesystems_windows.go b/pkg/fs/duf/filesystems_windows.go new file mode 100644 index 000000000..eda4aef5d --- /dev/null +++ b/pkg/fs/duf/filesystems_windows.go @@ -0,0 +1,55 @@ +//go:build windows + +package duf + +import ( + "golang.org/x/sys/windows/registry" +) + +const ( + WindowsSandboxMountPointRegistryPath = `Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\CPC\LocalMOF` +) + +var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints() + +func loadRegisteredWindowsSandboxMountPoints() (ret map[string]struct{}) { + ret = make(map[string]struct{}) + key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ) + if err != nil { + return + } + + keyInfo, err := key.Stat() + if err != nil { + return + } + + mountPoints, err := key.ReadValueNames(int(keyInfo.ValueCount)) + if err != nil { + return + } + + for _, val := range mountPoints { + ret[val] = struct{}{} + } + return ret +} + +func isFuseFs(m Mount) bool { + //FIXME: implement + return false +} + +func isNetworkFs(m Mount) bool { + _, ok := m.Metadata.(*NetResource) + return ok +} + +func isSpecialFs(m Mount) bool { + _, ok := windowsSandboxMountPoints[m.Mountpoint] + return ok +} + +func isHiddenFs(m Mount) bool { + return false +} diff --git a/pkg/fs/duf/filters.go b/pkg/fs/duf/filters.go new file mode 100644 index 000000000..21dfcf4a8 --- /dev/null +++ b/pkg/fs/duf/filters.go @@ -0,0 +1,23 @@ +package duf + +var ( + all = false + hideDevices = "" + hideFs = "" + hideMp = "" + onlyDevices = "" + onlyFs = "" + onlyMp = "" +) + +// FilterOptions contains all filters. +type FilterOptions struct { + HiddenDevices map[string]struct{} + OnlyDevices map[string]struct{} + + HiddenFilesystems map[string]struct{} + OnlyFilesystems map[string]struct{} + + HiddenMountPoints map[string]struct{} + OnlyMountPoints map[string]struct{} +} diff --git a/pkg/fs/duf/groups.go b/pkg/fs/duf/groups.go new file mode 100644 index 000000000..0bb789120 --- /dev/null +++ b/pkg/fs/duf/groups.go @@ -0,0 +1,113 @@ +package duf + +import ( + "strings" +) + +var ( + groups = []string{LocalDevice, NetworkDevice, FuseDevice, SpecialDevice, LoopsDevice, BindsMount} +) + +type GroupedMounts map[string][]Mount + +func GroupMounts(m []Mount, filters FilterOptions) GroupedMounts { + deviceMounts := make(GroupedMounts) + hasOnlyDevices := len(filters.OnlyDevices) != 0 + + _, hideLocal := filters.HiddenDevices[LocalDevice] + _, hideNetwork := filters.HiddenDevices[NetworkDevice] + _, hideFuse := filters.HiddenDevices[FuseDevice] + _, hideSpecial := filters.HiddenDevices[SpecialDevice] + _, hideLoops := filters.HiddenDevices[LoopsDevice] + _, hideBinds := filters.HiddenDevices[BindsMount] + + _, onlyLocal := filters.OnlyDevices[LocalDevice] + _, onlyNetwork := filters.OnlyDevices[NetworkDevice] + _, onlyFuse := filters.OnlyDevices[FuseDevice] + _, onlySpecial := filters.OnlyDevices[SpecialDevice] + _, onlyLoops := filters.OnlyDevices[LoopsDevice] + _, onlyBinds := filters.OnlyDevices[BindsMount] + + // sort/filter devices + for _, v := range m { + if len(filters.OnlyFilesystems) != 0 { + // skip not onlyFs + if _, ok := filters.OnlyFilesystems[strings.ToLower(v.Fstype)]; !ok { + continue + } + } else { + // skip hideFs + if _, ok := filters.HiddenFilesystems[strings.ToLower(v.Fstype)]; ok { + continue + } + } + + // skip hidden devices + if isHiddenFs(v) && !all { + continue + } + + // skip bind-mounts + if strings.Contains(v.Opts, "bind") { + if (hasOnlyDevices && !onlyBinds) || (hideBinds && !all) { + continue + } + } + + // skip loop devices + if strings.HasPrefix(v.Device, "/dev/loop") { + if (hasOnlyDevices && !onlyLoops) || (hideLoops && !all) { + continue + } + } + + // skip special devices + if v.Blocks == 0 && !all { + continue + } + + // skip zero size devices + if v.BlockSize == 0 && !all { + continue + } + + // skip not only mount point + if len(filters.OnlyMountPoints) != 0 { + if !findInKey(v.Mountpoint, filters.OnlyMountPoints) { + continue + } + } + + // skip hidden mount point + if len(filters.HiddenMountPoints) != 0 { + if findInKey(v.Mountpoint, filters.HiddenMountPoints) { + continue + } + } + + t := deviceType(v) + + if !all { + switch { + case hasOnlyDevices && onlyLocal && t != LocalDevice: + continue + case hasOnlyDevices && onlyNetwork && t != NetworkDevice: + continue + case hasOnlyDevices && onlyFuse && t != FuseDevice: + continue + case hasOnlyDevices && onlySpecial && t != SpecialDevice: + continue + case + t == LocalDevice && hideLocal, + t == NetworkDevice && hideNetwork, + t == FuseDevice && hideFuse, + t == SpecialDevice && hideSpecial: + continue + } + } + + deviceMounts[t] = append(deviceMounts[t], v) + } + + return deviceMounts +} diff --git a/pkg/fs/duf/groups_test.go b/pkg/fs/duf/groups_test.go new file mode 100644 index 000000000..05a0d427e --- /dev/null +++ b/pkg/fs/duf/groups_test.go @@ -0,0 +1,46 @@ +package duf + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGroupMounts(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Get slice of mounted file systems. + m, warnings, err := Mounts() + + // No warnings or errors are expected. + assert.NoError(t, err) + assert.Empty(t, warnings) + + filters := FilterOptions{ + HiddenDevices: parseCommaSeparatedValues(hideDevices), + OnlyDevices: parseCommaSeparatedValues(onlyDevices), + HiddenFilesystems: parseCommaSeparatedValues(hideFs), + OnlyFilesystems: parseCommaSeparatedValues(onlyFs), + HiddenMountPoints: parseCommaSeparatedValues(hideMp), + OnlyMountPoints: parseCommaSeparatedValues(onlyMp), + } + + results := GroupMounts(m, filters) + + t.Logf("results, %#v", results) + + // At least one mount returned? + if len(results) < 1 { + t.Error("at least one result expected") + } else if local, found := results[LocalDevice]; found { + for _, d := range local { + if d.Total <= 0 { + t.Error("total should be a positive integer") + } else { + t.Logf("%s is mounted at %s: %d of %d GB used (%.1f%%)", d.Device, d.Mountpoint, d.Used/GB, d.Total/GB, (float64(d.Used)/float64(d.Total))*100) + } + } + } else { + t.Error("no local devices found") + } + }) +} diff --git a/pkg/fs/duf/mounts.go b/pkg/fs/duf/mounts.go new file mode 100644 index 000000000..f17193edf --- /dev/null +++ b/pkg/fs/duf/mounts.go @@ -0,0 +1,92 @@ +package duf + +import ( + "bufio" + "os" + "strconv" +) + +// Mount contains all metadata for a single filesystem mount. +type Mount struct { + Device string `json:"device"` + DeviceType string `json:"device_type"` + Mountpoint string `json:"mount_point"` + Fstype string `json:"fs_type"` + Type string `json:"type"` + Opts string `json:"opts"` + Total uint64 `json:"total"` + Free uint64 `json:"free"` + Used uint64 `json:"used"` + Inodes uint64 `json:"inodes"` + InodesFree uint64 `json:"inodes_free"` + InodesUsed uint64 `json:"inodes_used"` + Blocks uint64 `json:"blocks"` + BlockSize uint64 `json:"block_size"` + Metadata interface{} `json:"-"` +} + +func readLines(filename string) ([]string, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() //nolint:errcheck // ignore error + + scanner := bufio.NewScanner(file) + var s []string + for scanner.Scan() { + s = append(s, scanner.Text()) + } + + return s, scanner.Err() +} + +func unescapeFstab(path string) string { + escaped, err := strconv.Unquote(`"` + path + `"`) + if err != nil { + return path + } + return escaped +} + +//nolint:deadcode,unused // used on BSD +func byteToString(orig []byte) string { + n := -1 + l := -1 + for i, b := range orig { + // skip left side null + if l == -1 && b == 0 { + continue + } + if l == -1 { + l = i + } + + if b == 0 { + break + } + n = i + 1 + } + if n == -1 { + return string(orig) + } + return string(orig[l:n]) +} + +//nolint:deadcode,unused // used on OpenBSD +func intToString(orig []int8) string { + ret := make([]byte, len(orig)) + size := -1 + for i, o := range orig { + if o == 0 { + size = i + break + } + ret[i] = byte(o) + } + if size == -1 { + size = len(orig) + } + + return string(ret[0:size]) +} diff --git a/pkg/fs/duf/mounts_darwin.go b/pkg/fs/duf/mounts_darwin.go new file mode 100644 index 000000000..9af8790e6 --- /dev/null +++ b/pkg/fs/duf/mounts_darwin.go @@ -0,0 +1,95 @@ +//go:build darwin + +package duf + +import ( + "golang.org/x/sys/unix" +) + +func (m *Mount) Stat() unix.Statfs_t { + return m.Metadata.(unix.Statfs_t) +} + +func mounts() ([]Mount, []string, error) { + var ret []Mount + var warnings []string + + count, err := unix.Getfsstat(nil, unix.MNT_WAIT) + if err != nil { + return nil, nil, err + } + fs := make([]unix.Statfs_t, count) + if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { + return nil, nil, err + } + + for _, stat := range fs { + opts := "rw" + if stat.Flags&unix.MNT_RDONLY != 0 { + opts = "ro" + } + if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { + opts += ",sync" + } + if stat.Flags&unix.MNT_NOEXEC != 0 { + opts += ",noexec" + } + if stat.Flags&unix.MNT_NOSUID != 0 { + opts += ",nosuid" + } + if stat.Flags&unix.MNT_UNION != 0 { + opts += ",union" + } + if stat.Flags&unix.MNT_ASYNC != 0 { + opts += ",async" + } + if stat.Flags&unix.MNT_DONTBROWSE != 0 { + opts += ",nobrowse" + } + if stat.Flags&unix.MNT_AUTOMOUNTED != 0 { + opts += ",automounted" + } + if stat.Flags&unix.MNT_JOURNALED != 0 { + opts += ",journaled" + } + if stat.Flags&unix.MNT_MULTILABEL != 0 { + opts += ",multilabel" + } + if stat.Flags&unix.MNT_NOATIME != 0 { + opts += ",noatime" + } + if stat.Flags&unix.MNT_NODEV != 0 { + opts += ",nodev" + } + + device := byteToString(stat.Mntfromname[:]) + mountPoint := byteToString(stat.Mntonname[:]) + fsType := byteToString(stat.Fstypename[:]) + + if len(device) == 0 { + continue + } + + d := Mount{ + Device: device, + Mountpoint: mountPoint, + Fstype: fsType, + Type: fsType, + Opts: opts, + Metadata: stat, + Total: stat.Blocks * uint64(stat.Bsize), + Free: stat.Bavail * uint64(stat.Bsize), + Used: (stat.Blocks - stat.Bfree) * uint64(stat.Bsize), + Inodes: stat.Files, + InodesFree: stat.Ffree, + InodesUsed: stat.Files - stat.Ffree, + Blocks: stat.Blocks, + BlockSize: uint64(stat.Bsize), + } + d.DeviceType = deviceType(d) + + ret = append(ret, d) + } + + return ret, warnings, nil +} diff --git a/pkg/fs/duf/mounts_freebsd.go b/pkg/fs/duf/mounts_freebsd.go new file mode 100644 index 000000000..ba646a754 --- /dev/null +++ b/pkg/fs/duf/mounts_freebsd.go @@ -0,0 +1,107 @@ +//go:build freebsd + +package duf + +import ( + "golang.org/x/sys/unix" +) + +func (m *Mount) Stat() unix.Statfs_t { + return m.Metadata.(unix.Statfs_t) +} + +func mounts() ([]Mount, []string, error) { + var ret []Mount + var warnings []string + + count, err := unix.Getfsstat(nil, unix.MNT_WAIT) + if err != nil { + return nil, nil, err + } + fs := make([]unix.Statfs_t, count) + if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { + return nil, nil, err + } + + for _, stat := range fs { + opts := "rw" + if stat.Flags&unix.MNT_RDONLY != 0 { + opts = "ro" + } + if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { + opts += ",sync" + } + if stat.Flags&unix.MNT_NOEXEC != 0 { + opts += ",noexec" + } + if stat.Flags&unix.MNT_NOSUID != 0 { + opts += ",nosuid" + } + if stat.Flags&unix.MNT_UNION != 0 { + opts += ",union" + } + if stat.Flags&unix.MNT_ASYNC != 0 { + opts += ",async" + } + if stat.Flags&unix.MNT_SUIDDIR != 0 { + opts += ",suiddir" + } + if stat.Flags&unix.MNT_SOFTDEP != 0 { + opts += ",softdep" + } + if stat.Flags&unix.MNT_NOSYMFOLLOW != 0 { + opts += ",nosymfollow" + } + if stat.Flags&unix.MNT_GJOURNAL != 0 { + opts += ",gjournal" + } + if stat.Flags&unix.MNT_MULTILABEL != 0 { + opts += ",multilabel" + } + if stat.Flags&unix.MNT_ACLS != 0 { + opts += ",acls" + } + if stat.Flags&unix.MNT_NOATIME != 0 { + opts += ",noatime" + } + if stat.Flags&unix.MNT_NOCLUSTERR != 0 { + opts += ",noclusterr" + } + if stat.Flags&unix.MNT_NOCLUSTERW != 0 { + opts += ",noclusterw" + } + if stat.Flags&unix.MNT_NFS4ACLS != 0 { + opts += ",nfsv4acls" + } + + device := byteToString(stat.Mntfromname[:]) + mountPoint := byteToString(stat.Mntonname[:]) + fsType := byteToString(stat.Fstypename[:]) + + if len(device) == 0 { + continue + } + + d := Mount{ + Device: device, + Mountpoint: mountPoint, + Fstype: fsType, + Type: fsType, + Opts: opts, + Metadata: stat, + Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), + Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), + Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), + Inodes: stat.Files, + InodesFree: uint64(stat.Ffree), + InodesUsed: stat.Files - uint64(stat.Ffree), + Blocks: uint64(stat.Blocks), + BlockSize: uint64(stat.Bsize), + } + d.DeviceType = deviceType(d) + + ret = append(ret, d) + } + + return ret, warnings, nil +} diff --git a/pkg/fs/duf/mounts_linux.go b/pkg/fs/duf/mounts_linux.go new file mode 100644 index 000000000..179e163e2 --- /dev/null +++ b/pkg/fs/duf/mounts_linux.go @@ -0,0 +1,171 @@ +//go:build linux + +package duf + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "golang.org/x/sys/unix" +) + +const ( + // A line of self/mountinfo has the following structure: + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) + // + // (0) mount ID: unique identifier of the mount (may be reused after umount). + //mountinfoMountID = 0 + // (1) parent ID: ID of parent (or of self for the top of the mount tree). + //mountinfoParentID = 1 + // (2) major:minor: value of st_dev for files on filesystem. + //mountinfoMajorMinor = 2 + // (3) root: root of the mount within the filesystem. + //mountinfoRoot = 3 + // (4) mount point: mount point relative to the process's root. + mountinfoMountPoint = 4 + // (5) mount options: per mount options. + mountinfoMountOpts = 5 + // (6) optional fields: zero or more fields terminated by "-". + mountinfoOptionalFields = 6 + // (7) separator between optional fields. + //mountinfoSeparator = 7 + // (8) filesystem type: name of filesystem of the form. + mountinfoFsType = 8 + // (9) mount source: filesystem specific information or "none". + mountinfoMountSource = 9 + // (10) super options: per super block options. + //mountinfoSuperOptions = 10 +) + +// Stat returns the mountpoint's stat information. +func (m *Mount) Stat() unix.Statfs_t { + return m.Metadata.(unix.Statfs_t) +} + +func mounts() ([]Mount, []string, error) { + var warnings []string + + filename := "/proc/self/mountinfo" + lines, err := readLines(filename) + if err != nil { + return nil, nil, err + } + + ret := make([]Mount, 0, len(lines)) + for _, line := range lines { + nb, fields := parseMountInfoLine(line) + if nb == 0 { + continue + } + + // if the number of fields does not match the structure of mountinfo, + // emit a warning and ignore the line. + if nb < 10 || nb > 11 { + warnings = append(warnings, fmt.Sprintf("found invalid mountinfo line: %s", line)) + continue + } + + // blockDeviceID := fields[mountinfoMountID] + mountPoint := fields[mountinfoMountPoint] + mountOpts := fields[mountinfoMountOpts] + fstype := fields[mountinfoFsType] + device := fields[mountinfoMountSource] + + var stat unix.Statfs_t + err := unix.Statfs(mountPoint, &stat) + if err != nil { + if err != os.ErrPermission { + warnings = append(warnings, fmt.Sprintf("%s: %s", mountPoint, err)) + continue + } + + stat = unix.Statfs_t{} + } + + d := Mount{ + Device: device, + Mountpoint: mountPoint, + Fstype: fstype, + Type: fsTypeMap[int64(stat.Type)], //nolint:unconvert + Opts: mountOpts, + Metadata: stat, + Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), //nolint:unconvert + Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), //nolint:unconvert + Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), //nolint:unconvert + Inodes: stat.Files, + InodesFree: stat.Ffree, + InodesUsed: stat.Files - stat.Ffree, + Blocks: uint64(stat.Blocks), //nolint:unconvert + BlockSize: uint64(stat.Bsize), + } + d.DeviceType = deviceType(d) + + // resolve /dev/mapper/* device names + if strings.HasPrefix(d.Device, "/dev/mapper/") { + re := regexp.MustCompile(`^\/dev\/mapper\/(.*)-(.*)`) + match := re.FindAllStringSubmatch(d.Device, -1) + if len(match) > 0 && len(match[0]) == 3 { + d.Device = filepath.Join("/dev", match[0][1], match[0][2]) + } + } + + ret = append(ret, d) + } + + return ret, warnings, nil +} + +// parseMountInfoLine parses a line of /proc/self/mountinfo and returns the +// amount of parsed fields and their values. +func parseMountInfoLine(line string) (int, [11]string) { + var fields [11]string + + if len(line) == 0 || line[0] == '#' { + // ignore comments and empty lines + return 0, fields + } + + var i int + for _, f := range strings.Fields(line) { + // when parsing the optional fields, loop until we find the separator + if i == mountinfoOptionalFields { + // (6) optional fields: zero or more fields of the form + // "tag[:value]"; see below. + // (7) separator: the end of the optional fields is marked + // by a single hyphen. + if f != "-" { + if fields[i] == "" { + fields[i] += f + } else { + fields[i] += " " + f + } + + // keep reading until we reach the separator + continue + } + + // separator found, continue parsing + i++ + } + + switch i { + case mountinfoMountPoint: + fallthrough + case mountinfoMountSource: + fallthrough + case mountinfoFsType: + fields[i] = unescapeFstab(f) + + default: + fields[i] = f + } + + i++ + } + + return i, fields +} diff --git a/pkg/fs/duf/mounts_linux_test.go b/pkg/fs/duf/mounts_linux_test.go new file mode 100644 index 000000000..45d0615cd --- /dev/null +++ b/pkg/fs/duf/mounts_linux_test.go @@ -0,0 +1,207 @@ +//go:build linux + +package duf + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLinuxMounts(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // Get slice of mounted file systems. + results, warnings, err := mounts() + + // No warnings or errors are expected. + assert.NoError(t, err) + assert.Empty(t, warnings) + + // At least one mount returned? + if len(results) < 1 { + t.Error("at least one result expected") + } else { + // If so, check the first mount for plausibility. + result := results[0] + assert.NotEmpty(t, result.Device) + assert.Equal(t, "local", result.DeviceType) + assert.Equal(t, "/", result.Mountpoint) + assert.NotEmpty(t, result.Fstype) + assert.NotEmpty(t, result.Opts) + assert.NotEmpty(t, result.Total) + assert.NotEmpty(t, result.Used) + assert.NotEmpty(t, result.Free) + assert.NotEmpty(t, result.Inodes) + assert.NotEmpty(t, result.InodesFree) + assert.NotEmpty(t, result.InodesUsed) + assert.NotEmpty(t, result.Blocks) + assert.NotEmpty(t, result.BlockSize) + assert.NotEmpty(t, result.Metadata) + } + }) +} + +func TestGetFields(t *testing.T) { + var tt = []struct { + input string + number int + expected [11]string + }{ + // Empty lines + { + input: "", + number: 0, + }, + { + input: " ", + number: 0, + }, + { + input: " ", + number: 0, + }, + { + input: " ", + number: 0, + }, + + // Comments + { + input: "#", + number: 0, + }, + { + input: "# ", + number: 0, + }, + { + input: "# ", + number: 0, + }, + { + input: "# I'm a lazy dog", + number: 0, + }, + + // Bad fields + { + input: "1 2", + number: 2, + expected: [11]string{"1", "2"}, + }, + { + input: "1 2", + number: 2, + expected: [11]string{"1", "2"}, + }, + { + input: "1 2 3", + number: 3, + expected: [11]string{"1", "2", "3"}, + }, + { + input: "1 2 3 4", + number: 4, + expected: [11]string{"1", "2", "3", "4"}, + }, + + // No optional separator or no options + { + input: "1 2 3 4 5 6 7 NotASeparator 9 10 11", + number: 6, + expected: [11]string{"1", "2", "3", "4", "5", "6", "7 NotASeparator 9 10 11"}, + }, + { + input: "1 2 3 4 5 6 7 8 9 10 11", + number: 6, + expected: [11]string{"1", "2", "3", "4", "5", "6", "7 8 9 10 11"}, + }, + { + input: "1 2 3 4 5 6 - 9 10 11", + number: 11, + expected: [11]string{"1", "2", "3", "4", "5", "6", "", "-", "9", "10", "11"}, + }, + + // Normal mount table line + { + input: "22 27 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw", + number: 11, + expected: [11]string{"22", "27", "0:21", "/", "/proc", "rw,nosuid,nodev,noexec,relatime", "shared:5", "-", "proc", "proc", "rw"}, + }, + { + input: "31 23 0:27 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:9 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot", + number: 11, + expected: [11]string{"31", "23", "0:27", "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "shared:9", "-", "cgroup2", "cgroup2", "rw,nsdelegate,memory_recursiveprot"}, + }, + { + input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 - tmpfs tmpfs", + number: 10, + expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18", "-", "tmpfs", "tmpfs"}, + }, + { + input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 shared:22 - tmpfs tmpfs", + number: 10, + expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18 shared:22", "-", "tmpfs", "tmpfs"}, + }, + { + input: "50 27 0:33 / /tmp rw,nosuid,nodev - tmpfs tmpfs", + number: 10, + expected: [11]string{"50", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "", "-", "tmpfs", "tmpfs"}, + }, + + // Exceptional mount table lines + { + input: "328 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs - rw,inode64", + number: 11, + expected: [11]string{"328", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "-", "rw,inode64"}, + }, + { + input: "330 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs 👾 rw,inode64", + number: 11, + expected: [11]string{"330", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, + }, + { + input: "335 27 0:73 / /mnt/👾 rw,relatime shared:206 - tmpfs 👾 rw,inode64", + number: 11, + expected: [11]string{"335", "27", "0:73", "/", "/mnt/👾", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, + }, + { + input: "509 27 0:78 / /mnt/- rw,relatime shared:223 - tmpfs 👾 rw,inode64", + number: 11, + expected: [11]string{"509", "27", "0:78", "/", "/mnt/-", "rw,relatime", "shared:223", "-", "tmpfs", "👾", "rw,inode64"}, + }, + { + input: "362 27 0:76 / /mnt/a\\040b rw,relatime shared:215 - tmpfs 👾 rw,inode64", + number: 11, + expected: [11]string{"362", "27", "0:76", "/", "/mnt/a b", "rw,relatime", "shared:215", "-", "tmpfs", "👾", "rw,inode64"}, + }, + { + input: "1 2 3:3 / /mnt/\\011 rw shared:7 - tmpfs - rw,inode64", + number: 11, + expected: [11]string{"1", "2", "3:3", "/", "/mnt/\t", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, + }, + { + input: "11 2 3:3 / /mnt/a\\012b rw shared:7 - tmpfs - rw,inode64", + number: 11, + expected: [11]string{"11", "2", "3:3", "/", "/mnt/a\nb", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, + }, + { + input: "111 2 3:3 / /mnt/a\\134b rw shared:7 - tmpfs - rw,inode64", + number: 11, + expected: [11]string{"111", "2", "3:3", "/", "/mnt/a\\b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, + }, + { + input: "1111 2 3:3 / /mnt/a\\042b rw shared:7 - tmpfs - rw,inode64", + number: 11, + expected: [11]string{"1111", "2", "3:3", "/", "/mnt/a\"b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, + }, + } + + for _, tc := range tt { + nb, actual := parseMountInfoLine(tc.input) + if nb != tc.number || !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("\nparseMountInfoLine(%q) == \n(%d) %q, \nexpected (%d) %q", tc.input, nb, actual, tc.number, tc.expected) + } + } +} diff --git a/pkg/fs/duf/mounts_openbsd.go b/pkg/fs/duf/mounts_openbsd.go new file mode 100644 index 000000000..c86823312 --- /dev/null +++ b/pkg/fs/duf/mounts_openbsd.go @@ -0,0 +1,86 @@ +//go:build openbsd + +package duf + +import ( + "golang.org/x/sys/unix" +) + +func (m *Mount) Stat() unix.Statfs_t { + return m.Metadata.(unix.Statfs_t) +} + +func mounts() ([]Mount, []string, error) { + var ret []Mount + var warnings []string + + count, err := unix.Getfsstat(nil, unix.MNT_WAIT) + if err != nil { + return nil, nil, err + } + fs := make([]unix.Statfs_t, count) + if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { + return nil, nil, err + } + + for _, stat := range fs { + opts := "rw" + if stat.F_flags&unix.MNT_RDONLY != 0 { + opts = "ro" + } + if stat.F_flags&unix.MNT_SYNCHRONOUS != 0 { + opts += ",sync" + } + if stat.F_flags&unix.MNT_NOEXEC != 0 { + opts += ",noexec" + } + if stat.F_flags&unix.MNT_NOSUID != 0 { + opts += ",nosuid" + } + if stat.F_flags&unix.MNT_NODEV != 0 { + opts += ",nodev" + } + if stat.F_flags&unix.MNT_ASYNC != 0 { + opts += ",async" + } + if stat.F_flags&unix.MNT_SOFTDEP != 0 { + opts += ",softdep" + } + if stat.F_flags&unix.MNT_NOATIME != 0 { + opts += ",noatime" + } + if stat.F_flags&unix.MNT_WXALLOWED != 0 { + opts += ",wxallowed" + } + + device := byteToString(stat.F_mntfromname[:]) + mountPoint := byteToString(stat.F_mntonname[:]) + fsType := byteToString(stat.F_fstypename[:]) + + if len(device) == 0 { + continue + } + + d := Mount{ + Device: device, + Mountpoint: mountPoint, + Fstype: fsType, + Type: fsType, + Opts: opts, + Metadata: stat, + Total: (uint64(stat.F_blocks) * uint64(stat.F_bsize)), + Free: (uint64(stat.F_bavail) * uint64(stat.F_bsize)), + Used: (uint64(stat.F_blocks) - uint64(stat.F_bfree)) * uint64(stat.F_bsize), + Inodes: stat.F_files, + InodesFree: uint64(stat.F_ffree), + InodesUsed: stat.F_files - uint64(stat.F_ffree), + Blocks: uint64(stat.F_blocks), + BlockSize: uint64(stat.F_bsize), + } + d.DeviceType = deviceType(d) + + ret = append(ret, d) + } + + return ret, warnings, nil +} diff --git a/pkg/fs/duf/mounts_windows.go b/pkg/fs/duf/mounts_windows.go new file mode 100644 index 000000000..06323a229 --- /dev/null +++ b/pkg/fs/duf/mounts_windows.go @@ -0,0 +1,377 @@ +//go:build windows + +package duf + +import ( + "fmt" + "math" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Local devices +const ( + guidBufLen = windows.MAX_PATH + 1 + volumeNameBufLen = windows.MAX_PATH + 1 + rootPathBufLen = windows.MAX_PATH + 1 + fileSystemBufLen = windows.MAX_PATH + 1 +) + +func getMountPoint(guidBuf []uint16) (mountPoint string, err error) { + var rootPathLen uint32 + rootPathBuf := make([]uint16, rootPathBufLen) + + err = windows.GetVolumePathNamesForVolumeName(&guidBuf[0], &rootPathBuf[0], rootPathBufLen*2, &rootPathLen) + if err != nil && err.(windows.Errno) == windows.ERROR_MORE_DATA { + // Retry if buffer size is too small + rootPathBuf = make([]uint16, (rootPathLen+1)/2) + err = windows.GetVolumePathNamesForVolumeName( + &guidBuf[0], &rootPathBuf[0], rootPathLen, &rootPathLen) + } + return windows.UTF16ToString(rootPathBuf), err +} + +func getVolumeInfo(guidOrMountPointBuf []uint16) (volumeName string, fsType string, err error) { + volumeNameBuf := make([]uint16, volumeNameBufLen) + fsTypeBuf := make([]uint16, fileSystemBufLen) + + err = windows.GetVolumeInformation(&guidOrMountPointBuf[0], &volumeNameBuf[0], volumeNameBufLen*2, + nil, nil, nil, + &fsTypeBuf[0], fileSystemBufLen*2) + + return windows.UTF16ToString(volumeNameBuf), windows.UTF16ToString(fsTypeBuf), err +} + +func getSpaceInfo(guidOrMountPointBuf []uint16) (totalBytes uint64, freeBytes uint64, err error) { + err = windows.GetDiskFreeSpaceEx(&guidOrMountPointBuf[0], nil, &totalBytes, &freeBytes) + return +} + +func getClusterInfo(guidOrMountPointBuf []uint16) (totalClusters uint32, clusterSize uint32, err error) { + var sectorsPerCluster uint32 + var bytesPerSector uint32 + err = GetDiskFreeSpace(&guidOrMountPointBuf[0], §orsPerCluster, &bytesPerSector, nil, &totalClusters) + clusterSize = bytesPerSector * sectorsPerCluster + return +} + +func getMount(guidOrMountPointBuf []uint16, isGUID bool) (m Mount, skip bool, warnings []string) { + var err error + guidOrMountPoint := windows.UTF16ToString(guidOrMountPointBuf) + + mountPoint := guidOrMountPoint + if isGUID { + mountPoint, err = getMountPoint(guidOrMountPointBuf) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) + } + // Skip unmounted volumes + if len(mountPoint) == 0 { + skip = true + return + } + } + + // Get volume name & filesystem type + volumeName, fsType, err := getVolumeInfo(guidOrMountPointBuf) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) + } + + // Get space info + totalBytes, freeBytes, err := getSpaceInfo(guidOrMountPointBuf) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) + } + + // Get cluster info + totalClusters, clusterSize, err := getClusterInfo(guidOrMountPointBuf) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) + } + + m = Mount{ + Device: volumeName, + Mountpoint: mountPoint, + Fstype: fsType, + Type: fsType, + Opts: "", + Total: totalBytes, + Free: freeBytes, + Used: totalBytes - freeBytes, + Blocks: uint64(totalClusters), + BlockSize: uint64(clusterSize), + } + m.DeviceType = deviceType(m) + return +} + +func getMountFromGUID(guidBuf []uint16) (m Mount, skip bool, warnings []string) { + m, skip, warnings = getMount(guidBuf, true) + + // Use GUID as volume name if no label was set + if len(m.Device) == 0 { + m.Device = windows.UTF16ToString(guidBuf) + } + + return +} + +func getMountFromMountPoint(mountPointBuf []uint16) (m Mount, warnings []string) { + m, _, warnings = getMount(mountPointBuf, false) + + // Use mount point as volume name if no label was set + if len(m.Device) == 0 { + m.Device = windows.UTF16ToString(mountPointBuf) + } + + return m, warnings +} + +func appendLocalMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { + guidBuf := make([]uint16, guidBufLen) + + hFindVolume, err := windows.FindFirstVolume(&guidBuf[0], guidBufLen*2) + if err != nil { + return mounts, warnings, err + } + +VolumeLoop: + for ; ; err = windows.FindNextVolume(hFindVolume, &guidBuf[0], guidBufLen*2) { + if err != nil { + switch err.(windows.Errno) { + case windows.ERROR_NO_MORE_FILES: + break VolumeLoop + default: + warnings = append(warnings, fmt.Sprintf("%s: %s", windows.UTF16ToString(guidBuf), err)) + continue VolumeLoop + } + } + + if m, skip, w := getMountFromGUID(guidBuf); !skip { + mounts = append(mounts, m) + warnings = append(warnings, w...) + } + } + + if err = windows.FindVolumeClose(hFindVolume); err != nil { + warnings = append(warnings, fmt.Sprintf("%s", err)) + } + return mounts, warnings, nil +} + +// Network devices +func getMountFromNetResource(netResource NetResource) (m Mount, warnings []string) { + mountPoint := windows.UTF16PtrToString(netResource.LocalName) + if !strings.HasSuffix(mountPoint, string(filepath.Separator)) { + mountPoint += string(filepath.Separator) + } + mountPointBuf := windows.StringToUTF16(mountPoint) + + m, _, warnings = getMount(mountPointBuf, false) + + // Use remote name as volume name if no label was set + if len(m.Device) == 0 { + m.Device = windows.UTF16PtrToString(netResource.RemoteName) + } + + return +} + +func appendNetworkMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { + hEnumResource, err := WNetOpenEnum(RESOURCE_CONNECTED, RESOURCETYPE_DISK, RESOURCEUSAGE_CONNECTABLE, nil) + if err != nil { + return mounts, warnings, err + } + +EnumLoop: + for { + // Reference: https://docs.microsoft.com/en-us/windows/win32/wnet/enumerating-network-resources + var nrBuf [16384]byte + count := uint32(math.MaxUint32) + size := uint32(len(nrBuf)) + if err := WNetEnumResource(hEnumResource, &count, &nrBuf[0], &size); err != nil { + switch err.(windows.Errno) { + case windows.ERROR_NO_MORE_ITEMS: + break EnumLoop + default: + warnings = append(warnings, err.Error()) + break EnumLoop + } + } + + for i := uint32(0); i < count; i++ { + nr := (*NetResource)(unsafe.Pointer(&nrBuf[uintptr(i)*NetResourceSize])) + m, w := getMountFromNetResource(*nr) + mounts = append(mounts, m) + warnings = append(warnings, w...) + } + } + + if err = WNetCloseEnum(hEnumResource); err != nil { + warnings = append(warnings, fmt.Sprintf("%s", err)) + } + return mounts, warnings, nil +} + +func mountPointAlreadyPresent(mounts []Mount, mountPoint string) bool { + for _, m := range mounts { + if m.Mountpoint == mountPoint { + return true + } + } + + return false +} + +func appendLogicalDrives(mounts []Mount, warnings []string) ([]Mount, []string) { + driveBitmap, err := windows.GetLogicalDrives() + if err != nil { + warnings = append(warnings, fmt.Sprintf("GetLogicalDrives(): %s", err)) + return mounts, warnings + } + + for drive := 'A'; drive <= 'Z'; drive, driveBitmap = drive+1, driveBitmap>>1 { + if driveBitmap&0x1 == 0 { + continue + } + + mountPoint := fmt.Sprintf("%c:\\", drive) + if mountPointAlreadyPresent(mounts, mountPoint) { + continue + } + + mountPointBuf := windows.StringToUTF16(mountPoint) + m, w := getMountFromMountPoint(mountPointBuf) + mounts = append(mounts, m) + warnings = append(warnings, w...) + } + + return mounts, warnings +} + +func mounts() (ret []Mount, warnings []string, err error) { + ret = make([]Mount, 0) + + // Local devices + if ret, warnings, err = appendLocalMounts(ret, warnings); err != nil { + return + } + + // Network devices + if ret, warnings, err = appendNetworkMounts(ret, warnings); err != nil { + return + } + + // Logical devices (from GetLogicalDrives bitflag) + // Check any possible logical drives, in case of some special virtual devices, such as RAM disk + ret, warnings = appendLogicalDrives(ret, warnings) + + return ret, warnings, nil +} + +// Windows API +const ( + // Windows Networking const + // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw + RESOURCE_CONNECTED = 0x00000001 + RESOURCE_GLOBALNET = 0x00000002 + RESOURCE_REMEMBERED = 0x00000003 + RESOURCE_RECENT = 0x00000004 + RESOURCE_CONTEXT = 0x00000005 + + RESOURCETYPE_ANY = 0x00000000 + RESOURCETYPE_DISK = 0x00000001 + RESOURCETYPE_PRINT = 0x00000002 + RESOURCETYPE_RESERVED = 0x00000008 + RESOURCETYPE_UNKNOWN = 0xFFFFFFFF + + RESOURCEUSAGE_CONNECTABLE = 0x00000001 + RESOURCEUSAGE_CONTAINER = 0x00000002 + RESOURCEUSAGE_NOLOCALDEVICE = 0x00000004 + RESOURCEUSAGE_SIBLING = 0x00000008 + RESOURCEUSAGE_ATTACHED = 0x00000010 + RESOURCEUSAGE_ALL = RESOURCEUSAGE_CONNECTABLE | RESOURCEUSAGE_CONTAINER | RESOURCEUSAGE_ATTACHED + RESOURCEUSAGE_RESERVED = 0x80000000 +) + +var ( + // Windows syscall + modmpr = windows.NewLazySystemDLL("mpr.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + + procWNetOpenEnumW = modmpr.NewProc("WNetOpenEnumW") + procWNetCloseEnum = modmpr.NewProc("WNetCloseEnum") + procWNetEnumResourceW = modmpr.NewProc("WNetEnumResourceW") + procGetDiskFreeSpaceW = modkernel32.NewProc("GetDiskFreeSpaceW") + + NetResourceSize = unsafe.Sizeof(NetResource{}) +) + +// Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/ns-winnetwk-netresourcew +type NetResource struct { + Scope uint32 + Type uint32 + DisplayType uint32 + Usage uint32 + LocalName *uint16 + RemoteName *uint16 + Comment *uint16 + Provider *uint16 +} + +// Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw +func WNetOpenEnum(scope uint32, resourceType uint32, usage uint32, resource *NetResource) (handle windows.Handle, err error) { + r1, _, e1 := syscall.Syscall6(procWNetOpenEnumW.Addr(), 5, uintptr(scope), uintptr(resourceType), uintptr(usage), uintptr(unsafe.Pointer(resource)), uintptr(unsafe.Pointer(&handle)), 0) + if r1 != windows.NO_ERROR { + if e1 != 0 { + err = e1 + } else { + err = syscall.EINVAL + } + } + return +} + +// Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetenumresourcew +func WNetEnumResource(enumResource windows.Handle, count *uint32, buffer *byte, bufferSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procWNetEnumResourceW.Addr(), 4, uintptr(enumResource), uintptr(unsafe.Pointer(count)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferSize)), 0, 0) + if r1 != windows.NO_ERROR { + if e1 != 0 { + err = e1 + } else { + err = syscall.EINVAL + } + } + return +} + +// Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetcloseenum +func WNetCloseEnum(enumResource windows.Handle) (err error) { + r1, _, e1 := syscall.Syscall(procWNetCloseEnum.Addr(), 1, uintptr(enumResource), 0, 0) + if r1 != windows.NO_ERROR { + if e1 != 0 { + err = e1 + } else { + err = syscall.EINVAL + } + } + return +} + +// Reference: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdiskfreespacew +func GetDiskFreeSpace(directoryName *uint16, sectorsPerCluster *uint32, bytesPerSector *uint32, numberOfFreeClusters *uint32, totalNumberOfClusters *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procGetDiskFreeSpaceW.Addr(), 5, uintptr(unsafe.Pointer(directoryName)), uintptr(unsafe.Pointer(sectorsPerCluster)), uintptr(unsafe.Pointer(bytesPerSector)), uintptr(unsafe.Pointer(numberOfFreeClusters)), uintptr(unsafe.Pointer(totalNumberOfClusters)), 0) + if r1 == 0 { + if e1 != 0 { + err = e1 + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/pkg/fs/duf/util.go b/pkg/fs/duf/util.go new file mode 100644 index 000000000..6237189a6 --- /dev/null +++ b/pkg/fs/duf/util.go @@ -0,0 +1,53 @@ +package duf + +import ( + "fmt" + "strings" + + "github.com/IGLOU-EU/go-wildcard" +) + +// parseCommaSeparatedValues parses comma separated string into a map. +func parseCommaSeparatedValues(values string) map[string]struct{} { + m := make(map[string]struct{}) + for _, v := range strings.Split(values, ",") { + v = strings.TrimSpace(v) + if len(v) == 0 { + continue + } + + v = strings.ToLower(v) + m[v] = struct{}{} + } + return m +} + +// validateGroups validates the parsed group maps. +func validateGroups(m map[string]struct{}) error { + for k := range m { + found := false + for _, g := range groups { + if g == k { + found = true + break + } + } + + if !found { + return fmt.Errorf("unknown device group: %s", k) + } + } + + return nil +} + +// findInKey parse a slice of pattern to match the given key. +func findInKey(str string, km map[string]struct{}) bool { + for p := range km { + if wildcard.Match(p, str) { + return true + } + } + + return false +}