This commit is contained in:
2026-01-22 01:53:13 -08:00
parent b3e7f3e277
commit 33406a6ecf
17 changed files with 857 additions and 877 deletions

View File

@@ -1,4 +1,4 @@
"""search-file cmdlet: Search for files in storage backends (Folder, Hydrus)."""
"""search-file cmdlet: Search for files in storage backends (Hydrus)."""
from __future__ import annotations
@@ -11,12 +11,12 @@ import sys
from SYS.logger import log, debug
from ProviderCore.registry import get_search_provider, list_search_providers
from SYS.config import get_local_storage_path
from SYS.rich_display import (
show_provider_config_panel,
show_store_config_panel,
show_available_providers_panel,
)
from SYS.database import insert_worker, update_worker, append_worker_stdout
from ._shared import (
Cmdlet,
@@ -32,17 +32,52 @@ from SYS import pipeline as ctx
STORAGE_ORIGINS = {"local",
"hydrus",
"folder",
"zerotier"}
class _WorkerLogger:
def __init__(self, worker_id: str) -> None:
self.worker_id = worker_id
def __enter__(self) -> "_WorkerLogger":
return self
def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
return None
def insert_worker(
self,
worker_id: str,
worker_type: str,
title: str = "",
description: str = "",
**kwargs: Any,
) -> None:
try:
insert_worker(worker_id, worker_type, title=title, description=description)
except Exception:
pass
def update_worker_status(self, worker_id: str, status: str) -> None:
try:
update_worker(worker_id, status=status)
except Exception:
pass
def append_worker_stdout(self, worker_id: str, content: str) -> None:
try:
append_worker_stdout(worker_id, content)
except Exception:
pass
class search_file(Cmdlet):
"""Class-based search-file cmdlet for searching storage backends."""
def __init__(self) -> None:
super().__init__(
name="search-file",
summary="Search storage backends (Folder, Hydrus) or external providers (via -provider).",
summary="Search storage backends (Hydrus) or external providers (via -provider).",
usage="search-file [-query <query>] [-store BACKEND] [-limit N] [-provider NAME]",
arg=[
CmdletArg(
@@ -65,7 +100,7 @@ class search_file(Cmdlet):
),
],
detail=[
"Search across storage backends: Folder stores and Hydrus instances",
"Search across storage backends: Hydrus instances",
"Use -store to search a specific backend by name",
"URL search: url:* (any URL) or url:<value> (URL substring)",
"Extension search: ext:<value> (e.g., ext:png)",
@@ -74,12 +109,12 @@ class search_file(Cmdlet):
"Examples:",
"search-file -query foo # Search all storage backends",
"search-file -store home -query '*' # Search 'home' Hydrus instance",
"search-file -store test -query 'video' # Search 'test' folder store",
"search-file -store home -query 'video' # Search 'home' Hydrus instance",
"search-file -query 'hash:deadbeef...' # Search by SHA256 hash",
"search-file -query 'url:*' # Files that have any URL",
"search-file -query 'url:youtube.com' # Files whose URL contains substring",
"search-file -query 'ext:png' # Files whose metadata ext is png",
"search-file -query 'system:filetype = png' # Hydrus: native; Folder: maps to metadata.ext",
"search-file -query 'system:filetype = png' # Hydrus: native",
"",
"Provider search (-provider):",
"search-file -provider youtube 'tutorial' # Search YouTube provider",
@@ -210,49 +245,15 @@ class search_file(Cmdlet):
return 1
worker_id = str(uuid.uuid4())
library_root = get_local_storage_path(config or {}) if get_local_storage_path else None
if not library_root:
try:
from Store.registry import get_backend_instance
# Try the first configured folder backend without instantiating all backends
store_cfg = (config or {}).get("store") or {}
folder_cfg = None
for raw_store_type, instances in store_cfg.items():
if _normalize_store_type(str(raw_store_type)) == "folder":
folder_cfg = instances
break
if isinstance(folder_cfg, dict):
for instance_name, instance_config in folder_cfg.items():
try:
backend = get_backend_instance(config, instance_name, suppress_debug=True)
if backend and type(backend).__name__ == "Folder":
library_root = expand_path(getattr(backend, "_location", None))
if library_root:
break
except Exception:
pass
except Exception:
pass
db = None
# Disable Folder DB usage for "external" searches when not using a folder store
# db = None
if library_root and False: # Disabled to prevent 'database is locked' errors during external searches
try:
from API.folder import API_folder_store
db = API_folder_store(library_root)
db.__enter__()
db.insert_worker(
worker_id,
"search-file",
title=f"Search: {query}",
description=f"Provider: {provider_name}, Query: {query}",
pipe=ctx.get_current_command_text(),
)
except Exception:
db = None
try:
insert_worker(
worker_id,
"search-file",
title=f"Search: {query}",
description=f"Provider: {provider_name}, Query: {query}",
)
except Exception:
pass
try:
results_list: List[Dict[str, Any]] = []
@@ -381,9 +382,11 @@ class search_file(Cmdlet):
if not results:
log(f"No results found for query: {query}", file=sys.stderr)
if db is not None:
db.append_worker_stdout(worker_id, json.dumps([], indent=2))
db.update_worker_status(worker_id, "completed")
try:
append_worker_stdout(worker_id, json.dumps([], indent=2))
update_worker(worker_id, status="completed")
except Exception:
pass
return 0
for search_result in results:
@@ -415,9 +418,11 @@ class search_file(Cmdlet):
ctx.set_current_stage_table(table)
if db is not None:
db.append_worker_stdout(worker_id, json.dumps(results_list, indent=2))
db.update_worker_status(worker_id, "completed")
try:
append_worker_stdout(worker_id, json.dumps(results_list, indent=2))
update_worker(worker_id, status="completed")
except Exception:
pass
return 0
@@ -426,18 +431,11 @@ class search_file(Cmdlet):
import traceback
debug(traceback.format_exc())
if db is not None:
try:
db.update_worker_status(worker_id, "error")
except Exception:
pass
try:
update_worker(worker_id, status="error")
except Exception:
pass
return 1
finally:
if db is not None:
try:
db.__exit__(None, None, None)
except Exception:
pass
# --- Execution ------------------------------------------------------
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
@@ -591,37 +589,12 @@ class search_file(Cmdlet):
log("Provide a search query", file=sys.stderr)
return 1
from API.folder import API_folder_store
worker_id = str(uuid.uuid4())
from Store import Store
storage_registry = Store(config=config or {})
library_root = get_local_storage_path(config or {})
if not library_root:
# Fallback for search-file: if no global folder path is found,
# try to use the specific backend mentioned in -store or the first available folder backend.
if storage_backend:
try:
backend = storage_registry[storage_backend]
if backend and type(backend).__name__ == "Folder":
library_root = expand_path(getattr(backend, "_location", None))
except Exception:
pass
else:
# Try all backends until we find a Folder one
for name in storage_registry.list_backends():
try:
backend = storage_registry[name]
if type(backend).__name__ == "Folder":
library_root = expand_path(getattr(backend, "_location", None))
if library_root:
break
except Exception:
continue
if not library_root:
if not storage_registry.list_backends():
# Internal refreshes should not trigger config panels or stop progress.
if "-internal-refresh" in args_list:
return 1
@@ -635,11 +608,11 @@ class search_file(Cmdlet):
progress.stop()
except Exception:
pass
show_store_config_panel(["Folder Store"])
show_store_config_panel(["Hydrus Network"])
return 1
# Use context manager to ensure database is always closed
with API_folder_store(library_root) as db:
# Use a lightweight worker logger to track search results in the central DB
with _WorkerLogger(worker_id) as db:
try:
if "-internal-refresh" not in args_list:
db.insert_worker(
@@ -713,18 +686,7 @@ class search_file(Cmdlet):
# Resolve a path/URL string if possible
path_str: Optional[str] = None
# IMPORTANT: avoid calling get_file() for remote backends.
# For Hydrus, get_file() returns a browser URL (and may include access keys),
# which should not be pulled during search/refresh.
try:
if type(resolved_backend).__name__ == "Folder":
maybe_path = resolved_backend.get_file(h)
if isinstance(maybe_path, Path):
path_str = str(maybe_path)
elif isinstance(maybe_path, str) and maybe_path:
path_str = maybe_path
except Exception:
path_str = None
# Avoid calling get_file() for remote backends during search/refresh.
meta_obj: Dict[str,
Any] = {}