From 6e0d44c50de124fc7fbc45052c71b24b6e06d075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Sun, 23 Aug 2026 23:40:53 +0800 Subject: [PATCH] feat(nixos): manage scattered trash dirs with trash-cli retention timer (#265) * feat(nushell): add trash command using the home trash can On a tmpfs root with persistent dirs bind-mounted in, `rm --trash` scatters items into per-mount .Trash-$uid dirs that file managers never show, and gio refuses to trash on those internal mounts. Add a `trash` command implementing the freedesktop trash spec 'failsafe' mode: items are moved to ~/.local/share/Trash with spec-compliant .trashinfo entries, so Thunar can list and restore them. Also preserve ~/.local/share/Trash on idols-ai so the trash survives reboots. * feat(nixos): manage scattered trash dirs with trash-cli retention timer Replace the hand-written nushell trash command with the established trash-cli tooling. On a tmpfs root with persistent dirs bind-mounted in, the trash crate (nushell rm --trash) scatters items into per-mount .Trash-$uid dirs that file managers never show and nothing cleans up. - add trash-cli to system packages: trash-list scans/trash-restore handles every mount point's trash dir - daily systemd timer runs 'trash-empty 30 -f', purging items older than 30 days across the home trash and all mount points - drop the hand-written trash.nu module - note in preservation.nix: do NOT persist ~/.local/share/Trash; a bind-mounted trash dir breaks the trash crate's home-topdir match for files straight under $HOME (it would try /.Trash-$uid, EACCES) * fix(nixos): order trash-empty after preservation.target The scattered .Trash-$uid dirs live inside the preservation bind mounts, which use DefaultDependencies=no and are therefore NOT ordered after local-fs.target. Add preservation.target to After= so trash-empty only runs once all those mounts are up. --- hosts/idols-ai/preservation.nix | 8 +++++ modules/base/packages.nix | 1 + modules/nixos/base/trash.nix | 61 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 modules/nixos/base/trash.nix diff --git a/hosts/idols-ai/preservation.nix b/hosts/idols-ai/preservation.nix index c51a4230..c1256878 100644 --- a/hosts/idols-ai/preservation.nix +++ b/hosts/idols-ai/preservation.nix @@ -110,6 +110,14 @@ in # Keep .cache off tmpfs to avoid high RAM usage; many apps use it and it is storage-heavy. ".cache" + # NOTE: do NOT persist ~/.local/share/Trash here. The trash crate (nushell + # `rm --trash`) picks the home trash only when the file's mount topdir equals + # the trash dir's topdir; a bind-mounted Trash dir would make files straight + # under $HOME fail with EACCES (it would try /.Trash-$uid). The home trash on + # tmpfs is lost on reboot, which is acceptable: scattered per-mount + # .Trash-$uid dirs on persistent volumes are covered by the trash-empty + # retention timer (modules/nixos/base/trash.nix). + # ====================================== # Codes / Work / Playground # ====================================== diff --git a/modules/base/packages.nix b/modules/base/packages.nix index 6e243161..4e4241c9 100644 --- a/modules/base/packages.nix +++ b/modules/base/packages.nix @@ -86,5 +86,6 @@ which tree tealdeer # a very fast version of tldr + trash-cli # freedesktop trash can CLI: trash-list / trash-restore / trash-put / trash-empty ]; } diff --git a/modules/nixos/base/trash.nix b/modules/nixos/base/trash.nix new file mode 100644 index 00000000..1c70598c --- /dev/null +++ b/modules/nixos/base/trash.nix @@ -0,0 +1,61 @@ +{ + # ================================================================== + # + # Trash retention cleanup (nixpkgs `trash-cli`) + # https://github.com/andreafrancia/trash-cli + # + # On a tmpfs root with persistent dirs bind-mounted in, the trash + # crate (nushell `rm --trash`, GUI file managers) scatters items into + # per-mount .Trash-$uid dirs that file managers never show. This timer + # purges trash items older than RETENTION_DAYS across the home trash + # and all mount points. + # + # Usage: + # trash-list # view items in all trash dirs + # trash-restore # restore an item + # trash-rm # remove a single item from the trash + # trash-empty 30 -f # run the retention purge manually + # + # ================================================================== + + myvars, + pkgs, + ... +}: +let + retentionDays = 30; +in +{ + systemd.services.trash-empty = { + description = "Purge trash items older than ${toString retentionDays} days"; + script = '' + ${pkgs.trash-cli}/bin/trash-empty ${toString retentionDays} -f + ''; + # The scattered .Trash-$uid dirs live inside the preservation bind mounts, so we + # must run after preservation.target (reached only once all those mounts are up). + # preservation.mount units use DefaultDependencies=no, so they are NOT ordered + # after local-fs.target on their own; list both to be safe on every host. + # On hosts without preservation, the After=preservation.target edge is a no-op. + after = [ + "local-fs.target" + "preservation.target" + ]; + serviceConfig = { + Type = "oneshot"; + User = myvars.username; + Environment = [ + "HOME=/home/${myvars.username}" + "XDG_DATA_HOME=/home/${myvars.username}/.local/share" + ]; + }; + }; + + systemd.timers.trash-empty = { + description = "Timer for the trash-empty service"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + RandomizedDelaySec = "15min"; + }; + }; +}