Compare commits

..

10 Commits

Author SHA1 Message Date
58c682d4d3 Merge pull request 'Improve trash output' (#24) from improve_trash_list into main
Reviewed-on: #24
2025-12-13 18:09:46 +00:00
45d9f5f6c8 Update README, version bump
All checks were successful
Lint & Security / precommit-and-security (pull_request) Successful in 1m2s
2025-12-13 18:06:59 +00:00
659a76f5c9 Make --empty delete dangling files in trash folder not associated with metadata file, edit completer function's name to be reusable 2025-12-13 18:04:55 +00:00
250077c592 Add --inspect flag 2025-12-13 17:34:35 +00:00
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
5 changed files with 172 additions and 39 deletions

View File

@@ -20,7 +20,7 @@ jobs:
run: pip install pre-commit
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
run: pre-commit run --all-files --color always
- name: Install pip-audit
run: pip install pip-audit

View File

@@ -1,8 +1,6 @@
[![License](https://img.shields.io/github/license/guardutils/resrm?style=flat)](LICENCE)
[![Language](https://img.shields.io/github/languages/top/guardutils/resrm.svg)](https://github.com/guardutils/resrm/)
[![GitHub Release](https://img.shields.io/github/v/release/guardutils/resrm?display_name=release&logo=github)](https://github.com/guardutils/resrm/releases)
[![PyPI - Version](https://img.shields.io/pypi/v/resrm?logo=pypi)](https://pypi.org/project/resrm/#history)
[![PyPI downloads](https://img.shields.io/pypi/dm/resrm.svg)](https://pypi.org/project/resrm/)
[![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
@@ -22,34 +20,63 @@ It moves files to a per-user _trash_ instead of permanently deleting them, while
> 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 package manager
### From GuardUtils package repo
This is the preferred method of installation.
**Ubuntu 22.04 and 24.04**
### 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 add-apt-repository ppa:mdaleo/resrm
sudo apt update
sudo apt install resrm
```
**Fedora 41, 42, 43**
### Fedora/RHEL
#### 1) Import the GPG key
```
sudo dnf copr enable mdaleo/resrm
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
```
@@ -74,7 +101,7 @@ pip install resrm
### From this repository
```bash
git clone https://github.com/guardutils/resrm.git
git clone https://git.sysmd.uk/guardutils/resrm.git
cd resrm/
poetry install
```
@@ -103,16 +130,28 @@ resrm -l
# Restore a file by ID or basename
resrm --restore <id|name>
# Show full details of trashed item
resrm --inspect <id|name>
# Empty the trash permanently
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`
```

View File

@@ -1,12 +1,12 @@
[tool.poetry]
name = "resrm"
version = "0.3.3"
version = "0.4.0"
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/guardutils/resrm"
repository = "https://github.com/guardutils/resrm"
homepage = "https://git.sysmd.uk/guardutils/resrm"
repository = "https://git.sysmd.uk/guardutils/resrm"
packages = [{include = "resrm", from = "src"}]
[tool.poetry.dependencies]

View File

@@ -10,6 +10,7 @@ Basic usage:
resrm --skip-trash file # permanent delete (bypass trash)
resrm -l|--list # list trash entries (neat table)
resrm --restore <id|name> # restore by short-id (8 chars) or exact basename
resrm --inspect <id|name> # output full detail list of trashed item
resrm --empty # empty trash entries (permanent)
"""
@@ -268,20 +269,23 @@ def restore(identifier: str):
def empty_trash():
"""Permanently remove all trashed files and clear metadata."""
# Remove everything inside the trash directory
count = 0
for entry in list(meta):
f = TRASH_DIR / entry["id"]
for item in TRASH_DIR.iterdir():
try:
if f.exists():
if f.is_dir():
shutil.rmtree(f, ignore_errors=True)
else:
f.unlink(missing_ok=True)
meta.remove(entry)
if item.is_dir():
shutil.rmtree(item, ignore_errors=True)
else:
item.unlink(missing_ok=True)
count += 1
except Exception as e:
print(f"Failed to remove {f}: {e}")
print(f"Failed to remove {item}: {e}")
# Clear metadata
meta.clear()
save_meta(meta)
print(f"Trash emptied ({count} entries removed).")
@@ -374,6 +378,79 @@ def move_to_trash(
print(f"Removed '{path}' -> trash id {short_id(uid)}")
def inspect_entry(identifier: str):
"""Show full information about trash entries matching the identifier."""
candidates = find_candidates(identifier)
if not candidates:
print(f"No match found for '{identifier}'")
return
for entry in candidates:
# Validate entry structure
if not isinstance(entry, dict):
print(f"Invalid metadata entry (not a dict): {entry!r}")
print()
continue
entry_id = entry.get("id")
orig_path = entry.get("orig_path", "?")
timestamp = entry.get("timestamp", "?")
if not entry_id:
print(f"Invalid metadata entry (missing id): {entry}")
continue
trash_path = TRASH_DIR / entry_id
print(f"ID: {short_id(entry_id)}")
print(f"Original: {orig_path}")
print(f"Deleted at: {human_time(timestamp)}")
print(f"Stored at: {trash_path}")
try:
st = trash_path.lstat() # preserves symlink info
import stat, pwd, grp
# Type detection
if stat.S_ISDIR(st.st_mode):
ftype = "directory"
elif stat.S_ISLNK(st.st_mode):
try:
target = os.readlink(trash_path)
ftype = f"symlink → {target}"
except Exception:
ftype = "symlink"
else:
ftype = "file"
# Permissions
perms = stat.filemode(st.st_mode)
# Ownership
try:
user = pwd.getpwuid(st.st_uid).pw_name
except Exception:
user = st.st_uid
try:
group = grp.getgrgid(st.st_gid).gr_name
except Exception:
group = st.st_gid
owner = f"{user}:{group}"
# Size (bytes for file, recursive for directories)
size = st.st_size
print(f"Type: {ftype}")
print(f"Size: {size} bytes")
print(f"Permissions: {perms}")
print(f"Ownership: {owner}")
except Exception as e:
print(f"Unknown stats for {e}")
def main(argv: Optional[List[str]] = None):
if argv is None:
argv = sys.argv[1:]
@@ -386,6 +463,15 @@ def main(argv: Optional[List[str]] = None):
parser.add_argument(
"--skip-trash", action="store_true", help="permanent delete"
)
inspect_arg = parser.add_argument(
"--inspect",
"-I",
nargs="+",
metavar="item",
help="show full metadata and original path for this trash entry",
)
restore_arg = parser.add_argument(
"--restore",
nargs="+",
@@ -393,8 +479,8 @@ def main(argv: Optional[List[str]] = None):
help="restore by id or basename",
)
# restore completer
def restore_completer(prefix, parsed_args, **kwargs):
# completer
def id_name_completer(prefix, parsed_args, **kwargs):
return [
short_id(m["id"])
for m in meta
@@ -405,7 +491,8 @@ def main(argv: Optional[List[str]] = None):
if Path(m["orig_path"]).name.startswith(prefix)
]
restore_arg.completer = restore_completer
restore_arg.completer = id_name_completer
inspect_arg.completer = id_name_completer
parser.add_argument("-l", "--list", action="store_true", help="list trash")
parser.add_argument(
"--empty", action="store_true", help="empty the trash permanently"
@@ -424,7 +511,9 @@ def main(argv: Optional[List[str]] = None):
print(__doc__)
return
if not args.paths and not (args.list or args.empty or args.restore):
if not args.paths and not (
args.list or args.empty or args.restore or args.inspect
):
print("resrm: missing operand")
print("Try 'resrm --help' for more information.")
return
@@ -433,6 +522,11 @@ def main(argv: Optional[List[str]] = None):
list_trash()
return
if args.inspect:
for item in args.inspect:
inspect_entry(item)
return
if args.empty:
empty_trash()
return