Compare commits
37 Commits
flags_impr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
631843b3c5
|
|||
|
9c653e44a4
|
|||
| cdd3ba0cbd | |||
|
eee00bb6ee
|
|||
| f9586bbd0e | |||
|
51a7001bf2
|
|||
|
ccf383ebfb
|
|||
|
|
6670c79d47 | ||
|
3285fbaef4
|
|||
|
|
c07b7598d0 | ||
|
9edae1d233
|
|||
|
e36ac044d9
|
|||
|
|
1ad635e37e | ||
|
649e16c03a
|
|||
|
|
fc02895965 | ||
| feb0d313e8 | |||
| af6c7a0797 | |||
| ccaa2dcb25 | |||
|
|
5bb1437a49 | ||
| 3c4bbcbc34 | |||
| ba29cc590d | |||
|
|
2fd6fbb2c2 | ||
|
|
96f7ebf4fc | ||
| 7ba20632ab | |||
| f46e699420 | |||
|
|
b3aff6d8c5 | ||
| 2013e6b645 | |||
|
|
6a73270f23 | ||
| 4f1e5043fd | |||
| 877a490b57 | |||
|
|
bcad54d94e | ||
| 80fb24b7c5 | |||
| 5f8d6c03d8 | |||
|
|
be9867a007 | ||
| e76c8726a2 | |||
| 657f14d95d | |||
|
|
b6c6fcc14d |
29
.gitea/workflows/lint-and-security.yml
Normal file
29
.gitea/workflows/lint-and-security.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Lint & Security
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
precommit-and-security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --all-files --color always
|
||||
|
||||
- name: Install pip-audit
|
||||
run: pip install pip-audit
|
||||
|
||||
- name: Run pip-audit
|
||||
run: pip-audit
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
dist
|
||||
dist
|
||||
.coverage
|
||||
|
||||
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
repos:
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.9
|
||||
hooks:
|
||||
- id: bandit
|
||||
files: ^src/resrm/
|
||||
args: ["-lll", "-iii", "-s", "B110,B112"]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.13
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
118
README.md
118
README.md
@@ -1,42 +1,107 @@
|
||||
[](https://git.sysmd.uk/guardutils/resrm/src/branch/main/LICENCE)
|
||||
[](https://git.sysmd.uk/guardutils/resrm/releases)
|
||||
[](https://git.sysmd.uk/guardutils/resrm/src/branch/main/.pre-commit-config.yaml)
|
||||
|
||||
# resrm
|
||||
|
||||
**resrm** is a safe, drop-in replacement for the Linux `rm` command with **undo/restore support**.
|
||||
**resrm** is a safe, drop-in replacement for the Linux `rm` command with **undo/restore support**.
|
||||
It moves files to a per-user _trash_ instead of permanently deleting them, while still allowing full `sudo` support for root-owned files.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Move files and directories to a **trash folder** instead of permanent deletion
|
||||
- Restore deleted files by **short ID or exact basename**
|
||||
- Empty trash safely
|
||||
- Supports `-r`, `-f`, `-i`, `--perma` options
|
||||
- Works with `sudo` for root-owned files
|
||||
- Move files and directories to a **Trash folder** instead of permanent deletion
|
||||
- Restore deleted files by **short ID or exact basename**
|
||||
- Empty trash safely
|
||||
- Supports `-r`, `-f`, `-i`, `--skip-trash` options
|
||||
- Works with `sudo` for root-owned files
|
||||
- Automatically prunes Trash entries older than `$RESRM_TRASH_LIFE` days (default **7**, minimum **1**)
|
||||
|
||||
> Note: if you need immediate deletion, use the `--skip-trash` flag.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### From GuardUtils package repo
|
||||
|
||||
This is the preferred method of installation.
|
||||
|
||||
### Debian/Ubuntu
|
||||
|
||||
#### 1) Import the GPG key
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /usr/share/keyrings
|
||||
curl -fsSL https://repo.sysmd.uk/guardutils/guardutils.gpg | sudo gpg --dearmor -o /usr/share/keyrings/guardutils.gpg
|
||||
```
|
||||
|
||||
The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
|
||||
|
||||
#### 2) Add the APT source
|
||||
|
||||
```bash
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/guardutils.gpg] https://repo.sysmd.uk/guardutils/debian stable main" | sudo tee /etc/apt/sources.list.d/guardutils.list
|
||||
```
|
||||
|
||||
#### 3) Update and install
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install resrm
|
||||
```
|
||||
|
||||
### Fedora/RHEL
|
||||
|
||||
#### 1) Import the GPG key
|
||||
|
||||
```
|
||||
sudo rpm --import https://repo.sysmd.uk/guardutils/guardutils.gpg
|
||||
```
|
||||
|
||||
#### 2) Add the repository configuration
|
||||
|
||||
```
|
||||
sudo tee /etc/yum.repos.d/guardutils.repo > /dev/null << 'EOF'
|
||||
[guardutils]
|
||||
name=GuardUtils Repository
|
||||
baseurl=https://repo.sysmd.uk/guardutils/rpm/$basearch
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1
|
||||
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 4) Update and install
|
||||
|
||||
```
|
||||
sudo dnf upgrade --refresh
|
||||
sudo dnf install resrm
|
||||
```
|
||||
|
||||
### From PyPI
|
||||
|
||||
**NOTE:** To use `resrm` with `sudo`, the path to `resrm` must be in the `$PATH` seen by `root`.\
|
||||
Either:
|
||||
|
||||
* install `resrm` as `root` (_preferred_), or
|
||||
* install `resrm` as `root`, or
|
||||
* add the path to `resrm` to the `secure_path` parameter in `/etc/sudoers`. For example, where `/home/user/.local/bin` is where `resrm` is:
|
||||
|
||||
``` bash
|
||||
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/user/.local/bin"
|
||||
```
|
||||
|
||||
Install via PyPI (_preferred_):
|
||||
Install with:
|
||||
|
||||
```bash
|
||||
pip install resrm
|
||||
```
|
||||
|
||||
Or clone the repo and install locally:
|
||||
### From this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mdaleo404/resrm.git
|
||||
git clone https://git.sysmd.uk/guardutils/resrm.git
|
||||
cd resrm/
|
||||
poetry install
|
||||
```
|
||||
@@ -57,7 +122,7 @@ resrm -f file
|
||||
resrm -i file
|
||||
|
||||
# Permanent delete (bypass trash)
|
||||
resrm --perma file
|
||||
resrm --skip-trash file
|
||||
|
||||
# List trash entries
|
||||
resrm -l
|
||||
@@ -69,8 +134,37 @@ resrm --restore <id|name>
|
||||
resrm --empty
|
||||
```
|
||||
|
||||
|
||||
## Trash Location
|
||||
|
||||
Normal users: `~/.local/share/resrm/files`
|
||||
|
||||
Root user: `/root/.local/share/resrm/files`
|
||||
|
||||
## Configuration
|
||||
|
||||
To control how long trashed files are kept, add this line to your shell configuration (e.g. `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export RESRM_TRASH_LIFE=10
|
||||
```
|
||||
|
||||
### TAB completion
|
||||
Add this to your `.bashrc`
|
||||
```
|
||||
eval "$(register-python-argcomplete resrm)"
|
||||
```
|
||||
And then
|
||||
```
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
## pre-commit
|
||||
This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks).
|
||||
|
||||
To enable it:
|
||||
```
|
||||
poetry install
|
||||
poetry run pre-commit install
|
||||
```
|
||||
This ensures consistent formatting, catches common issues early, and keeps the codebase clean.
|
||||
|
||||
225
poetry.lock
generated
225
poetry.lock
generated
@@ -1,7 +1,226 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
package = []
|
||||
|
||||
[[package]]
|
||||
name = "argcomplete"
|
||||
version = "3.6.3"
|
||||
description = "Bash tab completion for argparse"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
|
||||
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
description = "Validate configuration and produce human readable error messages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"},
|
||||
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"},
|
||||
{file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.15"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"},
|
||||
{file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
|
||||
type = ["mypy (>=1.18.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.8.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cfgv = ">=2.0.0"
|
||||
identify = ">=1.0.0"
|
||||
nodeenv = ">=0.11.1"
|
||||
pyyaml = ">=5.1"
|
||||
virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"},
|
||||
{file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.13"
|
||||
content-hash = "f01b553f3895e558c34b4f10542e05acdef39bf0527c8090bd136d914dc73f94"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "7f8ea4efe2d270a676fdd9c882c02f43b4b118bfe7a5fd6da1098ff4ec84ce3d"
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
[tool.poetry]
|
||||
name = "resrm"
|
||||
version = "0.2.1"
|
||||
version = "0.3.3"
|
||||
description = "drop-in replacement for rm with undo/restore built-in."
|
||||
authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/mdaleo404/resrm"
|
||||
repository = "https://github.com/mdaleo404/resrm"
|
||||
homepage = "https://git.sysmd.uk/guardutils/resrm"
|
||||
repository = "https://git.sysmd.uk/guardutils/resrm"
|
||||
packages = [{include = "resrm", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.13"
|
||||
python = ">=3.10,<4.0"
|
||||
argcomplete = ">=2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pre-commit = "^3.8"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
resrm = "resrm.cli:main"
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -1 +1 @@
|
||||
from resrm.core import main
|
||||
from resrm.core import main
|
||||
|
||||
@@ -8,13 +8,14 @@ Basic usage:
|
||||
resrm -f file # ignore nonexistent, no prompt
|
||||
resrm -i file # interactive prompt before removal
|
||||
resrm --skip-trash file # permanent delete (bypass trash)
|
||||
resrm -l # list trash entries (neat table)
|
||||
resrm -l|--list # list trash entries (neat table)
|
||||
resrm --restore <id|name> # restore by short-id (8 chars) or exact basename
|
||||
resrm --empty # empty trash entries (permanent)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import argcomplete
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -22,10 +23,19 @@ import sys
|
||||
import uuid
|
||||
import datetime
|
||||
import textwrap
|
||||
import importlib.metadata
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
# Config
|
||||
def get_version():
|
||||
try:
|
||||
return importlib.metadata.version("resrm")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_trash_base_for_user(uid: int) -> Path:
|
||||
"""Return the trash base path depending on whether user is root or normal."""
|
||||
if uid == 0:
|
||||
@@ -33,6 +43,7 @@ def get_trash_base_for_user(uid: int) -> Path:
|
||||
else:
|
||||
try:
|
||||
import pwd
|
||||
|
||||
user_info = pwd.getpwuid(uid)
|
||||
home_dir = Path(user_info.pw_dir)
|
||||
except Exception:
|
||||
@@ -53,6 +64,46 @@ def get_trash_paths() -> tuple[Path, Path]:
|
||||
TRASH_DIR, META_FILE = get_trash_paths()
|
||||
DATEFMT = "%Y-%m-%d %H:%M"
|
||||
|
||||
|
||||
def prune_old_trash():
|
||||
"""Remove trash entries older than RESRM_TRASH_LIFE days (default 7)."""
|
||||
try:
|
||||
life_days = int(os.environ.get("RESRM_TRASH_LIFE", "7"))
|
||||
except ValueError:
|
||||
life_days = 7
|
||||
|
||||
if life_days < 1:
|
||||
life_days = 1
|
||||
|
||||
cutoff = datetime.datetime.now() - datetime.timedelta(days=life_days)
|
||||
removed = 0
|
||||
|
||||
for entry in list(meta): # make copy since we'll modify meta
|
||||
try:
|
||||
ts = datetime.datetime.fromisoformat(entry["timestamp"])
|
||||
except Exception:
|
||||
continue # skip malformed entries
|
||||
|
||||
if ts < cutoff:
|
||||
f = TRASH_DIR / entry["id"]
|
||||
try:
|
||||
if f.exists():
|
||||
if f.is_dir():
|
||||
shutil.rmtree(f, ignore_errors=True)
|
||||
else:
|
||||
f.unlink(missing_ok=True)
|
||||
meta.remove(entry)
|
||||
removed += 1
|
||||
except Exception as e:
|
||||
print(f"Failed to prune {f}: {e}")
|
||||
|
||||
if removed > 0:
|
||||
save_meta(meta)
|
||||
print(
|
||||
f"Pruned {removed} trash entr{'y' if removed == 1 else 'ies'} older than {life_days} da{'y' if life_days == 1 else 'ys'}."
|
||||
)
|
||||
|
||||
|
||||
def load_meta() -> List[Dict]:
|
||||
if META_FILE.exists():
|
||||
try:
|
||||
@@ -62,15 +113,19 @@ def load_meta() -> List[Dict]:
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def save_meta(meta: List[Dict]):
|
||||
with META_FILE.open("w", encoding="utf-8") as f:
|
||||
json.dump(meta, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
meta = load_meta()
|
||||
|
||||
|
||||
def short_id(fullid: str) -> str:
|
||||
return fullid[:8]
|
||||
|
||||
|
||||
def human_time(ts: str) -> str:
|
||||
"""
|
||||
Convert ISO timestamp string from metadata to a human-readable format.
|
||||
@@ -82,13 +137,15 @@ def human_time(ts: str) -> str:
|
||||
# Fallback: just return the raw string
|
||||
return ts
|
||||
|
||||
|
||||
def entry_display(entry: Dict, width: int = 80) -> str:
|
||||
id8 = short_id(entry["id"])
|
||||
ts = human_time(entry["timestamp"])
|
||||
path = entry["orig_path"]
|
||||
wrapped = textwrap.fill(path, width=width-32)
|
||||
wrapped = textwrap.fill(path, width=width - 32)
|
||||
return f"{id8:<8} {ts:<19} {wrapped}"
|
||||
|
||||
|
||||
def list_trash():
|
||||
if not meta:
|
||||
print("Trash empty.")
|
||||
@@ -96,16 +153,17 @@ def list_trash():
|
||||
|
||||
header = f"{'ID':<8} {'Deleted at':<19} {'Original path'}"
|
||||
print(header)
|
||||
print('-' * len(header))
|
||||
print("-" * len(header))
|
||||
for entry in meta:
|
||||
id8 = short_id(entry["id"])
|
||||
ts = human_time(entry["timestamp"])
|
||||
path = entry["orig_path"]
|
||||
max_path_len = 80
|
||||
if len(path) > max_path_len:
|
||||
path = "…" + path[-(max_path_len - 1):]
|
||||
path = "…" + path[-(max_path_len - 1) :]
|
||||
print(f"{id8:<8} {ts:<19} {path}")
|
||||
|
||||
|
||||
def find_candidates(identifier: str) -> List[Dict]:
|
||||
# exact basename match first
|
||||
exact = [m for m in meta if Path(m["orig_path"]).name == identifier]
|
||||
@@ -118,6 +176,7 @@ def find_candidates(identifier: str) -> List[Dict]:
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def restore_many(identifiers: List[str]):
|
||||
"""Restore multiple files, prompting when needed."""
|
||||
for identifier in identifiers:
|
||||
@@ -135,7 +194,9 @@ def restore_many(identifiers: List[str]):
|
||||
# Multiple matches - prompt user
|
||||
print(f"Multiple matches for '{identifier}':")
|
||||
for i, entry in enumerate(candidates, start=1):
|
||||
print(f"{i}) {short_id(entry['id'])} {entry['orig_path']} ({entry['timestamp']})")
|
||||
print(
|
||||
f"{i}) {short_id(entry['id'])} {entry['orig_path']} ({entry['timestamp']})"
|
||||
)
|
||||
|
||||
try:
|
||||
choice = input("Choose number to restore (or skip): ").strip()
|
||||
@@ -153,6 +214,7 @@ def restore_many(identifiers: List[str]):
|
||||
else:
|
||||
print("Invalid selection. Skipped.")
|
||||
|
||||
|
||||
def restore_one(entry: Dict) -> bool:
|
||||
src = TRASH_DIR / entry["id"]
|
||||
dest = Path(entry["orig_path"])
|
||||
@@ -174,6 +236,7 @@ def restore_one(entry: Dict) -> bool:
|
||||
print(f"Restored to: {dest}")
|
||||
return True
|
||||
|
||||
|
||||
def restore(identifier: str):
|
||||
candidates = find_candidates(identifier)
|
||||
if not candidates:
|
||||
@@ -185,7 +248,9 @@ def restore(identifier: str):
|
||||
# multiple candidates -> show list and ask
|
||||
print("Multiple matches:")
|
||||
for i, e in enumerate(candidates, start=1):
|
||||
print(f"{i}) {short_id(e['id'])} {e['orig_path']} ({e['timestamp']})")
|
||||
print(
|
||||
f"{i}) {short_id(e['id'])} {e['orig_path']} ({e['timestamp']})"
|
||||
)
|
||||
try:
|
||||
choice = input("Choose number to restore (or abort): ").strip()
|
||||
except KeyboardInterrupt:
|
||||
@@ -200,6 +265,7 @@ def restore(identifier: str):
|
||||
return
|
||||
restore_one(candidates[idx])
|
||||
|
||||
|
||||
def empty_trash():
|
||||
"""Permanently remove all trashed files and clear metadata."""
|
||||
count = 0
|
||||
@@ -218,7 +284,10 @@ def empty_trash():
|
||||
save_meta(meta)
|
||||
print(f"Trash emptied ({count} entries removed).")
|
||||
|
||||
def move_to_trash(path: Path, interactive: bool, force: bool, skip_trash: bool):
|
||||
|
||||
def move_to_trash(
|
||||
path: Path, interactive: bool, force: bool, skip_trash: bool
|
||||
):
|
||||
if not path.exists():
|
||||
if force:
|
||||
return
|
||||
@@ -250,7 +319,9 @@ def move_to_trash(path: Path, interactive: bool, force: bool, skip_trash: bool):
|
||||
try:
|
||||
st = path.stat()
|
||||
if st.st_uid == 0 and os.geteuid() != 0:
|
||||
print(f"resrm: permission denied: '{path}' (root-owned file, try sudo)")
|
||||
print(
|
||||
f"resrm: permission denied: '{path}' (root-owned file, try sudo)"
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
@@ -258,6 +329,7 @@ def move_to_trash(path: Path, interactive: bool, force: bool, skip_trash: bool):
|
||||
# Detect which trash to use (based on file owner)
|
||||
try:
|
||||
import pwd
|
||||
|
||||
owner_uid = path.stat().st_uid
|
||||
owner_info = pwd.getpwuid(owner_uid)
|
||||
owner_home = Path(owner_info.pw_dir)
|
||||
@@ -293,7 +365,7 @@ def move_to_trash(path: Path, interactive: bool, force: bool, skip_trash: bool):
|
||||
entry = {
|
||||
"id": uid,
|
||||
"orig_path": str(path.resolve()),
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
}
|
||||
owner_meta.append(entry)
|
||||
with meta_file.open("w", encoding="utf-8") as f:
|
||||
@@ -301,20 +373,50 @@ def move_to_trash(path: Path, interactive: bool, force: bool, skip_trash: bool):
|
||||
|
||||
print(f"Removed '{path}' -> trash id {short_id(uid)}")
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
prune_old_trash()
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("paths", nargs="*", help="files to remove")
|
||||
parser.add_argument("-r", action="store_true", help="recursive")
|
||||
parser.add_argument("-f", "--force", action="store_true", help="force")
|
||||
parser.add_argument("-i", action="store_true", help="interactive")
|
||||
parser.add_argument("--skip-trash", action="store_true", help="permanent delete")
|
||||
parser.add_argument("--restore", nargs="+", metavar="item", help="restore by id or basename")
|
||||
parser.add_argument("-l", action="store_true", help="list trash")
|
||||
parser.add_argument("--empty", action="store_true", help="empty the trash permanently")
|
||||
parser.add_argument(
|
||||
"--skip-trash", action="store_true", help="permanent delete"
|
||||
)
|
||||
restore_arg = parser.add_argument(
|
||||
"--restore",
|
||||
nargs="+",
|
||||
metavar="item",
|
||||
help="restore by id or basename",
|
||||
)
|
||||
|
||||
# restore completer
|
||||
def restore_completer(prefix, parsed_args, **kwargs):
|
||||
return [
|
||||
short_id(m["id"])
|
||||
for m in meta
|
||||
if short_id(m["id"]).startswith(prefix)
|
||||
] + [
|
||||
Path(m["orig_path"]).name
|
||||
for m in meta
|
||||
if Path(m["orig_path"]).name.startswith(prefix)
|
||||
]
|
||||
|
||||
restore_arg.completer = restore_completer
|
||||
parser.add_argument("-l", "--list", action="store_true", help="list trash")
|
||||
parser.add_argument(
|
||||
"--empty", action="store_true", help="empty the trash permanently"
|
||||
)
|
||||
parser.add_argument("-h", "--help", action="store_true", help="show help")
|
||||
parser.add_argument("-V", "--version", action="store_true", help="show version")
|
||||
parser.add_argument(
|
||||
"-V", "--version", action="version", version=f"resrm {get_version()}"
|
||||
)
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Always print docstring if -h or --help
|
||||
@@ -322,16 +424,12 @@ def main(argv: Optional[List[str]] = None):
|
||||
print(__doc__)
|
||||
return
|
||||
|
||||
if args.version:
|
||||
print("resrm 0.2.1")
|
||||
return
|
||||
|
||||
if not args.paths and not (args.l or args.empty or args.restore):
|
||||
if not args.paths and not (args.list or args.empty or args.restore):
|
||||
print("resrm: missing operand")
|
||||
print("Try 'resrm --help' for more information.")
|
||||
return
|
||||
|
||||
if args.l:
|
||||
if args.list:
|
||||
list_trash()
|
||||
return
|
||||
|
||||
@@ -352,8 +450,13 @@ def main(argv: Optional[List[str]] = None):
|
||||
pth = Path(p)
|
||||
# simplistic recursive handling: if -r not given and it's a directory, mimic rm behavior: error unless -r
|
||||
if pth.is_dir() and not args.r:
|
||||
if args.f:
|
||||
if args.force:
|
||||
continue
|
||||
print(f"resrm: cannot remove '{pth}': Is a directory")
|
||||
continue
|
||||
move_to_trash(pth, interactive=args.i, force=args.force, skip_trash=args.skip_trash)
|
||||
move_to_trash(
|
||||
pth,
|
||||
interactive=args.i,
|
||||
force=args.force,
|
||||
skip_trash=args.skip_trash,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user