2022-02-09 19:42:28 +03:00
|
|
|
/*
|
|
|
|
_____ _____ _____ ____ ______ _____ ------
|
|
|
|
| | | | | | | | | | | | |
|
|
|
|
| | | | | | | | | | | | |
|
|
|
|
| --- | | | | |-----| |---- | | |-----| |----- ------
|
|
|
|
| | | | | | | | | | | | |
|
|
|
|
| ____| |_____ | ____| | ____| | |_____| _____| |_____ |_____
|
|
|
|
|
|
|
|
|
|
|
|
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
|
|
|
|
2024-01-02 13:10:39 +03:00
|
|
|
Copyright © 2020-2024 Microsoft Corporation. All rights reserved.
|
2022-02-09 19:42:28 +03:00
|
|
|
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"
|
2023-08-25 09:50:34 +03:00
|
|
|
"bytes"
|
2022-02-09 19:42:28 +03:00
|
|
|
"crypto/aes"
|
|
|
|
"crypto/cipher"
|
|
|
|
"crypto/rand"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2023-08-25 09:50:34 +03:00
|
|
|
"os/exec"
|
2022-02-09 19:42:28 +03:00
|
|
|
"os/user"
|
2022-09-01 17:00:08 +03:00
|
|
|
"path/filepath"
|
2022-02-09 19:42:28 +03:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-06-24 23:50:01 +03:00
|
|
|
"sync"
|
2023-03-22 07:59:36 +03:00
|
|
|
"syscall"
|
2022-02-09 19:42:28 +03:00
|
|
|
|
|
|
|
"gopkg.in/ini.v1"
|
|
|
|
)
|
|
|
|
|
|
|
|
var RootMount bool
|
2023-03-22 07:59:36 +03:00
|
|
|
var ForegroundMount bool
|
2024-11-05 09:08:27 +03:00
|
|
|
var IsStream bool
|
2022-02-09 19:42:28 +03:00
|
|
|
|
2023-05-10 12:23:05 +03:00
|
|
|
// IsDirectoryMounted is a utility function that returns true if the directory is already mounted using fuse
|
2022-02-09 19:42:28 +03:00
|
|
|
func IsDirectoryMounted(path string) bool {
|
2023-05-10 12:23:05 +03:00
|
|
|
mntList, err := os.ReadFile("/etc/mtab")
|
2022-02-09 19:42:28 +03:00
|
|
|
if err != nil {
|
|
|
|
//fmt.Println("failed to read mount points : ", err.Error())
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-04-01 08:10:49 +03:00
|
|
|
// removing trailing / from the path
|
|
|
|
path = strings.TrimRight(path, "/")
|
|
|
|
|
2022-02-09 19:42:28 +03:00
|
|
|
for _, line := range strings.Split(string(mntList), "\n") {
|
|
|
|
if strings.TrimSpace(line) != "" {
|
|
|
|
mntPoint := strings.Split(line, " ")[1]
|
|
|
|
if path == mntPoint {
|
2022-03-10 06:48:05 +03:00
|
|
|
// 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") {
|
2022-02-09 19:42:28 +03:00
|
|
|
//fmt.Println(path, " is already mounted.")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-07-17 05:46:58 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-05-10 12:23:05 +03:00
|
|
|
// IsDirectoryEmpty is a utility function that returns true if the directory at that path is empty or not
|
2022-02-09 19:42:28 +03:00
|
|
|
func IsDirectoryEmpty(path string) bool {
|
2024-07-17 05:46:58 +03:00
|
|
|
if !DirectoryExists(path) {
|
|
|
|
// Directory does not exists so safe to assume its empty
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-02-09 19:42:28 +03:00
|
|
|
f, _ := os.Open(path)
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
_, err := f.Readdirnames(1)
|
2024-07-17 05:46:58 +03:00
|
|
|
// 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())
|
|
|
|
}
|
2022-02-09 19:42:28 +03:00
|
|
|
|
2024-07-17 05:46:58 +03:00
|
|
|
// Delete all first level children with their hierarchy
|
|
|
|
for _, entry := range dirents {
|
|
|
|
os.RemoveAll(filepath.Join(path, entry.Name()))
|
|
|
|
}
|
2022-02-09 19:42:28 +03:00
|
|
|
}
|
|
|
|
|
2024-07-17 05:46:58 +03:00
|
|
|
return nil
|
2022-02-09 19:42:28 +03:00
|
|
|
}
|
|
|
|
|
2023-05-10 12:23:05 +03:00
|
|
|
// DirectoryExists is a utility function that returns true if the directory at that path exists and returns false if it does not exist.
|
2022-02-09 19:42:28 +03:00
|
|
|
func DirectoryExists(path string) bool {
|
|
|
|
_, err := os.Stat(path)
|
|
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return false
|
|
|
|
} else if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-05-10 12:23:05 +03:00
|
|
|
// GetCurrentUser is a utility function that returns the UID and GID of the user that invokes the blobfuse2 command.
|
2022-02-09 19:42:28 +03:00
|
|
|
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
|
|
|
|
}
|
2022-09-08 23:09:32 +03:00
|
|
|
|
2022-02-09 19:42:28 +03:00
|
|
|
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
|
|
|
|
}
|
2022-02-19 13:26:43 +03:00
|
|
|
|
|
|
|
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) }
|
2022-06-24 23:50:01 +03:00
|
|
|
|
2024-01-22 15:08:30 +03:00
|
|
|
// Reset : Reset the whole bitmap by setting it to 0
|
|
|
|
func (bm *BitMap16) Reset() { *bm = 0 }
|
|
|
|
|
2022-06-24 23:50:01 +03:00
|
|
|
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
|
|
|
|
}
|
2022-08-27 17:17:39 +03:00
|
|
|
|
2022-09-08 23:09:32 +03:00
|
|
|
// check if health monitor is enabled and blofuse stats monitor is not disabled
|
2022-08-27 17:17:39 +03:00
|
|
|
func MonitorBfs() bool {
|
|
|
|
return EnableMonitoring && !BfsDisabled
|
|
|
|
}
|
2022-09-01 17:00:08 +03:00
|
|
|
|
|
|
|
// convert ~ to $HOME in path
|
|
|
|
func ExpandPath(path string) string {
|
2023-05-10 13:41:32 +03:00
|
|
|
if path == "" {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
2022-09-01 17:00:08 +03:00
|
|
|
if strings.HasPrefix(path, "~/") {
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
path = filepath.Join(homeDir, path[2:])
|
|
|
|
}
|
|
|
|
|
2023-05-10 13:41:32 +03:00
|
|
|
path = os.ExpandEnv(path)
|
|
|
|
path, _ = filepath.Abs(path)
|
|
|
|
return path
|
2022-09-01 17:00:08 +03:00
|
|
|
}
|
2023-03-22 07:59:36 +03:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2023-08-25 09:50:34 +03:00
|
|
|
|
|
|
|
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 = ""
|
|
|
|
|
2023-08-28 15:00:42 +03:00
|
|
|
// GetUsage: The current disk usage in MB
|
2023-08-25 09:50:34 +03:00
|
|
|
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
|
|
|
|
}
|
2023-08-28 15:00:42 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-04-09 19:18:51 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-11-11 09:22:03 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|