mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 17:52:02 +02:00
Utility function to delete content of dir pointed to by open file
This commit is contained in:
80
tools/utils/rmtree.go
Normal file
80
tools/utils/rmtree.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
// RemoveChildren recursively removes all files and subdirectories
|
||||||
|
// within the directory pointed to by dirFile. Removes all it can but returns
|
||||||
|
// the first error, if any.
|
||||||
|
func RemoveChildren(dirFile *os.File) error {
|
||||||
|
fd := int(dirFile.Fd())
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
// Rewind directory pointer to ensure we start from the beginning
|
||||||
|
if _, err := dirFile.Seek(0, 0); err != nil {
|
||||||
|
return &os.PathError{Op: "seek", Path: dirFile.Name(), Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Read names in small chunks to handle very large directories
|
||||||
|
names, err := dirFile.Readdirnames(64)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = &os.PathError{Op: "readdirnames", Path: dirFile.Name(), Err: err}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
var stat unix.Stat_t
|
||||||
|
// Get file info relative to the parent FD
|
||||||
|
err := unix.Fstatat(fd, name, &stat, unix.AT_SYMLINK_NOFOLLOW)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = &os.PathError{Op: "fstatat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.Mode & unix.S_IFMT) == unix.S_IFDIR {
|
||||||
|
// Open subdirectory relative to parent FD
|
||||||
|
childFd, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = &os.PathError{Op: "openat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
childFile := os.NewFile(uintptr(childFd), name)
|
||||||
|
if err := RemoveChildren(childFile); err != nil && firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
childFile.Close()
|
||||||
|
|
||||||
|
// Remove the empty subdirectory
|
||||||
|
if err := unix.Unlinkat(fd, name, unix.AT_REMOVEDIR); err != nil && firstErr == nil {
|
||||||
|
firstErr = &os.PathError{Op: "unlinkat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove file/symlink
|
||||||
|
if err := unix.Unlinkat(fd, name, 0); err != nil && firstErr == nil {
|
||||||
|
firstErr = &os.PathError{Op: "unlinkat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = dirFile.Seek(0, 0)
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
52
tools/utils/rmtree_test.go
Normal file
52
tools/utils/rmtree_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoveChildren(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create nested structure
|
||||||
|
subDir := filepath.Join(tmpDir, "subdir")
|
||||||
|
os.Mkdir(subDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("data"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(subDir, "file2.txt"), []byte("data"), 0644)
|
||||||
|
|
||||||
|
d, err := os.Open(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer d.Close()
|
||||||
|
|
||||||
|
if err := RemoveChildren(d); err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify directory is empty
|
||||||
|
entries, _ := os.ReadDir(tmpDir)
|
||||||
|
if len(entries) != 0 {
|
||||||
|
t.Errorf("expected 0 entries, got %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveChildren_FirstError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a read-only file to trigger an error on some systems
|
||||||
|
// (Note: Behavior varies by OS; this is a conceptual test for firstErr)
|
||||||
|
lockedFile := filepath.Join(tmpDir, "locked")
|
||||||
|
os.WriteFile(lockedFile, nil, 0000)
|
||||||
|
|
||||||
|
d, _ := os.Open(tmpDir)
|
||||||
|
defer d.Close()
|
||||||
|
|
||||||
|
err := RemoveChildren(d)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*os.PathError); !ok {
|
||||||
|
t.Errorf("expected *os.PathError, got %T", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user