From 250077c592aacd016f7b586fa5dc530bec7a6cfd Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sat, 13 Dec 2025 17:34:35 +0000 Subject: [PATCH 1/3] Add --inspect flag --- src/resrm/core.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src/resrm/core.py b/src/resrm/core.py index d00c65f..e62a7e6 100644 --- a/src/resrm/core.py +++ b/src/resrm/core.py @@ -10,6 +10,7 @@ Basic usage: resrm --skip-trash file # permanent delete (bypass trash) resrm -l|--list # list trash entries (neat table) resrm --restore # restore by short-id (8 chars) or exact basename + resrm --inspect # output full detail list of trashed item resrm --empty # empty trash entries (permanent) """ @@ -374,6 +375,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 optional) + 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 +460,15 @@ def main(argv: Optional[List[str]] = None): parser.add_argument( "--skip-trash", action="store_true", help="permanent delete" ) + + 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="+", @@ -424,7 +507,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 +518,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 -- 2.49.1 From 659a76f5c9c576c46aa0d091f58f49688971a0de Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sat, 13 Dec 2025 18:04:55 +0000 Subject: [PATCH 2/3] Make --empty delete dangling files in trash folder not associated with metadata file, edit completer function's name to be reusable --- src/resrm/core.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/resrm/core.py b/src/resrm/core.py index e62a7e6..aa185fe 100644 --- a/src/resrm/core.py +++ b/src/resrm/core.py @@ -269,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).") @@ -436,7 +439,7 @@ def inspect_entry(identifier: str): group = st.st_gid owner = f"{user}:{group}" - # Size (bytes for file, recursive for directories optional) + # Size (bytes for file, recursive for directories) size = st.st_size print(f"Type: {ftype}") @@ -445,7 +448,7 @@ def inspect_entry(identifier: str): print(f"Ownership: {owner}") except Exception as e: - print(f"Unknown stats for {e})") + print(f"Unknown stats for {e}") def main(argv: Optional[List[str]] = None): @@ -461,7 +464,7 @@ def main(argv: Optional[List[str]] = None): "--skip-trash", action="store_true", help="permanent delete" ) - parser.add_argument( + inspect_arg = parser.add_argument( "--inspect", "-I", nargs="+", @@ -476,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 @@ -488,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" -- 2.49.1 From 45d9f5f6c89e8764f0eb8764c4a2dbf7300e6ada Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sat, 13 Dec 2025 18:06:59 +0000 Subject: [PATCH 3/3] Update README, version bump --- README.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aab606d..2576382 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,9 @@ resrm -l # Restore a file by ID or basename resrm --restore +# Show full details of trashed item +resrm --inspect + # Empty the trash permanently resrm --empty ``` diff --git a/pyproject.toml b/pyproject.toml index 76bafdf..1d0eb67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [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 "] license = "GPL-3.0-or-later" -- 2.49.1