Files
narratio/internal/fileops/cleanup.go

86 lines
2.6 KiB
Go

package fileops
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// RemoveAllUnderRoot removes target and its contents beneath root. target must
// name a proper descendant of root; removal is confined by the opened root.
func RemoveAllUnderRoot(rootPath, target string) error {
root, targetName, err := openCleanupRoot(rootPath, target)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
defer func() { _ = root.Close() }()
if err := root.RemoveAll(targetName); err != nil {
return fmt.Errorf("remove cleanup target %q: %w", targetName, err)
}
return nil
}
// RemoveFileUnderRoot removes an exact regular-file target below root without
// following the leaf. A missing target is successful; directories, symlinks,
// and other non-regular entries are rejected.
func RemoveFileUnderRoot(rootPath, target string) error {
root, targetName, err := openCleanupRoot(rootPath, target)
if err != nil {
return err
}
defer func() { _ = root.Close() }()
info, err := root.Lstat(targetName)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("inspect cleanup file %q: %w", targetName, err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("refusing to delete non-regular file path %q", targetName)
}
if err := root.Remove(targetName); err != nil {
return fmt.Errorf("remove cleanup file %q: %w", targetName, err)
}
return nil
}
func openCleanupRoot(rootPath, target string) (*os.Root, string, error) {
if strings.TrimSpace(rootPath) == "" {
return nil, "", fmt.Errorf("cleanup root is required")
}
if strings.TrimSpace(target) == "" {
return nil, "", fmt.Errorf("cleanup target is required")
}
rootAbs, err := filepath.Abs(rootPath)
if err != nil {
return nil, "", fmt.Errorf("resolve cleanup root: %w", err)
}
targetAbs, err := filepath.Abs(target)
if err != nil {
return nil, "", fmt.Errorf("resolve cleanup target: %w", err)
}
relative, err := filepath.Rel(rootAbs, targetAbs)
if err != nil {
return nil, "", fmt.Errorf("resolve cleanup target below root: %w", err)
}
if relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return nil, "", fmt.Errorf("cleanup target %q must be below root %q", targetAbs, rootAbs)
}
name := filepath.Clean(relative)
if filepath.IsAbs(name) || name == "." || name == ".." || strings.HasPrefix(name, ".."+string(filepath.Separator)) {
return nil, "", fmt.Errorf("cleanup target %q must be below root %q", targetAbs, rootAbs)
}
root, err := os.OpenRoot(rootAbs)
if err != nil {
return nil, "", fmt.Errorf("open cleanup root %q: %w", rootAbs, err)
}
return root, name, nil
}