f
This commit is contained in:
@@ -1,28 +1,13 @@
|
||||
"""Delete file relationships."""
|
||||
"""Delete file relationships (Currently Disabled)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from SYS.logger import log
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from SYS import pipeline as ctx
|
||||
from . import _shared as sh
|
||||
|
||||
Cmdlet = sh.Cmdlet
|
||||
CmdletArg = sh.CmdletArg
|
||||
SharedArgs = sh.SharedArgs
|
||||
parse_cmdlet_args = sh.parse_cmdlet_args
|
||||
normalize_hash = sh.normalize_hash
|
||||
normalize_result_input = sh.normalize_result_input
|
||||
get_field = sh.get_field
|
||||
should_show_help = sh.should_show_help
|
||||
from API.folder import API_folder_store
|
||||
from Store import Store
|
||||
from SYS.config import get_local_storage_path
|
||||
|
||||
|
||||
def _extract_hash(item: Any) -> Optional[str]:
|
||||
@@ -33,471 +18,42 @@ def _extract_hash(item: Any) -> Optional[str]:
|
||||
return normalize_hash(str(h)) if h else None
|
||||
|
||||
|
||||
def _upsert_relationships(
|
||||
db: API_folder_store,
|
||||
file_hash: str,
|
||||
relationships: Dict[str,
|
||||
Any]
|
||||
) -> None:
|
||||
conn = db.connection
|
||||
if conn is None:
|
||||
raise RuntimeError("Store DB connection is not initialized")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO metadata (hash, relationships)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(hash) DO UPDATE SET
|
||||
relationships = excluded.relationships,
|
||||
time_modified = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(file_hash,
|
||||
json.dumps(relationships) if relationships else "{}"),
|
||||
)
|
||||
|
||||
|
||||
def _remove_reverse_link(
|
||||
db: API_folder_store,
|
||||
*,
|
||||
src_hash: str,
|
||||
dst_hash: str,
|
||||
rel_type: str
|
||||
) -> None:
|
||||
meta = db.get_metadata(dst_hash) or {}
|
||||
rels = meta.get("relationships") if isinstance(meta, dict) else None
|
||||
if not isinstance(rels, dict) or not rels:
|
||||
return
|
||||
|
||||
key_to_edit: Optional[str] = None
|
||||
for k in list(rels.keys()):
|
||||
if str(k).lower() == str(rel_type).lower():
|
||||
key_to_edit = str(k)
|
||||
break
|
||||
if not key_to_edit:
|
||||
return
|
||||
|
||||
bucket = rels.get(key_to_edit)
|
||||
if not isinstance(bucket, list) or not bucket:
|
||||
return
|
||||
|
||||
new_bucket = [h for h in bucket if str(h).lower() != str(src_hash).lower()]
|
||||
if new_bucket:
|
||||
rels[key_to_edit] = new_bucket
|
||||
else:
|
||||
try:
|
||||
del rels[key_to_edit]
|
||||
except Exception:
|
||||
rels[key_to_edit] = []
|
||||
|
||||
_upsert_relationships(db, dst_hash, rels)
|
||||
|
||||
|
||||
def _refresh_relationship_view_if_current(
|
||||
target_hash: Optional[str],
|
||||
target_path: Optional[str],
|
||||
other: Optional[str],
|
||||
config: Dict[str,
|
||||
Any],
|
||||
) -> None:
|
||||
"""If the current subject matches the target, refresh relationships via get-relationship."""
|
||||
def _run(args: List[str], config: Dict[str, Any]) -> int:
|
||||
try:
|
||||
from cmdlet import get as get_cmdlet # type: ignore
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
subject = ctx.get_last_result_subject()
|
||||
if subject is None:
|
||||
return
|
||||
|
||||
def norm(val: Any) -> str:
|
||||
return str(val).lower()
|
||||
|
||||
target_hashes = [norm(v) for v in [target_hash, other] if v]
|
||||
target_paths = [norm(v) for v in [target_path, other] if v]
|
||||
|
||||
subj_hashes: list[str] = []
|
||||
subj_paths: list[str] = []
|
||||
for field in ("hydrus_hash", "hash", "hash_hex", "file_hash"):
|
||||
val = get_field(subject, field)
|
||||
if val:
|
||||
subj_hashes.append(norm(val))
|
||||
for field in ("file_path", "path", "target"):
|
||||
val = get_field(subject, field)
|
||||
if val:
|
||||
subj_paths.append(norm(val))
|
||||
|
||||
is_match = False
|
||||
if target_hashes and any(h in subj_hashes for h in target_hashes):
|
||||
is_match = True
|
||||
if target_paths and any(p in subj_paths for p in target_paths):
|
||||
is_match = True
|
||||
if not is_match:
|
||||
return
|
||||
|
||||
refresh_args: list[str] = []
|
||||
if target_hash:
|
||||
refresh_args.extend(["-query", f"hash:{target_hash}"])
|
||||
|
||||
cmd = get_cmdlet("get-relationship")
|
||||
if not cmd:
|
||||
return
|
||||
cmd(subject, refresh_args, config)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Delete relationships from files.
|
||||
|
||||
Args:
|
||||
result: Input result(s) from previous cmdlet
|
||||
args: Command arguments
|
||||
config: CLI configuration
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success)
|
||||
"""
|
||||
try:
|
||||
if should_show_help(args):
|
||||
log(
|
||||
f"Cmdlet: {CMDLET.name}\nSummary: {CMDLET.summary}\nUsage: {CMDLET.usage}"
|
||||
)
|
||||
parsed, _, _ = sh.parse_cmdlet_args(args)
|
||||
if sh.should_show_help(parsed):
|
||||
return 0
|
||||
|
||||
# Parse arguments
|
||||
parsed_args = parse_cmdlet_args(args, CMDLET)
|
||||
delete_all_flag = parsed_args.get("all", False)
|
||||
rel_type_filter = parsed_args.get("type")
|
||||
override_store = parsed_args.get("store")
|
||||
override_hashes = sh.parse_hash_query(parsed_args.get("query"))
|
||||
if parsed_args.get("query") and not override_hashes:
|
||||
log("Invalid -query value (expected hash:<sha256>)", file=sys.stderr)
|
||||
return 1
|
||||
raw_path = parsed_args.get("path")
|
||||
|
||||
# Normalize input
|
||||
results = normalize_result_input(result)
|
||||
|
||||
# Allow store/hash-first usage when no pipeline items were provided
|
||||
if (not results) and override_hashes:
|
||||
if not override_store:
|
||||
log(
|
||||
"-store is required when using -query without piped items",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
results = [
|
||||
{
|
||||
"hash": h,
|
||||
"store": str(override_store)
|
||||
} for h in override_hashes
|
||||
]
|
||||
|
||||
if not results:
|
||||
# Legacy -path mode below may still apply
|
||||
if raw_path:
|
||||
results = [{
|
||||
"file_path": raw_path
|
||||
}]
|
||||
else:
|
||||
log("No results to process", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Decide store (for same-store enforcement + folder-store DB routing)
|
||||
store_name: Optional[str] = str(override_store
|
||||
).strip() if override_store else None
|
||||
if not store_name:
|
||||
stores = {
|
||||
str(get_field(r,
|
||||
"store"))
|
||||
for r in results if get_field(r, "store")
|
||||
}
|
||||
if len(stores) == 1:
|
||||
store_name = next(iter(stores))
|
||||
elif len(stores) > 1:
|
||||
log(
|
||||
"Multiple stores detected in pipeline; use -store to choose one",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
deleted_count = 0
|
||||
|
||||
# STORE/HASH FIRST: folder-store DB deletion (preferred)
|
||||
if store_name:
|
||||
backend = None
|
||||
store_root: Optional[Path] = None
|
||||
try:
|
||||
store = Store(config)
|
||||
backend = store[str(store_name)]
|
||||
loc = getattr(backend, "location", None)
|
||||
if callable(loc):
|
||||
store_root = Path(str(loc()))
|
||||
except Exception:
|
||||
backend = None
|
||||
store_root = None
|
||||
|
||||
if store_root is not None:
|
||||
try:
|
||||
with API_folder_store(store_root) as db:
|
||||
conn = db.connection
|
||||
if conn is None:
|
||||
raise RuntimeError("Store DB connection is not initialized")
|
||||
for single_result in results:
|
||||
# Enforce same-store when items carry store info
|
||||
item_store = get_field(single_result, "store")
|
||||
if item_store and str(item_store) != str(store_name):
|
||||
log(
|
||||
f"Cross-store delete blocked: item store '{item_store}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
file_hash = _extract_hash(single_result)
|
||||
if not file_hash:
|
||||
# Try path -> hash lookup within this store
|
||||
fp = (
|
||||
get_field(single_result,
|
||||
"file_path")
|
||||
or get_field(single_result,
|
||||
"path")
|
||||
or get_field(single_result,
|
||||
"target")
|
||||
)
|
||||
if fp:
|
||||
try:
|
||||
file_hash = db.get_file_hash(Path(str(fp)))
|
||||
except Exception:
|
||||
file_hash = None
|
||||
if not file_hash:
|
||||
log(
|
||||
'Could not extract file hash for deletion (use -query "hash:<sha256>" or ensure pipeline includes hash)',
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
meta = db.get_metadata(file_hash) or {}
|
||||
rels = meta.get("relationships"
|
||||
) if isinstance(meta,
|
||||
dict) else None
|
||||
if not isinstance(rels, dict) or not rels:
|
||||
continue
|
||||
|
||||
if delete_all_flag:
|
||||
# remove reverse edges for all types
|
||||
for rt, hashes in list(rels.items()):
|
||||
if not isinstance(hashes, list):
|
||||
continue
|
||||
for other_hash in hashes:
|
||||
other_norm = normalize_hash(str(other_hash))
|
||||
if other_norm:
|
||||
_remove_reverse_link(
|
||||
db,
|
||||
src_hash=file_hash,
|
||||
dst_hash=other_norm,
|
||||
rel_type=str(rt),
|
||||
)
|
||||
rels = {}
|
||||
elif rel_type_filter:
|
||||
# delete one type (case-insensitive key match)
|
||||
key_to_delete: Optional[str] = None
|
||||
for k in list(rels.keys()):
|
||||
if str(k).lower() == str(rel_type_filter).lower():
|
||||
key_to_delete = str(k)
|
||||
break
|
||||
if not key_to_delete:
|
||||
continue
|
||||
hashes = rels.get(key_to_delete)
|
||||
if isinstance(hashes, list):
|
||||
for other_hash in hashes:
|
||||
other_norm = normalize_hash(str(other_hash))
|
||||
if other_norm:
|
||||
_remove_reverse_link(
|
||||
db,
|
||||
src_hash=file_hash,
|
||||
dst_hash=other_norm,
|
||||
rel_type=str(key_to_delete),
|
||||
)
|
||||
try:
|
||||
del rels[key_to_delete]
|
||||
except Exception:
|
||||
rels[key_to_delete] = []
|
||||
else:
|
||||
log(
|
||||
"Specify --all to delete all relationships or -type <type> to delete specific type",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
_upsert_relationships(db, file_hash, rels)
|
||||
conn.commit()
|
||||
_refresh_relationship_view_if_current(
|
||||
file_hash,
|
||||
None,
|
||||
None,
|
||||
config
|
||||
)
|
||||
deleted_count += 1
|
||||
|
||||
log(
|
||||
f"Successfully deleted relationships from {deleted_count} file(s)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log(f"Error deleting store relationships: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# LEGACY PATH MODE (single local DB)
|
||||
# Get storage path
|
||||
local_storage_path = get_local_storage_path(config)
|
||||
if not local_storage_path:
|
||||
log("Local storage path not configured", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
with API_folder_store(Path(local_storage_path)) as db:
|
||||
conn = db.connection
|
||||
if conn is None:
|
||||
raise RuntimeError("Store DB connection is not initialized")
|
||||
|
||||
for single_result in results:
|
||||
# Get file path from result
|
||||
file_path_from_result = (
|
||||
get_field(single_result,
|
||||
"file_path") or get_field(single_result,
|
||||
"path")
|
||||
or get_field(single_result,
|
||||
"target")
|
||||
or (
|
||||
str(single_result) if not isinstance(single_result,
|
||||
dict) else None
|
||||
)
|
||||
)
|
||||
|
||||
if not file_path_from_result:
|
||||
log("Could not extract file path from result", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
file_path_obj = Path(str(file_path_from_result))
|
||||
|
||||
if not file_path_obj.exists():
|
||||
log(f"File not found: {file_path_obj}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
file_hash = db.get_file_hash(file_path_obj)
|
||||
except Exception:
|
||||
file_hash = None
|
||||
file_hash = normalize_hash(str(file_hash)) if file_hash else None
|
||||
if not file_hash:
|
||||
log(
|
||||
f"File not in database: {file_path_obj.name}",
|
||||
file=sys.stderr
|
||||
)
|
||||
continue
|
||||
|
||||
meta = db.get_metadata(file_hash) or {}
|
||||
rels = meta.get("relationships") if isinstance(meta, dict) else None
|
||||
if not isinstance(rels, dict) or not rels:
|
||||
continue
|
||||
|
||||
if delete_all_flag:
|
||||
for rt, hashes in list(rels.items()):
|
||||
if not isinstance(hashes, list):
|
||||
continue
|
||||
for other_hash in hashes:
|
||||
other_norm = normalize_hash(str(other_hash))
|
||||
if other_norm:
|
||||
_remove_reverse_link(
|
||||
db,
|
||||
src_hash=file_hash,
|
||||
dst_hash=other_norm,
|
||||
rel_type=str(rt),
|
||||
)
|
||||
rels = {}
|
||||
elif rel_type_filter:
|
||||
key_to_delete: Optional[str] = None
|
||||
for k in list(rels.keys()):
|
||||
if str(k).lower() == str(rel_type_filter).lower():
|
||||
key_to_delete = str(k)
|
||||
break
|
||||
if not key_to_delete:
|
||||
continue
|
||||
hashes = rels.get(key_to_delete)
|
||||
if isinstance(hashes, list):
|
||||
for other_hash in hashes:
|
||||
other_norm = normalize_hash(str(other_hash))
|
||||
if other_norm:
|
||||
_remove_reverse_link(
|
||||
db,
|
||||
src_hash=file_hash,
|
||||
dst_hash=other_norm,
|
||||
rel_type=str(key_to_delete),
|
||||
)
|
||||
try:
|
||||
del rels[key_to_delete]
|
||||
except Exception:
|
||||
rels[key_to_delete] = []
|
||||
else:
|
||||
log(
|
||||
"Specify --all to delete all relationships or -type <type> to delete specific type",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
_upsert_relationships(db, file_hash, rels)
|
||||
conn.commit()
|
||||
_refresh_relationship_view_if_current(
|
||||
file_hash,
|
||||
str(file_path_obj),
|
||||
None,
|
||||
config
|
||||
)
|
||||
deleted_count += 1
|
||||
except Exception as exc:
|
||||
log(f"Error deleting relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
log(
|
||||
f"Successfully deleted relationships from {deleted_count} file(s)",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 0
|
||||
# Relationship deletion is currently not implemented for Hydrus.
|
||||
# Legacy Folder storage has been removed.
|
||||
sh.log("Relationship deletion is currently only supported via Folder storage, which has been removed.", file=sys.stderr)
|
||||
sh.log("Hydrus relationship management via Medios-Macina is planned for a future update.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
except Exception as exc:
|
||||
log(f"Error in delete-relationship: {exc}", file=sys.stderr)
|
||||
sh.log(f"Error in delete-relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
CMDLET = sh.Cmdlet(
|
||||
name="delete-relationship",
|
||||
summary="Remove relationships from files.",
|
||||
usage=
|
||||
"@1 | delete-relationship --all OR delete-relationship -path <file> --all OR @1-3 | delete-relationship -type alt",
|
||||
summary="Remove relationships from files (Currently Disabled).",
|
||||
usage="@1 | delete-relationship --all",
|
||||
arg=[
|
||||
SharedArgs.PATH,
|
||||
SharedArgs.STORE,
|
||||
SharedArgs.QUERY,
|
||||
CmdletArg(
|
||||
sh.SharedArgs.PATH,
|
||||
sh.SharedArgs.STORE,
|
||||
sh.SharedArgs.QUERY,
|
||||
sh.CmdletArg(
|
||||
"all",
|
||||
type="flag",
|
||||
description="Delete all relationships for the file(s)."
|
||||
),
|
||||
CmdletArg(
|
||||
sh.CmdletArg(
|
||||
"type",
|
||||
type="string",
|
||||
description=
|
||||
"Delete specific relationship type ('alt', 'king', 'related'). Default: delete all types.",
|
||||
description="Delete specific relationship type.",
|
||||
),
|
||||
],
|
||||
detail=[
|
||||
"- Delete all relationships: pipe files | delete-relationship --all",
|
||||
"- Delete specific type: pipe files | delete-relationship -type alt",
|
||||
"- Delete all from file: delete-relationship -path <file> --all",
|
||||
],
|
||||
)
|
||||
|
||||
CMDLET.exec = _run
|
||||
|
||||
Reference in New Issue
Block a user