mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-04-27 19:28:59 +00:00
311 lines
7.2 KiB
Go
311 lines
7.2 KiB
Go
// nolint:goheader
|
|
|
|
/*
|
|
Copyright © 2022 spf13/afero
|
|
Copyright © 2022 SUSE LLC
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package fsutils
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
|
"github.com/twpayne/go-vfs/v5"
|
|
"github.com/twpayne/go-vfs/v5/vfst"
|
|
)
|
|
|
|
// DirSize returns the accumulated size of all files in folder
|
|
func DirSize(fs v1.FS, path string) (int64, error) {
|
|
var size int64
|
|
err := vfs.Walk(fs, path, func(_ string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
size += info.Size()
|
|
}
|
|
return err
|
|
})
|
|
return size, err
|
|
}
|
|
|
|
// Check if a file or directory exists.
|
|
func Exists(fs v1.FS, path string) (bool, error) {
|
|
_, err := fs.Stat(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// IsDir check if the path is a dir
|
|
func IsDir(fs v1.FS, path string) (bool, error) {
|
|
fi, err := fs.Stat(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return fi.IsDir(), nil
|
|
}
|
|
|
|
// MkdirAll directory and all parents if not existing
|
|
func MkdirAll(fs v1.FS, name string, mode os.FileMode) (err error) {
|
|
if _, isReadOnly := fs.(*vfs.ReadOnlyFS); isReadOnly {
|
|
return permError("mkdir", name)
|
|
}
|
|
if name, err = fs.RawPath(name); err != nil {
|
|
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
|
}
|
|
return os.MkdirAll(name, mode)
|
|
}
|
|
|
|
// permError returns an *os.PathError with Err syscall.EPERM.
|
|
func permError(op, path string) error {
|
|
return &os.PathError{
|
|
Op: op,
|
|
Path: path,
|
|
Err: syscall.EPERM,
|
|
}
|
|
}
|
|
|
|
// Random number state.
|
|
// We generate random temporary file names so that there's a good
|
|
// chance the file doesn't exist yet - keeps the number of tries in
|
|
// TempFile to a minimum.
|
|
var rand uint32
|
|
var randmu sync.Mutex
|
|
|
|
func reseed() uint32 {
|
|
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
|
}
|
|
|
|
func nextRandom() string {
|
|
randmu.Lock()
|
|
r := rand
|
|
if r == 0 {
|
|
r = reseed()
|
|
}
|
|
r = r*1664525 + 1013904223 // constants from Numerical Recipes
|
|
rand = r
|
|
randmu.Unlock()
|
|
return strconv.Itoa(int(1e9 + r%1e9))[1:]
|
|
}
|
|
|
|
// TempDir creates a temp dir in the virtual fs
|
|
// Took from afero.FS code and adapted
|
|
func TempDir(fs v1.FS, dir, prefix string) (name string, err error) {
|
|
if dir == "" {
|
|
dir = os.TempDir()
|
|
}
|
|
// This skips adding random stuff to the created temp dir so the temp dir created is predictable for testing
|
|
if _, isTestFs := fs.(*vfst.TestFS); isTestFs {
|
|
err = MkdirAll(fs, filepath.Join(dir, prefix), 0700)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name = filepath.Join(dir, prefix)
|
|
return
|
|
}
|
|
nconflict := 0
|
|
for i := 0; i < 10000; i++ {
|
|
try := filepath.Join(dir, prefix+nextRandom())
|
|
err = MkdirAll(fs, try, 0700)
|
|
if os.IsExist(err) {
|
|
if nconflict++; nconflict > 10 {
|
|
randmu.Lock()
|
|
rand = reseed()
|
|
randmu.Unlock()
|
|
}
|
|
continue
|
|
}
|
|
if err == nil {
|
|
name = try
|
|
}
|
|
break
|
|
}
|
|
return
|
|
}
|
|
|
|
// TempFile creates a temp file in the virtual fs
|
|
// Took from afero.FS code and adapted
|
|
func TempFile(fs v1.FS, dir, pattern string) (f *os.File, err error) {
|
|
if dir == "" {
|
|
dir = os.TempDir()
|
|
}
|
|
|
|
var prefix, suffix string
|
|
if pos := strings.LastIndex(pattern, "*"); pos != -1 {
|
|
prefix, suffix = pattern[:pos], pattern[pos+1:]
|
|
} else {
|
|
prefix = pattern
|
|
}
|
|
|
|
nconflict := 0
|
|
for i := 0; i < 10000; i++ {
|
|
name := filepath.Join(dir, prefix+nextRandom()+suffix)
|
|
f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
|
if os.IsExist(err) {
|
|
if nconflict++; nconflict > 10 {
|
|
randmu.Lock()
|
|
rand = reseed()
|
|
randmu.Unlock()
|
|
}
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return
|
|
}
|
|
|
|
// Walkdir with an FS implementation
|
|
type statDirEntry struct {
|
|
info fs.FileInfo
|
|
}
|
|
|
|
func (d *statDirEntry) Name() string { return d.info.Name() }
|
|
func (d *statDirEntry) IsDir() bool { return d.info.IsDir() }
|
|
func (d *statDirEntry) Type() fs.FileMode { return d.info.Mode().Type() }
|
|
func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
|
|
|
|
// WalkDirFs is the same as filepath.WalkDir but accepts a v1.Fs so it can be run on any v1.Fs type
|
|
func WalkDirFs(fs v1.FS, root string, fn fs.WalkDirFunc) error {
|
|
info, err := fs.Stat(root)
|
|
if err != nil {
|
|
err = fn(root, nil, err)
|
|
} else {
|
|
err = walkDir(fs, root, &statDirEntry{info}, fn)
|
|
}
|
|
if errors.Is(err, filepath.SkipDir) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func walkDir(fs v1.FS, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
|
|
if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
|
|
if err == filepath.SkipDir && d.IsDir() {
|
|
// Successfully skipped directory.
|
|
err = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
dirs, err := readDir(fs, path)
|
|
if err != nil {
|
|
// Second call, to report ReadDir error.
|
|
err = walkDirFn(path, d, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, d1 := range dirs {
|
|
path1 := filepath.Join(path, d1.Name())
|
|
if err := walkDir(fs, path1, d1, walkDirFn); err != nil {
|
|
if errors.Is(err, filepath.SkipDir) {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readDir(fs v1.FS, dirname string) ([]fs.DirEntry, error) {
|
|
dirs, err := fs.ReadDir(dirname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
|
|
return dirs, nil
|
|
}
|
|
|
|
// Copy copies src to dst like the cp command.
|
|
func Copy(fs v1.FS, src, dst string) error {
|
|
if dst == src {
|
|
return os.ErrInvalid
|
|
}
|
|
|
|
srcF, err := fs.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer srcF.Close()
|
|
|
|
info, err := srcF.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dstF, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dstF.Close()
|
|
|
|
if _, err := io.Copy(dstF, srcF); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GlobFs returns the names of all files matching pattern or nil if there is no matching file.
|
|
// Only consider the names of files in the directory included in the pattern, not in subdirectories.
|
|
// So the pattern "dir/*" will return only the files in the directory "dir", not in "dir/subdir".
|
|
func GlobFs(fs v1.FS, pattern string) ([]string, error) {
|
|
var matches []string
|
|
|
|
// Check if the pattern is well formed.
|
|
if _, err := filepath.Match(pattern, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Split the pattern into directory and file parts.
|
|
dir, file := filepath.Split(pattern)
|
|
if dir == "" {
|
|
dir = "."
|
|
}
|
|
|
|
// Read the directory.
|
|
entries, err := fs.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Match the entries against the pattern.
|
|
for _, entry := range entries {
|
|
if matched, err := filepath.Match(file, entry.Name()); err != nil {
|
|
return nil, err
|
|
} else if matched {
|
|
matches = append(matches, filepath.Join(dir, entry.Name()))
|
|
}
|
|
}
|
|
|
|
return matches, nil
|
|
}
|