mapsize 1.2.0: permanent delete and in-map reveal when headless

On a headless Linux or BSD system (no DISPLAY or WAYLAND_DISPLAY) nothing
empties the freedesktop trash, so trashing never freed space. There, d now
deletes permanently after an explicit confirmation, checking owner and type
against the scan and removing through an os.Root on the parent so a swapped
symlink cannot redirect it. o shows the folder in the map instead of
launching a file manager. Desktop sessions, macOS and Windows are unchanged.
This commit is contained in:
Andy Dixon
2026-10-03 12:50:07 +01:00
parent 352d4292df
commit 8d9344cfef
15 changed files with 214 additions and 27 deletions
+35
View File
@@ -0,0 +1,35 @@
//go:build unix
package platform
import (
"errors"
"io/fs"
"os"
"path/filepath"
"syscall"
)
// Delete permanently removes path (recursively for a directory) if it still
// matches want. The check and the removal both go through an os.Root on the
// parent directory, so a symlink swapped in since the scan cannot redirect
// the removal outside it.
func Delete(path string, want Expect) error {
if err := checkAbs(path); err != nil {
return err
}
dir, base := filepath.Dir(path), filepath.Base(path)
root, err := os.OpenRoot(dir)
if err != nil {
return err
}
defer root.Close()
fi, err := root.Lstat(base)
if err != nil {
return err
}
if fi.Sys().(*syscall.Stat_t).Uid != want.UID || fi.Mode()&fs.ModeType != want.Mode&fs.ModeType {
return errors.New("item changed since the scan (owner or type differs); rescan first")
}
return root.RemoveAll(base)
}
+4
View File
@@ -75,3 +75,7 @@ func volumeOf(path string) string {
}
return ""
}
// Headless is always false: macOS has Finder and the Trash even without
// $DISPLAY (which it does not use).
func Headless() bool { return false }
+7
View File
@@ -123,3 +123,10 @@ func mountTop(path string, dev uint64) string {
cur = parent
}
}
// Headless reports whether there is no graphical session (no X or Wayland
// display), so there is no file manager to open and no desktop trash anyone
// will empty.
func Headless() bool {
return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == ""
}
+40
View File
@@ -113,3 +113,43 @@ func TestTrashRefusesSwappedItem(t *testing.T) {
t.Fatal(err)
}
}
func TestDeletePermanently(t *testing.T) {
base := t.TempDir()
dir := filepath.Join(base, "S01")
os.MkdirAll(filepath.Join(dir, "extras"), 0o755)
os.WriteFile(filepath.Join(dir, "extras", "E01.mkv"), []byte("x"), 0o644)
// A symlink inside must be removed, not followed.
keep := filepath.Join(base, "keep")
os.WriteFile(keep, []byte("k"), 0o644)
os.Symlink(keep, filepath.Join(dir, "link"))
w := want(t, dir)
if err := Delete(dir, Expect{UID: w.UID, Mode: 0}); err == nil {
t.Fatal("type mismatch accepted")
}
if err := Delete(dir, w); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(dir); !os.IsNotExist(err) {
t.Fatal("directory still present")
}
if _, err := os.Lstat(keep); err != nil {
t.Fatal("symlink target was deleted")
}
if err := Delete("relative/path", Expect{}); err == nil {
t.Fatal("relative paths must be refused")
}
}
func TestHeadless(t *testing.T) {
t.Setenv("DISPLAY", "")
t.Setenv("WAYLAND_DISPLAY", "")
if !Headless() {
t.Fatal("no display should be headless")
}
t.Setenv("WAYLAND_DISPLAY", "wayland-0")
if Headless() {
t.Fatal("wayland session reported headless")
}
}
+11 -1
View File
@@ -2,7 +2,10 @@
package platform
import "errors"
import (
"errors"
"os"
)
// TrashName is the user-facing name of the trash.
const TrashName = "trash"
@@ -12,3 +15,10 @@ func Reveal(string) error { return errors.New("not supported on this platform")
// Trash is unsupported on this platform.
func Trash(string, Expect) error { return errors.New("trash not supported on this platform") }
// Headless reports whether there is no graphical session (no X or Wayland
// display), so there is no file manager to open and no desktop trash anyone
// will empty.
func Headless() bool {
return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == ""
}
+6
View File
@@ -70,3 +70,9 @@ func Trash(path string, _ Expect) error {
}
return nil
}
// Headless is always false: Windows always has Explorer and the Recycle Bin.
func Headless() bool { return false }
// Delete is not offered on Windows; items go to the Recycle Bin.
func Delete(string, Expect) error { return errors.New("permanent delete not supported on Windows") }