dfdfdd
This commit is contained in:
@@ -117,6 +117,72 @@ class Add_File(Cmdlet):
|
||||
stage_ctx = ctx.get_stage_context()
|
||||
is_last_stage = (stage_ctx is None) or bool(getattr(stage_ctx, "is_last_stage", False))
|
||||
|
||||
# Directory-mode selector:
|
||||
# - First pass: `add-file -store X -path <DIR>` should ONLY show a selectable table.
|
||||
# - Second pass (triggered by @ selection expansion): re-run add-file with `-path file1,file2,...`
|
||||
# and actually ingest/copy.
|
||||
dir_scan_mode = False
|
||||
dir_scan_results: Optional[List[Dict[str, Any]]] = None
|
||||
explicit_path_list_results: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
if path_arg and location and not provider_name:
|
||||
# Support comma-separated path lists: -path "file1,file2,file3"
|
||||
# This is the mechanism used by @N expansion for directory tables.
|
||||
try:
|
||||
path_text = str(path_arg)
|
||||
except Exception:
|
||||
path_text = ""
|
||||
|
||||
if "," in path_text:
|
||||
parts = [p.strip().strip('"') for p in path_text.split(",")]
|
||||
parts = [p for p in parts if p]
|
||||
|
||||
batch: List[Dict[str, Any]] = []
|
||||
for p in parts:
|
||||
try:
|
||||
file_path = Path(p)
|
||||
except Exception:
|
||||
continue
|
||||
if not file_path.exists() or not file_path.is_file():
|
||||
continue
|
||||
ext = file_path.suffix.lower()
|
||||
if ext not in SUPPORTED_MEDIA_EXTENSIONS:
|
||||
continue
|
||||
try:
|
||||
hv = sha256_file(file_path)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
size = file_path.stat().st_size
|
||||
except Exception:
|
||||
size = 0
|
||||
batch.append({
|
||||
"path": file_path,
|
||||
"name": file_path.name,
|
||||
"hash": hv,
|
||||
"size": size,
|
||||
"ext": ext,
|
||||
})
|
||||
|
||||
if batch:
|
||||
explicit_path_list_results = batch
|
||||
# Clear path_arg so add-file doesn't treat it as a single path.
|
||||
path_arg = None
|
||||
else:
|
||||
# Directory scan (selector table, no ingest yet)
|
||||
try:
|
||||
candidate_dir = Path(str(path_arg))
|
||||
if candidate_dir.exists() and candidate_dir.is_dir():
|
||||
dir_scan_mode = True
|
||||
debug(f"[add-file] Scanning directory for batch add: {candidate_dir}")
|
||||
dir_scan_results = Add_File._scan_directory_for_files(candidate_dir)
|
||||
if dir_scan_results:
|
||||
debug(f"[add-file] Found {len(dir_scan_results)} supported files in directory")
|
||||
# Clear path_arg so it doesn't trigger single-item mode.
|
||||
path_arg = None
|
||||
except Exception as exc:
|
||||
debug(f"[add-file] Directory scan failed: {exc}")
|
||||
|
||||
# Determine if -store targets a registered backend (vs a filesystem export path).
|
||||
is_storage_backend_location = False
|
||||
if location:
|
||||
@@ -127,9 +193,16 @@ class Add_File(Cmdlet):
|
||||
is_storage_backend_location = False
|
||||
|
||||
# Decide which items to process.
|
||||
# - If directory scan was performed, use those results
|
||||
# - If user provided -path (and it was not reinterpreted as destination), treat this invocation as single-item.
|
||||
# - Otherwise, if piped input is a list, ingest each item.
|
||||
if path_arg:
|
||||
if explicit_path_list_results:
|
||||
items_to_process = explicit_path_list_results
|
||||
debug(f"[add-file] Using {len(items_to_process)} files from -path list")
|
||||
elif dir_scan_results:
|
||||
items_to_process = dir_scan_results
|
||||
debug(f"[add-file] Using {len(items_to_process)} files from directory scan")
|
||||
elif path_arg:
|
||||
items_to_process: List[Any] = [result]
|
||||
elif isinstance(result, list) and result:
|
||||
items_to_process = list(result)
|
||||
@@ -152,6 +225,65 @@ class Add_File(Cmdlet):
|
||||
debug(f"[add-file] INPUT result is list with {len(result)} items")
|
||||
debug(f"[add-file] PARSED args: location={location}, provider={provider_name}, delete={delete_after}")
|
||||
|
||||
# If this invocation was directory selector mode, show a selectable table and stop.
|
||||
# The user then runs @N (optionally piped), which replays add-file with selected paths.
|
||||
if dir_scan_mode:
|
||||
try:
|
||||
from result_table import ResultTable
|
||||
from pathlib import Path as _Path
|
||||
|
||||
# Build base args to replay: keep everything except the directory -path.
|
||||
base_args: List[str] = []
|
||||
skip_next = False
|
||||
for tok in list(args or []):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
t = str(tok)
|
||||
if t in {"-path", "--path", "-p"}:
|
||||
skip_next = True
|
||||
continue
|
||||
base_args.append(t)
|
||||
|
||||
table = ResultTable(title="Files in Directory", preserve_order=True)
|
||||
table.set_table("add-file.directory")
|
||||
table.set_source_command("add-file", base_args)
|
||||
|
||||
rows: List[Dict[str, Any]] = []
|
||||
for file_info in (dir_scan_results or []):
|
||||
p = file_info.get("path")
|
||||
hp = str(file_info.get("hash") or "")
|
||||
name = str(file_info.get("name") or "unknown")
|
||||
try:
|
||||
clean_title = _Path(name).stem
|
||||
except Exception:
|
||||
clean_title = name
|
||||
ext = str(file_info.get("ext") or "").lstrip(".")
|
||||
size = file_info.get("size", 0)
|
||||
|
||||
row_item = {
|
||||
"path": str(p) if p is not None else "",
|
||||
"hash": hp,
|
||||
"title": clean_title,
|
||||
"columns": [
|
||||
("Title", clean_title),
|
||||
("Hash", hp),
|
||||
("Size", size),
|
||||
("Ext", ext),
|
||||
],
|
||||
# Used by @N replay (CLI will combine selected rows into -path file1,file2,...)
|
||||
"_selection_args": ["-path", str(p) if p is not None else ""],
|
||||
}
|
||||
rows.append(row_item)
|
||||
table.add_result(row_item)
|
||||
|
||||
ctx.set_current_stage_table(table)
|
||||
ctx.set_last_result_table(table, rows, subject={"table": "add-file.directory"})
|
||||
log(f"✓ Found {len(rows)} files. Select with @N (e.g., @1 or @1-3).")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
debug(f"[add-file] Failed to display directory scan result table: {exc}")
|
||||
|
||||
collected_payloads: List[Dict[str, Any]] = []
|
||||
pending_relationship_pairs: Dict[str, set[tuple[str, str]]] = {}
|
||||
pending_url_associations: Dict[str, List[tuple[str, List[str]]]] = {}
|
||||
@@ -976,7 +1108,23 @@ class Add_File(Cmdlet):
|
||||
Returns (media_path_or_url, file_hash)
|
||||
where media_path_or_url can be a Path object or a URL string.
|
||||
"""
|
||||
# PRIORITY 1: Try hash+store from result dict (most reliable for @N selections)
|
||||
# PRIORITY 1a: Try hash+path from directory scan result (has 'path' and 'hash' keys)
|
||||
if isinstance(result, dict):
|
||||
result_path = result.get("path")
|
||||
result_hash = result.get("hash")
|
||||
# Check if this looks like a directory scan result (has path and hash but no 'store' key)
|
||||
result_store = result.get("store")
|
||||
if result_path and result_hash and not result_store:
|
||||
try:
|
||||
media_path = Path(result_path) if not isinstance(result_path, Path) else result_path
|
||||
if media_path.exists() and media_path.is_file():
|
||||
debug(f"[add-file] Using path+hash from directory scan: {media_path}")
|
||||
pipe_obj.path = str(media_path)
|
||||
return media_path, str(result_hash)
|
||||
except Exception as exc:
|
||||
debug(f"[add-file] Failed to use directory scan result: {exc}")
|
||||
|
||||
# PRIORITY 1b: Try hash+store from result dict (most reliable for @N selections)
|
||||
if isinstance(result, dict):
|
||||
result_hash = result.get("hash")
|
||||
result_store = result.get("store")
|
||||
@@ -1104,6 +1252,56 @@ class Add_File(Cmdlet):
|
||||
log("File path could not be resolved")
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def _scan_directory_for_files(directory: Path) -> List[Dict[str, Any]]:
|
||||
"""Scan a directory for supported media files and return list of file info dicts.
|
||||
|
||||
Each dict contains:
|
||||
- path: Path object
|
||||
- name: filename
|
||||
- hash: sha256 hash
|
||||
- size: file size in bytes
|
||||
- ext: file extension
|
||||
"""
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
return []
|
||||
|
||||
files_info: List[Dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
for item in directory.iterdir():
|
||||
if not item.is_file():
|
||||
continue
|
||||
|
||||
ext = item.suffix.lower()
|
||||
if ext not in SUPPORTED_MEDIA_EXTENSIONS:
|
||||
continue
|
||||
|
||||
# Compute hash
|
||||
try:
|
||||
file_hash = sha256_file(item)
|
||||
except Exception as exc:
|
||||
debug(f"Failed to hash {item}: {exc}")
|
||||
continue
|
||||
|
||||
# Get file size
|
||||
try:
|
||||
size = item.stat().st_size
|
||||
except Exception:
|
||||
size = 0
|
||||
|
||||
files_info.append({
|
||||
"path": item,
|
||||
"name": item.name,
|
||||
"hash": file_hash,
|
||||
"size": size,
|
||||
"ext": ext,
|
||||
})
|
||||
except Exception as exc:
|
||||
debug(f"Error scanning directory {directory}: {exc}")
|
||||
|
||||
return files_info
|
||||
|
||||
@staticmethod
|
||||
def _fetch_hydrus_path(
|
||||
file_hash: str,
|
||||
|
||||
Reference in New Issue
Block a user