Compare commits

31 Commits

Author SHA1 Message Date
631843b3c5 Fix installation instructions 2025-12-09 16:15:13 +00:00
9c653e44a4 Fix release badge link 2025-12-09 15:15:19 +00:00
cdd3ba0cbd Merge pull request 'Update README and pyproject.toml' (#23) from update_resrm_20251209 into main
Reviewed-on: #23
2025-12-09 15:13:46 +00:00
eee00bb6ee Edit badges, update installation instructions, swap github.com entries to git.sysmd.uk
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 48s
2025-12-09 15:11:54 +00:00
f9586bbd0e Merge pull request 'Rename .github folder to .gitea. Use pre-commit directly instead of action' (#22) from rename_github_folder into main
Reviewed-on: #22
2025-12-09 13:23:09 +00:00
51a7001bf2 Rename .github folder to .gitea. Use pre-commit directly instead of action
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 47s
2025-12-09 13:19:46 +00:00
ccf383ebfb Remove .coverage and add that to the .gitignore 2025-12-03 11:58:46 +00:00
Marco D'Aleo
6670c79d47 Merge pull request #21 from guardutils/args_list_fix
Fix list flag to use the long name
2025-12-03 11:57:06 +00:00
3285fbaef4 Fix list flag to use the long name 2025-12-03 11:55:15 +00:00
Marco D'Aleo
c07b7598d0 Merge pull request #20 from guardutils/update_resrm_20251202
Tab completion
2025-12-02 18:02:56 +00:00
9edae1d233 Add tab completion using argcomplete, update README 2025-12-02 18:01:17 +00:00
e36ac044d9 Update badges URLs 2025-11-29 16:43:12 +00:00
Marco D'Aleo
1ad635e37e Merge pull request #19 from guardutils/update_resrm_20251127
Switch ownership from mdaleo404 to guardutils in README and pyproject
2025-11-27 17:56:39 +00:00
649e16c03a Switch ownership from mdaleo404 to guardutils in README and pyproject 2025-11-27 17:55:29 +00:00
Marco D'Aleo
fc02895965 Merge pull request #18 from mdaleo404/add_badges_to_readme
Add badges to README
2025-11-23 07:32:05 +00:00
feb0d313e8 Add badges to README 2025-11-23 07:29:54 +00:00
af6c7a0797 Fix README 2025-11-17 19:05:32 +00:00
ccaa2dcb25 Update README with new installation methods 2025-11-17 18:56:51 +00:00
Marco D'Aleo
5bb1437a49 Merge pull request #17 from mdaleo404/update_resrm_20251117
Rename workflow and make it trigger on pull requests
2025-11-17 15:10:09 +00:00
3c4bbcbc34 Fix typo in workflow's file name 2025-11-17 15:08:51 +00:00
ba29cc590d Rename workflow and make it trigger on pull requests 2025-11-17 15:06:57 +00:00
Marco D'Aleo
2fd6fbb2c2 Merge pull request #16 from mig5/mig/fix-args-force
Use args.force, not args.f
2025-11-17 14:56:00 +00:00
Miguel Jacq
96f7ebf4fc Use args.force, not args.f 2025-11-17 14:11:32 +11:00
7ba20632ab Change Python dependecies version. Remove Black target-version from pyproject.toml.Remove CI pull_request trigger. 2025-11-16 14:53:57 +00:00
f46e699420 Add more checks on pre-commit-config, add CI workflow 2025-11-16 07:04:13 +00:00
Marco D'Aleo
b3aff6d8c5 Merge pull request #15 from mdaleo404/dynamic_version
Add function to fetch package version from pyproject.toml
2025-11-15 18:19:22 +00:00
2013e6b645 Add function to fetch package version from pyproject.toml" 2025-11-15 18:16:50 +00:00
Marco D'Aleo
6a73270f23 Merge pull request #14 from mdaleo404/remove_dev_dependencies
Remove bandit and black from pyproject.toml
2025-11-15 16:57:17 +00:00
4f1e5043fd Remove bandit and black from pyproject.toml 2025-11-15 16:56:33 +00:00
877a490b57 Adjust bandit's severity and confidence levels 2025-11-15 08:27:04 +00:00
Marco D'Aleo
bcad54d94e Merge pull request #13 from mdaleo404/update_resrm_20251115
Add pre-commit framework and hooks config
- bandit
- black
- trailing-whitespace
- end-of-file-fixer
2025-11-15 08:06:31 +00:00
8 changed files with 177 additions and 139 deletions

View 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

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
__pycache__
.pytest_cache
dist
.coverage

View File

@@ -4,7 +4,7 @@ repos:
hooks:
- id: bandit
files: ^src/resrm/
args: ["-s", "B110,B112"]
args: ["-lll", "-iii", "-s", "B110,B112"]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.11.0
@@ -17,3 +17,5 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml

102
README.md
View File

@@ -1,3 +1,7 @@
[![Licence](https://img.shields.io/badge/GPL--3.0-orange?label=Licence)](https://git.sysmd.uk/guardutils/resrm/src/branch/main/LICENCE)
[![Gitea Release](https://img.shields.io/gitea/v/release/guardutils/resrm?gitea_url=https%3A%2F%2Fgit.sysmd.uk%2F&style=flat&color=orange&logo=gitea)](https://git.sysmd.uk/guardutils/resrm/releases)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-blue?logo=pre-commit&style=flat)](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**.
@@ -13,42 +17,91 @@ It moves files to a per-user _trash_ instead of permanently deleting them, while
- 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 regular `rm` command instead.
---
> Note: if you need immediate deletion, use the `--skip-trash` flag.
## 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
```
---
## 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
```
@@ -81,12 +134,31 @@ 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).

124
poetry.lock generated
View File

@@ -1,28 +1,18 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "bandit"
version = "1.8.6"
description = "Security oriented static analyser for python code."
name = "argcomplete"
version = "3.6.3"
description = "Bash tab completion for argparse"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
files = [
{file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"},
{file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"},
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
]
[package.dependencies]
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
PyYAML = ">=5.3.1"
rich = "*"
stevedore = ">=1.20.0"
[package.extras]
baseline = ["GitPython (>=3.1.30)"]
sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"]
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"]
toml = ["tomli (>=1.1.0)"]
yaml = ["PyYAML"]
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
[[package]]
name = "cfgv"
@@ -35,17 +25,6 @@ files = [
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.4.0"
@@ -82,40 +61,6 @@ files = [
[package.extras]
license = ["ukkonen"]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.10"
files = [
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins (>=0.5.0)"]
profiling = ["gprof2dot"]
rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -145,13 +90,13 @@ type = ["mypy (>=1.18.2)"]
[[package]]
name = "pre-commit"
version = "4.4.0"
version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.10"
python-versions = ">=3.9"
files = [
{file = "pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813"},
{file = "pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15"},
{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]
@@ -161,20 +106,6 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -258,32 +189,14 @@ files = [
]
[[package]]
name = "rich"
version = "14.2.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "stevedore"
version = "5.5.0"
description = "Manage dynamic plugins for Python applications"
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 = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"},
{file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"},
{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]]
@@ -301,6 +214,7 @@ files = [
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)"]
@@ -308,5 +222,5 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata]
lock-version = "2.0"
python-versions = "^3.13"
content-hash = "4e4e273560503f091b728a8a24ad6907439e0af42923b0105adc1a8f1d7ef9a3"
python-versions = ">=3.10,<4.0"
content-hash = "7f8ea4efe2d270a676fdd9c882c02f43b4b118bfe7a5fd6da1098ff4ec84ce3d"

View File

@@ -1,28 +1,26 @@
[tool.poetry]
name = "resrm"
version = "0.3.0"
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"
bandit = "^1.7"
black = "^25.0"
[tool.poetry.scripts]
resrm = "resrm.cli:main"
[tool.black]
line-length = 79
target-version = ["py313"]
[build-system]
requires = ["poetry-core"]

View File

@@ -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,11 +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:
@@ -377,20 +386,37 @@ def main(argv: Optional[List[str]] = None):
parser.add_argument(
"--skip-trash", action="store_true", help="permanent delete"
)
parser.add_argument(
restore_arg = parser.add_argument(
"--restore",
nargs="+",
metavar="item",
help="restore by id or basename",
)
parser.add_argument("-l", action="store_true", help="list trash")
# 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"
"-V", "--version", action="version", version=f"resrm {get_version()}"
)
argcomplete.autocomplete(parser)
args = parser.parse_args(argv)
# Always print docstring if -h or --help
@@ -398,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
@@ -428,7 +450,7 @@ 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