//go:build !windows && !darwin package internal import ( "bufio" "bytes" "math" "net/url" "os" "sync/atomic" "golang.org/x/sys/unix" ) // The mounts could change anytime, but probably won't all too often. So we // read them each time Mount() is called, but remember the count, so we can // at least prepare a buffer of that size. This must only be accessed atomicly. var lastMountCount int32 // Mounts reads /proc/mounts and returns all mounts found, excluding // everything on the devices sysfs, rootfs, cgroup and /dev/ paths. func Mounts() ([]string, error) { handle, err := os.Open("/proc/mounts") if err != nil { return nil, err } defer handle.Close() scanner := bufio.NewScanner(handle) // I just assume that entries won't be much longer than 200, since my // own /proc/mounts was max 157 characters. Worst case, we'll probably // expand to 400. Either way, this should handle most cases efficiently. buffer := make([]byte, 0, 200) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexByte(data, '\n'); i >= 0 { if firstSpace := bytes.IndexByte(data, ' '); firstSpace >= 0 { // Some device won't contain a trash either way or might be // dangerous to interact with. device := string(data[:firstSpace]) switch device { case "rootfs", "sysfs", "cgroup", "cgroup2": // Skip line return i + 1, nil, nil } if nextSpace := bytes.IndexByte(data[firstSpace+1:], ' '); nextSpace >= 0 { path := data[firstSpace+1 : firstSpace+1+nextSpace] // Devices don't usually contain files and /sys should // be off-limits anyway. if bytes.HasPrefix(path, []byte("/dev/")) || bytes.HasPrefix(path, []byte("/sys/")) { return i + 1, nil, nil } return i + 1, path, nil } } return i + 1, nil, nil } // If we're at EOF, we have a final, non-terminated line. Return it. if atEOF { return len(data), data, nil } // Request more data. return 0, nil, nil }) scanner.Buffer(buffer, math.MaxInt) mounts := make([]string, 0, atomic.LoadInt32(&lastMountCount)) for scanner.Scan() { mounts = append(mounts, scanner.Text()) } atomic.StoreInt32(&lastMountCount, int32(len(mounts))) return mounts, nil } // RemoveAllIfExists tries to remove the given path. The path can either be a // directory or a file. This function catches certain errors, so you don't have // to. func RemoveAllIfExists(path string) error { RETRY: if err := os.RemoveAll(path); err != nil { pathErr, ok := err.(*os.PathError) if ok { switch pathErr.Err { case unix.EINTR: // A non-error basically which tells you to try again. // Use goto in order to prevent growing stack. goto RETRY case unix.ENOENT: // Does not exist. return nil case unix.ENOTDIR: // Not a directory or file. This happens on WSL when // attempting to delete in /mnt/wslg/versions.txt, which is // weird considering that os.Remove (used internally) can // delete files. return nil case unix.EROFS: // Occurs if the filesystem is read-only. return nil } } return err } return nil } // EscapeUrl escapes the path according to the FreeDesktop Trash specification. // Which basically just refers to "RFC 2396, section 2". func EscapeUrl(path string) string { u := &url.URL{Path: path} return u.EscapedPath() } // FileExists omits the parts to make this usable cross-platform and // therefore saves a minimal amount of CPU cycles and some allocations. func FileExists(path string) (bool, error) { var ( stat unix.Stat_t err error ) RETRY: for { err = unix.Stat(path, &stat) switch err { case nil: // No issue, exists return true, nil case unix.EINTR: // A non-error basically which tells you to try again. continue RETRY case unix.ENOENT: // Doesn't exist return false, nil default: // Unexpected error return false, err } } }