Files
Medios-Macina/cmdlets/delete_file.py

250 lines
10 KiB
Python
Raw Normal View History

2025-12-11 12:47:30 -08:00
"""Delete-file cmdlet: Delete files from local storage and/or Hydrus."""
2025-11-25 20:09:33 -08:00
from __future__ import annotations
from typing import Any, Dict, Sequence
import sys
from pathlib import Path
2025-12-11 12:47:30 -08:00
from helper.logger import debug, log
from helper.store import Folder
from ._shared import Cmdlet, CmdletArg, normalize_hash, looks_like_hash, get_origin, get_field, should_show_help
2025-11-25 20:09:33 -08:00
from helper import hydrus as hydrus_wrapper
2025-12-11 12:47:30 -08:00
import pipeline as ctx
2025-12-01 01:10:16 -08:00
2025-12-11 12:47:30 -08:00
class Delete_File(Cmdlet):
"""Class-based delete-file cmdlet with self-registration."""
def __init__(self) -> None:
super().__init__(
name="delete-file",
summary="Delete a file locally and/or from Hydrus, including database entries.",
usage="delete-file [-hash <sha256>] [-conserve <local|hydrus>] [-lib-root <path>] [reason]",
alias=["del-file"],
arg=[
CmdletArg("hash", description="Override the Hydrus file hash (SHA256) to target instead of the selected result."),
CmdletArg("conserve", description="Choose which copy to keep: 'local' or 'hydrus'."),
CmdletArg("lib-root", description="Path to local library root for database cleanup."),
CmdletArg("reason", description="Optional reason for deletion (free text)."),
],
detail=[
"Default removes both the local file and Hydrus file.",
"Use -conserve local to keep the local file, or -conserve hydrus to keep it in Hydrus.",
"Database entries are automatically cleaned up for local files.",
"Any remaining arguments are treated as the Hydrus reason text.",
],
exec=self.run,
)
self.register()
def _process_single_item(self, item: Any, override_hash: str | None, conserve: str | None,
lib_root: str | None, reason: str, config: Dict[str, Any]) -> bool:
"""Process deletion for a single item."""
# Handle item as either dict or object
if isinstance(item, dict):
hash_hex_raw = item.get("hash_hex") or item.get("hash")
target = item.get("target") or item.get("file_path") or item.get("path")
else:
hash_hex_raw = get_field(item, "hash_hex") or get_field(item, "hash")
target = get_field(item, "target") or get_field(item, "file_path") or get_field(item, "path")
2025-11-25 20:09:33 -08:00
2025-12-11 12:47:30 -08:00
origin = get_origin(item)
2025-11-25 20:09:33 -08:00
2025-12-11 12:47:30 -08:00
# Also check the store field explicitly from PipeObject
store = None
if isinstance(item, dict):
store = item.get("store")
else:
store = get_field(item, "store")
2025-12-01 01:10:16 -08:00
2025-12-11 12:47:30 -08:00
# For Hydrus files, the target IS the hash
if origin and origin.lower() == "hydrus" and not hash_hex_raw:
hash_hex_raw = target
2025-11-25 20:09:33 -08:00
2025-12-11 12:47:30 -08:00
hash_hex = normalize_hash(override_hash) if override_hash else normalize_hash(hash_hex_raw)
2025-11-25 20:09:33 -08:00
2025-12-11 12:47:30 -08:00
local_deleted = False
local_target = isinstance(target, str) and target.strip() and not str(target).lower().startswith(("http://", "https://"))
2025-11-25 20:09:33 -08:00
2025-12-11 12:47:30 -08:00
if conserve != "local" and local_target:
path = Path(str(target))
# If lib_root is provided and this is from a folder store, use the Folder class
if lib_root:
try:
folder = Folder(Path(lib_root), name=origin or "local")
if folder.delete_file(str(path)):
local_deleted = True
ctx.emit(f"Removed file: {path.name}")
log(f"Deleted: {path.name}", file=sys.stderr)
except Exception as exc:
debug(f"Folder.delete_file failed: {exc}", file=sys.stderr)
# Fallback to manual deletion
try:
if path.exists() and path.is_file():
path.unlink()
local_deleted = True
ctx.emit(f"Removed local file: {path}")
log(f"Deleted: {path.name}", file=sys.stderr)
except Exception as exc:
log(f"Local delete failed: {exc}", file=sys.stderr)
else:
# No lib_root, just delete the file
try:
if path.exists() and path.is_file():
path.unlink()
local_deleted = True
ctx.emit(f"Removed local file: {path}")
log(f"Deleted: {path.name}", file=sys.stderr)
except Exception as exc:
log(f"Local delete failed: {exc}", file=sys.stderr)
2025-12-01 01:10:16 -08:00
2025-12-11 12:47:30 -08:00
# Remove common sidecars regardless of file removal success
for sidecar in (path.with_suffix(".tags"), path.with_suffix(".tags.txt"),
path.with_suffix(".metadata"), path.with_suffix(".notes")):
2025-12-01 01:10:16 -08:00
try:
2025-12-11 12:47:30 -08:00
if sidecar.exists() and sidecar.is_file():
sidecar.unlink()
2025-12-01 01:10:16 -08:00
except Exception:
pass
2025-12-11 12:47:30 -08:00
hydrus_deleted = False
# Only attempt Hydrus deletion if store is explicitly Hydrus-related
# Check both origin and store fields to determine if this is a Hydrus file
2025-12-01 01:10:16 -08:00
should_try_hydrus = False
2025-12-11 12:47:30 -08:00
# Check if store indicates this is a Hydrus backend
if store and ("hydrus" in store.lower() or store.lower() == "home" or store.lower() == "work"):
should_try_hydrus = True
# Fallback to origin check if store not available
elif origin and origin.lower() == "hydrus":
should_try_hydrus = True
# If conserve is set to hydrus, definitely don't delete
if conserve == "hydrus":
should_try_hydrus = False
if should_try_hydrus and hash_hex:
try:
client = hydrus_wrapper.get_client(config)
except Exception as exc:
2025-11-25 20:09:33 -08:00
if not local_deleted:
2025-12-11 12:47:30 -08:00
log(f"Hydrus client unavailable: {exc}", file=sys.stderr)
2025-11-27 10:59:01 -08:00
return False
2025-11-25 20:09:33 -08:00
else:
2025-12-11 12:47:30 -08:00
if client is None:
2025-11-25 20:09:33 -08:00
if not local_deleted:
2025-12-11 12:47:30 -08:00
log("Hydrus client unavailable", file=sys.stderr)
2025-11-27 10:59:01 -08:00
return False
2025-12-11 12:47:30 -08:00
else:
payload: Dict[str, Any] = {"hashes": [hash_hex]}
if reason:
payload["reason"] = reason
try:
client._post("/add_files/delete_files", data=payload) # type: ignore[attr-defined]
hydrus_deleted = True
preview = hash_hex[:12] + ('' if len(hash_hex) > 12 else '')
debug(f"Deleted from Hydrus: {preview}", file=sys.stderr)
except Exception as exc:
# If it's not in Hydrus (e.g. 404 or similar), that's fine
if not local_deleted:
return False
if hydrus_deleted and hash_hex:
preview = hash_hex[:12] + ('' if len(hash_hex) > 12 else '')
2025-11-25 20:09:33 -08:00
if reason:
ctx.emit(f"Deleted {preview} (reason: {reason}).")
else:
ctx.emit(f"Deleted {preview}.")
2025-12-11 12:47:30 -08:00
if hydrus_deleted or local_deleted:
return True
2025-11-27 10:59:01 -08:00
2025-12-11 12:47:30 -08:00
log("Selected result has neither Hydrus hash nor local file target")
return False
2025-11-27 10:59:01 -08:00
2025-12-11 12:47:30 -08:00
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Execute delete-file command."""
if should_show_help(args):
log(f"Cmdlet: {self.name}\nSummary: {self.summary}\nUsage: {self.usage}")
2025-11-27 10:59:01 -08:00
return 0
2025-12-11 12:47:30 -08:00
# Parse arguments
override_hash: str | None = None
conserve: str | None = None
lib_root: str | None = None
reason_tokens: list[str] = []
i = 0
while i < len(args):
token = args[i]
low = str(token).lower()
if low in {"-hash", "--hash", "hash"} and i + 1 < len(args):
override_hash = str(args[i + 1]).strip()
i += 2
continue
if low in {"-conserve", "--conserve"} and i + 1 < len(args):
value = str(args[i + 1]).strip().lower()
if value in {"local", "hydrus"}:
conserve = value
i += 2
continue
if low in {"-lib-root", "--lib-root", "lib-root"} and i + 1 < len(args):
lib_root = str(args[i + 1]).strip()
2025-11-27 10:59:01 -08:00
i += 2
continue
2025-12-11 12:47:30 -08:00
reason_tokens.append(token)
i += 1
2025-11-27 10:59:01 -08:00
2025-12-11 12:47:30 -08:00
# If no lib_root provided, try to get the first folder store from config
if not lib_root:
try:
storage_config = config.get("storage", {})
folder_config = storage_config.get("folder", {})
if folder_config:
# Get first folder store path
for store_name, store_config in folder_config.items():
if isinstance(store_config, dict):
path = store_config.get("path")
if path:
lib_root = path
break
except Exception:
pass
2025-12-01 01:10:16 -08:00
2025-12-11 12:47:30 -08:00
reason = " ".join(token for token in reason_tokens if str(token).strip()).strip()
items = []
if isinstance(result, list):
items = result
elif result:
items = [result]
if not items:
log("No items to delete", file=sys.stderr)
return 1
2025-11-27 10:59:01 -08:00
2025-12-11 12:47:30 -08:00
success_count = 0
for item in items:
if self._process_single_item(item, override_hash, conserve, lib_root, reason, config):
success_count += 1
if success_count > 0:
# Clear cached tables/items so deleted entries are not redisplayed
try:
ctx.set_last_result_table_overlay(None, None, None)
ctx.set_last_result_table(None, [])
ctx.set_last_result_items_only([])
ctx.set_current_stage_table(None)
except Exception:
pass
2025-11-27 10:59:01 -08:00
2025-12-11 12:47:30 -08:00
return 0 if success_count > 0 else 1
2025-12-07 00:21:30 -08:00
2025-12-11 12:47:30 -08:00
# Instantiate and register the cmdlet
Delete_File()
2025-11-25 20:09:33 -08:00