azure-storage-fuse/common/util.go

503 строки
12 KiB
Go

/*
_____ _____ _____ ____ ______ _____ ------
| | | | | | | | | | | | |
| | | | | | | | | | | | |
| --- | | | | |-----| |---- | | |-----| |----- ------
| | | | | | | | | | | | |
| ____| |_____ | ____| | ____| | |_____| _____| |_____ |_____
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
Copyright © 2020-2024 Microsoft Corporation. All rights reserved.
Author : <blobfusedev@microsoft.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/
package common
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"gopkg.in/ini.v1"
)
var RootMount bool
var ForegroundMount bool
var IsStream bool
// IsDirectoryMounted is a utility function that returns true if the directory is already mounted using fuse
func IsDirectoryMounted(path string) bool {
mntList, err := os.ReadFile("/etc/mtab")
if err != nil {
//fmt.Println("failed to read mount points : ", err.Error())
return false
}
// removing trailing / from the path
path = strings.TrimRight(path, "/")
for _, line := range strings.Split(string(mntList), "\n") {
if strings.TrimSpace(line) != "" {
mntPoint := strings.Split(line, " ")[1]
if path == mntPoint {
// with earlier fuse driver ' fuse.' was searched in /etc/mtab
// however with libfuse entry does not have that signature
// if this path is already mounted using fuse then fail
if strings.Contains(line, "fuse") {
//fmt.Println(path, " is already mounted.")
return true
}
}
}
}
return false
}
func IsMountActive(path string) (bool, error) {
// Get the process details for this path using ps -aux
var out bytes.Buffer
cmd := exec.Command("pidof", "blobfuse2")
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
if err.Error() == "exit status 1" {
return false, nil
} else {
return true, fmt.Errorf("failed to get pid of blobfuse2 [%v]", err.Error())
}
}
// out contains the list of pids of the processes that are running
pidString := strings.Replace(out.String(), "\n", " ", -1)
pids := strings.Split(pidString, " ")
for _, pid := range pids {
// Get the mount path for this pid
// For this we need to check the command line arguments given to this command
// If the path is same then we need to return true
if pid == "" {
continue
}
cmd = exec.Command("ps", "-o", "args=", "-p", pid)
out.Reset()
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return true, fmt.Errorf("failed to get command line arguments for pid %s [%v]", pid, err.Error())
}
if strings.Contains(out.String(), path) {
return true, nil
}
}
return false, nil
}
// IsDirectoryEmpty is a utility function that returns true if the directory at that path is empty or not
func IsDirectoryEmpty(path string) bool {
if !DirectoryExists(path) {
// Directory does not exists so safe to assume its empty
return true
}
f, _ := os.Open(path)
defer f.Close()
_, err := f.Readdirnames(1)
// If there is nothing in the directory then it is empty
return err == io.EOF
}
func TempCacheCleanup(path string) error {
if !IsDirectoryEmpty(path) {
// List the first level children of the directory
dirents, err := os.ReadDir(path)
if err != nil {
// Failed to list, return back error
return fmt.Errorf("failed to list directory contents : %s", err.Error())
}
// Delete all first level children with their hierarchy
for _, entry := range dirents {
os.RemoveAll(filepath.Join(path, entry.Name()))
}
}
return nil
}
// DirectoryExists is a utility function that returns true if the directory at that path exists and returns false if it does not exist.
func DirectoryExists(path string) bool {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return false
} else if err != nil {
return false
}
return true
}
// GetCurrentUser is a utility function that returns the UID and GID of the user that invokes the blobfuse2 command.
func GetCurrentUser() (uint32, uint32, error) {
var (
currentUser *user.User
userUID, userGID uint64
)
currentUser, err := user.Current()
if err != nil {
return 0, 0, err
}
userUID, err = strconv.ParseUint(currentUser.Uid, 10, 32)
if err != nil {
return 0, 0, err
}
userGID, err = strconv.ParseUint(currentUser.Gid, 10, 32)
if err != nil {
return 0, 0, err
}
if currentUser.Name == "root" || userUID == 0 {
RootMount = true
} else {
RootMount = false
}
return uint32(userUID), uint32(userGID), nil
}
// normalizeObjectName : If file contains \\ in name replace it with ..
func NormalizeObjectName(name string) string {
return strings.ReplaceAll(name, "\\", "/")
}
// List all mount points which were mounted using blobfuse2
func ListMountPoints() ([]string, error) {
file, err := os.Open("/etc/mtab")
if err != nil {
return nil, err
}
defer file.Close()
// Read /etc/mtab file line by line
var mntList []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// If there is any directory mounted using blobfuse2 its of our interest
if strings.HasPrefix(line, "blobfuse2") {
// Extract the mount path from this line
mntPath := strings.Split(line, " ")[1]
mntList = append(mntList, mntPath)
}
}
return mntList, nil
}
// Encrypt given data using the key provided
func EncryptData(plainData []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, plainData, nil)
return ciphertext, nil
}
// Decrypt given data using the key provided
func DecryptData(cipherData []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := cipherData[:gcm.NonceSize()]
ciphertext := cipherData[gcm.NonceSize():]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func GetCurrentDistro() string {
cfg, err := ini.Load("/etc/os-release")
if err != nil {
return ""
}
distro := cfg.Section("").Key("PRETTY_NAME").String()
return distro
}
type BitMap16 uint16
// IsSet : Check whether the given bit is set or not
func (bm BitMap16) IsSet(bit uint16) bool { return (bm & (1 << bit)) != 0 }
// Set : Set the given bit in bitmap
func (bm *BitMap16) Set(bit uint16) { *bm |= (1 << bit) }
// Clear : Clear the given bit from bitmap
func (bm *BitMap16) Clear(bit uint16) { *bm &= ^(1 << bit) }
// Reset : Reset the whole bitmap by setting it to 0
func (bm *BitMap16) Reset() { *bm = 0 }
type KeyedMutex struct {
mutexes sync.Map // Zero value is empty and ready for use
}
func (m *KeyedMutex) GetLock(key string) *sync.Mutex {
value, _ := m.mutexes.LoadOrStore(key, &sync.Mutex{})
mtx := value.(*sync.Mutex)
return mtx
}
// check if health monitor is enabled and blofuse stats monitor is not disabled
func MonitorBfs() bool {
return EnableMonitoring && !BfsDisabled
}
// convert ~ to $HOME in path
func ExpandPath(path string) string {
if path == "" {
return path
}
if strings.HasPrefix(path, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
return path
}
path = filepath.Join(homeDir, path[2:])
}
path = os.ExpandEnv(path)
path, _ = filepath.Abs(path)
return path
}
// NotifyMountToParent : Send a signal to parent process about successful mount
func NotifyMountToParent() error {
if !ForegroundMount {
ppid := syscall.Getppid()
if ppid > 1 {
if err := syscall.Kill(ppid, syscall.SIGUSR2); err != nil {
return err
}
} else {
return fmt.Errorf("failed to get parent pid, received : %v", ppid)
}
}
return nil
}
var duPath []string = []string{"/usr/bin/du", "/usr/local/bin/du", "/usr/sbin/du", "/usr/local/sbin/du", "/sbin/du", "/bin/du"}
var selectedDuPath string = ""
// GetUsage: The current disk usage in MB
func GetUsage(path string) (float64, error) {
var currSize float64
var out bytes.Buffer
if selectedDuPath == "" {
selectedDuPath = "-"
for _, dup := range duPath {
_, err := os.Stat(dup)
if err == nil {
selectedDuPath = dup
break
}
}
}
if selectedDuPath == "-" {
return 0, fmt.Errorf("failed to find du")
}
// du - estimates file space usage
// https://man7.org/linux/man-pages/man1/du.1.html
// Note: We cannot just pass -BM as a parameter here since it will result in less accurate estimates of the size of the path
// (i.e. du will round up to 1M if the path is smaller than 1M).
cmd := exec.Command(selectedDuPath, "-sh", path)
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return 0, err
}
size := strings.Split(out.String(), "\t")[0]
if size == "0" {
return 0, nil
}
// some OS's use "," instead of "." that will not work for float parsing - replace it
size = strings.Replace(size, ",", ".", 1)
parsed, err := strconv.ParseFloat(size[:len(size)-1], 64)
if err != nil {
return 0, fmt.Errorf("failed to parse du output")
}
switch size[len(size)-1] {
case 'K':
currSize = parsed / float64(1024)
case 'M':
currSize = parsed
case 'G':
currSize = parsed * 1024
case 'T':
currSize = parsed * 1024 * 1024
}
return currSize, nil
}
var currentUID int = -1
// GetDiskUsageFromStatfs: Current disk usage of temp path
func GetDiskUsageFromStatfs(path string) (float64, float64, error) {
// We need to compute the disk usage percentage for the temp path
var stat syscall.Statfs_t
err := syscall.Statfs(path, &stat)
if err != nil {
return 0, 0, err
}
if currentUID == -1 {
currentUID = os.Getuid()
}
var availableSpace uint64
if currentUID == 0 {
// Sudo has mounted
availableSpace = stat.Bfree * uint64(stat.Frsize)
} else {
// non Sudo has mounted
availableSpace = stat.Bavail * uint64(stat.Frsize)
}
totalSpace := stat.Blocks * uint64(stat.Frsize)
usedSpace := float64(totalSpace - availableSpace)
return usedSpace, float64(usedSpace) / float64(totalSpace) * 100, nil
}
func GetFuseMinorVersion() int {
var out bytes.Buffer
cmd := exec.Command("fusermount3", "--version")
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return 0
}
output := strings.Split(out.String(), ":")
if len(output) < 2 {
return 0
}
version := strings.Trim(output[1], " ")
if version == "" {
return 0
}
output = strings.Split(version, ".")
if len(output) < 2 {
return 0
}
val, err := strconv.Atoi(output[1])
if err != nil {
return 0
}
return val
}
type WriteToFileOptions struct {
Flags int
Permission os.FileMode
}
func WriteToFile(filename string, data string, options WriteToFileOptions) error {
// Open the file with the provided flags, create it if it doesn't exist
//check if options.Permission is 0 if so then assign 0777
if options.Permission == 0 {
options.Permission = 0777
}
file, err := os.OpenFile(filename, options.Flags|os.O_CREATE|os.O_WRONLY, options.Permission)
if err != nil {
return fmt.Errorf("error opening file: [%s]", err.Error())
}
defer file.Close() // Ensure the file is closed when we're done
// Write the data content to the file
if _, err := file.WriteString(data); err != nil {
return fmt.Errorf("error writing to file [%s]", err.Error())
}
return nil
}