FS: Add /pkg/fs/duf to determine mount points and disk usage #4266

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-03-03 11:24:30 +01:00
parent 5c1b0ba96b
commit e1a9a68b29
23 changed files with 1983 additions and 11 deletions

7
go.mod
View File

@@ -78,6 +78,7 @@ require github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082
require golang.org/x/text v0.22.0 require golang.org/x/text v0.22.0
require ( require (
github.com/IGLOU-EU/go-wildcard v1.0.3
github.com/davidbyttow/govips/v2 v2.16.0 github.com/davidbyttow/govips/v2 v2.16.0
github.com/go-co-op/gocron/v2 v2.16.0 github.com/go-co-op/gocron/v2 v2.16.0
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
@@ -86,6 +87,7 @@ require (
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/urfave/cli/v2 v2.27.5 github.com/urfave/cli/v2 v2.27.5
github.com/zitadel/oidc/v3 v3.35.0 github.com/zitadel/oidc/v3 v3.35.0
golang.org/x/sys v0.30.0
) )
require ( require (
@@ -130,7 +132,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/procfs v0.15.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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/swaggo/swag v1.16.3 // indirect github.com/swaggo/swag v1.16.3 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
@@ -144,7 +146,6 @@ require (
go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/oauth2 v0.26.0 // 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 golang.org/x/tools v0.30.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -162,7 +163,7 @@ require (
require ( require (
github.com/emersion/go-webdav v0.6.0 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 github.com/tidwall/pretty v1.2.1 // indirect
) )

10
go.sum
View File

@@ -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/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/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/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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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.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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 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= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

View File

@@ -1,8 +1,8 @@
package fs package fs
// KiloByte, MegaByte, and GigaByte size constants. // Size constants for KByte, MByte, and GByte.
const ( const (
KiloByte = 1024 KB = 1024
MegaByte = KiloByte * 1024 MB = KB * 1024
GigaByte = MegaByte * 1024 GB = MB * 1024
) )

18
pkg/fs/duf/const.go Normal file
View File

@@ -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
)

40
pkg/fs/duf/duf.go Normal file
View File

@@ -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"):
<https://docs.photoprism.app/license/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:
<https://www.photoprism.app/trademark>
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:
<https://docs.photoprism.app/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()
}

40
pkg/fs/duf/duf_test.go Normal file
View File

@@ -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)
}
})
}

68
pkg/fs/duf/filesystems.go Normal file
View File

@@ -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" ]

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

23
pkg/fs/duf/filters.go Normal file
View File

@@ -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{}
}

113
pkg/fs/duf/groups.go Normal file
View File

@@ -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
}

46
pkg/fs/duf/groups_test.go Normal file
View File

@@ -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")
}
})
}

92
pkg/fs/duf/mounts.go Normal file
View File

@@ -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])
}

View File

@@ -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
}

View File

@@ -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
}

171
pkg/fs/duf/mounts_linux.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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], &sectorsPerCluster, &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
}

53
pkg/fs/duf/util.go Normal file
View File

@@ -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
}