Python bindings for rtrash — FreeDesktop trash as a safe alternative to os.remove
Project description
rtrash
Native Rust FreeDesktop trash for Linux: an rm-compatible put path and the everyday trash-cli suite (trash-put, trash-empty, trash-list, trash-restore, trash-rm) as one multi-call binary.
Implements the freedesktop.org Trash specification.
No interpreter startup; emptying deletes entries in parallel via rayon.
Documentation
| Doc | Contents |
|---|---|
| docs/getting-started.md | Install CLI + Python, shortest paths |
| docs/architecture.md | FreeDesktop layout, safety vs rm / os.remove / trash-cli |
| docs/benchmarks.md | Measured safer/better/faster vs trash-cli |
| docs/bindings.md | Maturin/PyO3 API (unlink / rmtree replacements) |
| CHANGELOG.md | Release notes (towncrier fragments in docs/newsfragments/) |
Org-mode sources (readcon-core style): docs/orgmode/.
Python: replace permanent delete
import rtrash
rtrash.unlink("file.txt") # not os.remove
rtrash.rmtree("build/") # not shutil.rmtree
$ pip install rtrash
Docs site
Published at https://rtrash.rgoswami.me (GitHub Pages).
Project documentation is authored in orgmode under docs/orgmode/ and built to a Sphinx + Shibuya static site (readcon-style: org → RST → HTML).
# needs: pandoc (or emacs + ox-rst), python3, venv
./docs/build.sh
# HTML → docs/build/index.html
python3 -m http.server -d docs/build 8000
| Page | Source |
|---|---|
| Landing | docs/orgmode/index.org |
| Getting started | docs/orgmode/getting-started.org |
| Architecture | docs/orgmode/architecture.org |
| Benchmarks | docs/orgmode/benchmarks.org |
| Python bindings | docs/orgmode/bindings.org |
Markdown mirrors under docs/*.md remain for quick GitHub reading; the site is the structured presentation of the same material.
Platform
Platform: Linux FreeDesktop trash only (home trash + per-mount trash dirs). Not a Windows/macOS system-trash wrapper, and not a colored TUI.
Install
Registry (primary): published on crates.io/crates/rtrash and pypi.org/project/rtrash.
$ cargo install rtrash # CLI from crates.io (needs Rust toolchain)
$ cargo binstall rtrash # CLI prebuilt musl (x86_64 / aarch64 Linux)
$ pip install rtrash # Python bindings (Linux x86_64 wheels, CPython 3.10–3.14)
$ rtrash setup # multi-call links, completions, man under ~/.local
Always run rtrash setup after the CLI binary lands on PATH (override with --prefix=DIR).
1. cargo binstall (preferred binary install)
Uses cargo-binstall to download the prebuilt Linux musl static tarball for your CPU (no local compile).
GitHub Releases ship x86_64 and aarch64 musl assets for tag v*.
Crate metadata remaps typical glibc hosts (*-unknown-linux-gnu) to the matching musl tarball, so a normal:
$ cargo binstall rtrash
$ rtrash setup
does not look for a non-existent *-linux-gnu*.tar.gz on x86_64 or aarch64 Linux.
| Arch | Asset basename |
|---|---|
| x86_64 | rtrash-<version>-x86_64-unknown-linux-musl.tar.gz |
| aarch64 (ARM64) | rtrash-<version>-aarch64-unknown-linux-musl.tar.gz |
Naming matches [package.metadata.binstall] in Cargo.toml / scripts/package-release.sh.
macOS and Windows have no prebuilt FreeDesktop assets (and Windows system trash is out of scope for this tool's niche). Use from-source only where the Linux FreeDesktop code path applies.
2. Manual musl tarball (no cargo at all)
When a Release exists, download the matching asset from the repository Releases page, extract, put bin/ on PATH, then rtrash setup --force (or follow INSTALL.txt in the tarball).
Build the same artifact yourself (builder host with musl target):
$ ./scripts/package-release.sh x86_64-unknown-linux-musl
$ ./scripts/package-release.sh aarch64-unknown-linux-musl
# or, on a host that can build both:
$ ./scripts/package-release.sh --all
# → dist/rtrash-<version>-{x86_64,aarch64}-unknown-linux-musl.tar.gz
CI: .github/workflows/release.yml builds both targets on v* tags (x86_64 and ubuntu-24.04-arm) and attaches the tarballs to the GitHub Release (what binstall downloads).
3. From crates.io (cargo install)
Requires a Rust toolchain (MSRV 1.77). Dynamically linked glibc binary by default (compiles on the install host).
$ cargo install rtrash
$ rtrash setup
Tip of main without waiting for a release:
$ cargo install --git https://github.com/HaoZeke/rtrash
$ rtrash setup
4. Python (pip install rtrash)
Prebuilt manylinux wheels for CPython 3.10–3.14 on Linux x86_64 (FreeDesktop only).
$ pip install rtrash
$ python -c "import rtrash; print(rtrash.version())"
From a checkout (dev): pip install maturin && maturin develop --features python.
What rtrash setup installs
| What | Where (default) |
|---|---|
Multi-call symlinks (trash-put, trash-empty, trash-list, trash-restore, trash-rm, trash) |
~/.local/bin/ → this rtrash |
| bash completion | ~/.local/share/bash-completion/completions/rtrash (+ multi-call links) |
| zsh completion | ~/.local/share/zsh/site-functions/_rtrash |
| fish completion | ~/.config/fish/completions/rtrash.fish (+ multi-call *.fish links) |
| man page | ~/.local/share/man/man1/rtrash.1 |
Useful flags: rtrash setup --dry-run, --force (refresh after upgrade), --with-rm (also link rm → put into trash), --prefix=/usr/local.
Subcommands work without multi-call names: rtrash put|empty|list|status|restore|rm ….
| Multi-call name | Meaning |
|---|---|
rm / trash / trash-put |
put (move paths into the trash) |
trash-list |
list |
trash-restore |
restore |
trash-empty |
empty |
trash-rm |
permanently delete matching trash entries |
Subcommand rtrash rm PATTERN is the same as multi-call trash-rm (not the same as multi-call rm, which puts).
Packagers and custom layouts
Sources also live in-tree for packaging (completions/, man/rtrash.1).
From any installed binary:
$ rtrash completions bash > rtrash.bash
$ rtrash completions zsh > _rtrash
$ rtrash completions fish > rtrash.fish
$ rtrash man > rtrash.1
$ rtrash setup --prefix=/usr --force # system prefix when packaging
Contributing and releases
- Changelog: add a towncrier fragment under
docs/newsfragments/for user-visible changes:towncrier create -c "Short description." +slug.added.md(or.fixed.md,.changed.md, …)- At release:
towncrier build --version X.Y.Z --yesthen tagvX.Y.Z
- Version lockstep: cocogitto (
cog.toml) bumpsCargo.toml,pyproject.toml, anddocs/source/conf.pytogether (disable_changelog = true; towncrier ownsCHANGELOG.md). - Hooks:
prek installthenprek run -a(builtin large-file check, yaml/toml, codespell, version lockstep). CI also runs HaoZeke/large-file-auditor and lychee. - Docs PRs: the documentation workflow uploads a
documentationartifact; HaoZeke/doc-previewer comments a preview link on the PR.
Tutorial
Trash a file, inspect the trash, restore it, then empty:
$ echo data > scratch.txt
$ rtrash scratch.txt
$ rtrash list
2026-07-03 14:00:00 /home/you/scratch.txt
$ rtrash restore scratch.txt
restored '/home/you/scratch.txt'
$ rtrash scratch.txt
$ rtrash empty
Removed 1 item
Selective permanent delete from the trash (trash-cli trash-rm):
$ rtrash put a.o b.c
$ rtrash rm '*.o' # quote globs; removes a.o from trash only
$ rtrash list # b.c still listed
rtrash FILE with no subcommand behaves like rm, so shell habits carry over: rtrash -rf build/ moves build/ to the trash instead of unlinking it.
Scripts / tests: pin with --trash-dir=… and/or isolate XDG_DATA_HOME so list/empty/restore/rm do not walk every mount’s trash.
Reference
rtrash put (also rm, trash, trash-put, or bare rtrash)
Accepts the common rm(1) flags with the same semantics, except that files move to the trash: -f, -i, -I, --interactive[=WHEN], -r/-R, -d, -v, --one-file-system (accepted for rm compatibility; no-op because entries move whole and are never walked like rm -x), --preserve-root (default), --no-preserve-root, --. Directories need -r (or -d when empty), ./..// are refused (unless --no-preserve-root for /), and exit codes mirror rm (0 success, 1 failure, 2 usage error).
As with GNU rm, the last of -f / -i / -I (and the matching long forms) wins for prompt vs force behavior.
-f also ignores missing paths.
Trash placement follows the spec: the home trash ($XDG_DATA_HOME/Trash, defaulting under ~/.local/share) for same-filesystem files, $top/.Trash/$uid (must be a sticky non-symlink directory) or $top/.Trash-$uid on other mounts, with a copy-into-home-trash fallback when the volume cannot host a trash directory.
Names are reserved atomically (create-new / O_EXCL on the .trashinfo), so concurrent invocations never clobber each other; collisions get name.2, name.3, ...
Putting a directory updates that trash dir’s FreeDesktop directorysizes cache (size mtime percent-encoded-name).
Putting ordinary files does not add directory lines.
rtrash empty [DAYS] (also trash-empty)
Purges every trash directory visible to the user (home trash plus mounted volumes).
With DAYS, only items trashed more than DAYS days ago go.
Entries are removed in parallel.
Orphaned files/ entries (no .trashinfo) and entries with broken metadata are purged on a full empty, and the directorysizes cache is pruned when present.
Options: -n/--dry-run (also prints an approximate reclaimable size via a fast in-process walk of the victims, like a small du of what would go away), -v/--verbose, --trash-dir=PATH (repeatable), -f (accepted for trash-cli compatibility; emptying never prompts).
rtrash status
Prints per-trash-root and total item count plus approximate reclaimable size (same disk-usage walk as empty dry-run).
Accepts --home-only and --trash-dir.
rtrash list (also trash-list)
Prints DELETION-DATE ORIGINAL-PATH per item, oldest first, in the trash-list output format (YYYY-MM-DD HH:MM:SS plus the original path).
Scans the home trash and per-mount trash directories owned by the current user, or only the directories given with --trash-dir=PATH (repeatable).
rtrash restore [PATH] (also trash-restore)
Restores the item trashed from PATH (or picks among items trashed from under the current directory when PATH is omitted).
A single match restores directly; multiple matches list with indices for interactive selection (requires a TTY on stdin).
Existing paths at the destination are preserved unless -f / --force is given; with -f, a blocking destination is removed first.
Same-filesystem restore uses rename; cross-device restore copies then deletes the trash payload (needed when put fell back to the home trash).
Options: --trash-dir=PATH (repeatable).
rtrash rm PATTERN... (also trash-rm)
Permanently deletes trash entries whose original path, basename, or trash name matches a shell-style glob PATTERN (quote globs from the shell).
Matching files/ payloads and .trashinfo files are removed; non-matches stay.
Does not restore.
Options: -v/--verbose, --trash-dir=PATH (repeatable).
FreeDesktop durability notes
Previously deferred items are implemented in the shipped put/empty path:
- Durable
.trashinfo: putfsyncs the reserved info file (and best-effort theinfo/dir) before moving the payload. - EXDEV fidelity: cross-device put/restore copies preserve content, symlink-as-link, mode, and mtime (not a bare content-only copy).
- Put/empty exclusion: each trash root takes an exclusive
flockon.rtrash.lockfor put and empty so the pair cannot tear mid-operation. - Btrfs multi-subvol topdir: volume topdir is the longest mount-point prefix from
/proc/self/mounts, not a purest_devparent walk. - Default multi-volume empty: with no
--trash-dir, empty/list/restore/rm discover home trash plus existing user trash on every non-pseudo mount (including/), matching trash-cli’s multi-volume default. Pin with--trash-dirin scripts.
Limitations and non-goals
- No pretty TUI: no colored tables or fuzzy restore (see trashy/gtrash).
- Linux FreeDesktop only: no Windows Recycle Bin / macOS Trash backends.
- Not a general soft-delete database: only the FreeDesktop on-disk layout.
- EXDEV does not re-create xattrs/ACLs/hardlinks (mode+mtime+symlink+bytes only).
- Locks are local
flock(advisory on some network FS).
Performance
Historical single-machine comparison against trash-cli 0.24.5.26 (CPython 3) on an NVMe-backed Linux host, best of warm runs. These numbers are not continuously re-verified in CI and will vary by filesystem, core count, and trash layout; treat them as order-of-magnitude evidence that native startup and parallel empty help, not as a live leaderboard.
| Operation | trash-cli | rtrash | ratio |
|---|---|---|---|
empty, 100 000 entries |
0.92 s | 0.36 s | 2.5x |
empty, 20 000 entries |
0.21 s | 0.07 s | 3x |
put, one file |
52 ms | 1 ms | 52x |
Full empty was further optimized for large trashcans: no pre-scan when not verbose, d_type-fast unlinkat for regular files, and serial wipes of the files/ then info/ roots with parallel children inside each.
Emptying uses an in-process bulk tree delete (readdir/unlinkat walk inspired by empty-source rsync --delete, not a shell-out to rsync).
On btrfs, if a trash payload is a real subvolume root, empty uses BTRFS_IOC_SNAP_DESTROY instead of walking the tree.
Interactive put mainly wins on process startup versus trash-cli.
Development
$ cargo test
Integration tests isolate trash under a temporary XDG_DATA_HOME and pin empty / list / restore / rm with --trash-dir=… so they never clear the host trash.
Citation
@software{rtrash,
author = {Goswami, Rohit},
title = {rtrash: a fast rm-compatible freedesktop.org trash tool},
url = {https://github.com/HaoZeke/rtrash},
year = {2026}
}
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rtrash-0.1.1-cp314-cp314-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rtrash-0.1.1-cp314-cp314-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 317.5 kB
- Tags: CPython 3.14, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
782c044042c28fb68785460fe0a4b4e513ae2bf7a0db8d694827cc600dce3fe2
|
|
| MD5 |
5391b01f4a67e10fe1ebe49a6deaa65f
|
|
| BLAKE2b-256 |
00b33fc78c42d1168f88cd63b7c35544bb3285036370d2d51ae5fd53f86aaa6d
|
File details
Details for the file rtrash-0.1.1-cp313-cp313-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rtrash-0.1.1-cp313-cp313-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 317.5 kB
- Tags: CPython 3.13, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8562eed3b9868cf39483f9d15aabe816277dc7c4d7de992eafde21667e3fc7e6
|
|
| MD5 |
ed23493bfad2c762d3655441db13f43d
|
|
| BLAKE2b-256 |
3abd0eff6c32fccb88692b36bfb45c1b9a6e1c13966f5830a1200ae235d0330e
|
File details
Details for the file rtrash-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rtrash-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 317.9 kB
- Tags: CPython 3.12, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80a6f3ffdcfa9bffce1e2d7e16ee7c5ec90d0f365b0f20f007e20320dcf3a81a
|
|
| MD5 |
1af26bd9297f73ffd734090866225aef
|
|
| BLAKE2b-256 |
bc10d62fc9adfb13430be7cafecb949fcc7e62f182b443c5e024c0e3273410dd
|
File details
Details for the file rtrash-0.1.1-cp311-cp311-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rtrash-0.1.1-cp311-cp311-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 318.6 kB
- Tags: CPython 3.11, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8460c95ef7110d4a9b968787c18153be84f5327afc7bffd710ccbf44b5949b2c
|
|
| MD5 |
5176ff20283d72bb8f7d9274a8592268
|
|
| BLAKE2b-256 |
fb8e08ea063be3ccd952cd111832295b35a3590353bb96b0c43a1b5f85361345
|
File details
Details for the file rtrash-0.1.1-cp310-cp310-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rtrash-0.1.1-cp310-cp310-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 318.9 kB
- Tags: CPython 3.10, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2865e7f0e8cded88ea4b8409c3745bdf613f818dc78efd5d41cc91b383f8cdf
|
|
| MD5 |
e21ecf6accdf804d34bdf6de18fe0f62
|
|
| BLAKE2b-256 |
be5350ad04df8eed40ac125ca890757a1bbf7881f8f16857fd999d136267510f
|