2024-05-22 09:46:24 +00:00
|
|
|
package disk
|
2022-11-05 11:10:19 +00:00
|
|
|
|
|
|
|
import (
|
2024-05-22 09:46:24 +00:00
|
|
|
"errors"
|
2023-01-11 11:13:13 +00:00
|
|
|
"fmt"
|
2022-11-05 11:10:19 +00:00
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"syscall"
|
|
|
|
|
2022-11-28 09:01:53 +00:00
|
|
|
"codeberg.org/gruf/go-fastpath/v2"
|
2024-05-22 09:46:24 +00:00
|
|
|
"codeberg.org/gruf/go-storage/internal"
|
2022-11-05 11:10:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// NOTE:
|
|
|
|
// These functions are for opening storage files,
|
|
|
|
// not necessarily for e.g. initial setup (OpenFile)
|
|
|
|
|
|
|
|
// walkDir traverses the dir tree of the supplied path, performing the supplied walkFn on each entry
|
2024-05-22 09:46:24 +00:00
|
|
|
func walkDir(pb *fastpath.Builder, path string, args OpenArgs, walkFn func(string, fs.DirEntry) error) error {
|
|
|
|
// Read directory entries at path.
|
|
|
|
entries, err := readDir(path, args)
|
2022-11-05 11:10:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// frame represents a directory entry
|
|
|
|
// walk-loop snapshot, taken when a sub
|
|
|
|
// directory requiring iteration is found
|
|
|
|
type frame struct {
|
|
|
|
path string
|
|
|
|
entries []fs.DirEntry
|
|
|
|
}
|
|
|
|
|
|
|
|
// stack contains a list of held snapshot
|
|
|
|
// frames, representing unfinished upper
|
|
|
|
// layers of a directory structure yet to
|
|
|
|
// be traversed.
|
|
|
|
var stack []frame
|
|
|
|
|
|
|
|
outer:
|
|
|
|
for {
|
|
|
|
if len(entries) == 0 {
|
|
|
|
if len(stack) == 0 {
|
|
|
|
// Reached end
|
|
|
|
break outer
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pop frame from stack
|
|
|
|
frame := stack[len(stack)-1]
|
|
|
|
stack = stack[:len(stack)-1]
|
|
|
|
|
|
|
|
// Update loop vars
|
|
|
|
entries = frame.entries
|
|
|
|
path = frame.path
|
|
|
|
}
|
|
|
|
|
|
|
|
for len(entries) > 0 {
|
|
|
|
// Pop next entry from queue
|
|
|
|
entry := entries[0]
|
|
|
|
entries = entries[1:]
|
|
|
|
|
|
|
|
// Pass to provided walk function
|
|
|
|
if err := walkFn(path, entry); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if entry.IsDir() {
|
|
|
|
// Push current frame to stack
|
|
|
|
stack = append(stack, frame{
|
|
|
|
path: path,
|
|
|
|
entries: entries,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Update current directory path
|
|
|
|
path = pb.Join(path, entry.Name())
|
|
|
|
|
|
|
|
// Read next directory entries
|
2024-05-22 09:46:24 +00:00
|
|
|
next, err := readDir(path, args)
|
2022-11-05 11:10:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set next entries
|
|
|
|
entries = next
|
|
|
|
|
|
|
|
continue outer
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// cleanDirs traverses the dir tree of the supplied path, removing any folders with zero children
|
2024-05-22 09:46:24 +00:00
|
|
|
func cleanDirs(path string, args OpenArgs) error {
|
|
|
|
pb := internal.GetPathBuilder()
|
|
|
|
err := cleanDir(pb, path, args, true)
|
|
|
|
internal.PutPathBuilder(pb)
|
|
|
|
return err
|
2022-11-05 11:10:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// cleanDir performs the actual dir cleaning logic for the above top-level version.
|
2024-05-22 09:46:24 +00:00
|
|
|
func cleanDir(pb *fastpath.Builder, path string, args OpenArgs, top bool) error {
|
|
|
|
// Get directory entries at path.
|
|
|
|
entries, err := readDir(path, args)
|
2022-11-05 11:10:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-11 11:13:13 +00:00
|
|
|
// If no entries, delete dir.
|
|
|
|
if !top && len(entries) == 0 {
|
2022-11-05 11:10:19 +00:00
|
|
|
return rmdir(path)
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
var errs []error
|
|
|
|
|
|
|
|
// Iterate all directory entries.
|
2022-11-05 11:10:19 +00:00
|
|
|
for _, entry := range entries {
|
2024-05-22 09:46:24 +00:00
|
|
|
|
2022-11-05 11:10:19 +00:00
|
|
|
if entry.IsDir() {
|
2023-01-11 11:13:13 +00:00
|
|
|
// Calculate directory path.
|
2024-05-22 09:46:24 +00:00
|
|
|
dir := pb.Join(path, entry.Name())
|
2023-01-11 11:13:13 +00:00
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// Recursively clean sub-directory entries, adding errs.
|
|
|
|
if err := cleanDir(pb, dir, args, false); err != nil {
|
|
|
|
err = fmt.Errorf("error(s) cleaning subdir %s: %w", dir, err)
|
|
|
|
errs = append(errs, err)
|
2022-11-05 11:10:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// Return combined errors.
|
|
|
|
return errors.Join(errs...)
|
2022-11-05 11:10:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// readDir will open file at path, read the unsorted list of entries, then close.
|
2024-05-22 09:46:24 +00:00
|
|
|
func readDir(path string, args OpenArgs) ([]fs.DirEntry, error) {
|
|
|
|
// Open directory at path.
|
|
|
|
file, err := open(path, args)
|
2022-11-05 11:10:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// Read ALL directory entries.
|
2022-11-05 11:10:19 +00:00
|
|
|
entries, err := file.ReadDir(-1)
|
|
|
|
|
|
|
|
// Done with file
|
|
|
|
_ = file.Close()
|
|
|
|
|
|
|
|
return entries, err
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// open is a simple wrapper around syscall.Open().
|
|
|
|
func open(path string, args OpenArgs) (*os.File, error) {
|
2022-11-05 11:10:19 +00:00
|
|
|
var fd int
|
|
|
|
err := retryOnEINTR(func() (err error) {
|
2024-05-22 09:46:24 +00:00
|
|
|
fd, err = syscall.Open(path, args.Flags, args.Perms)
|
2022-11-05 11:10:19 +00:00
|
|
|
return
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return os.NewFile(uintptr(fd), path), nil
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// stat is a simple wrapper around syscall.Stat().
|
|
|
|
func stat(path string) (*syscall.Stat_t, error) {
|
2022-11-05 11:10:19 +00:00
|
|
|
var stat syscall.Stat_t
|
|
|
|
err := retryOnEINTR(func() error {
|
|
|
|
return syscall.Stat(path, &stat)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
if err == syscall.ENOENT {
|
|
|
|
// not-found is no error
|
|
|
|
err = nil
|
|
|
|
}
|
2024-05-22 09:46:24 +00:00
|
|
|
return nil, err
|
2022-11-05 11:10:19 +00:00
|
|
|
}
|
2024-05-22 09:46:24 +00:00
|
|
|
return &stat, nil
|
2022-11-05 11:10:19 +00:00
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// unlink is a simple wrapper around syscall.Unlink().
|
2022-11-05 11:10:19 +00:00
|
|
|
func unlink(path string) error {
|
|
|
|
return retryOnEINTR(func() error {
|
|
|
|
return syscall.Unlink(path)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// rmdir is a simple wrapper around syscall.Rmdir().
|
2022-11-05 11:10:19 +00:00
|
|
|
func rmdir(path string) error {
|
|
|
|
return retryOnEINTR(func() error {
|
|
|
|
return syscall.Rmdir(path)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-22 09:46:24 +00:00
|
|
|
// retryOnEINTR is a low-level filesystem function
|
|
|
|
// for retrying syscalls on O_EINTR received.
|
2022-11-05 11:10:19 +00:00
|
|
|
func retryOnEINTR(do func() error) error {
|
|
|
|
for {
|
|
|
|
err := do()
|
|
|
|
if err == syscall.EINTR {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|