This commit is contained in:
nose
2025-12-23 16:36:39 -08:00
parent 16316bb3fd
commit 8bf04c6b71
25 changed files with 3165 additions and 234 deletions

View File

@@ -384,6 +384,8 @@ class Folder(Store):
try:
shutil.move(str(file_path), str(save_file))
debug(f"Local move: {save_file}", file=sys.stderr)
# After a move, the original path no longer exists; use destination for subsequent ops.
file_path = save_file
except Exception:
_copy_with_progress(file_path, save_file, label=f"folder:{self._name} move")
try:
@@ -395,6 +397,7 @@ class Folder(Store):
except Exception:
pass
debug(f"Local move (copy+delete): {save_file}", file=sys.stderr)
file_path = save_file
else:
_copy_with_progress(file_path, save_file, label=f"folder:{self._name} copy")
debug(f"Local copy: {save_file}", file=sys.stderr)
@@ -418,7 +421,7 @@ class Folder(Store):
db.save_metadata(save_file, {
'hash': file_hash,
'ext': ext_clean,
'size': file_path.stat().st_size,
'size': save_file.stat().st_size,
'duration': duration_value,
})
@@ -441,6 +444,7 @@ class Folder(Store):
"""Search local database for files by title tag or filename."""
from fnmatch import fnmatch
from API.folder import DatabaseAPI
import unicodedata
limit = kwargs.get("limit")
try:
@@ -453,6 +457,30 @@ class Folder(Store):
query = query.lower()
query_lower = query # Ensure query_lower is defined for all code paths
def _normalize_namespace_text(text: str, *, allow_wildcards: bool) -> str:
"""Normalize tag namespace values for consistent matching.
Removes control/format chars (e.g. zero-width spaces) that frequently appear in scraped tags,
collapses whitespace, and lowercases.
"""
s = str(text or "")
# Normalize newlines/tabs/etc to spaces early.
s = s.replace("\r", " ").replace("\n", " ").replace("\t", " ")
# Drop control / format chars (Cc/Cf) while preserving wildcard tokens when requested.
cleaned_chars: list[str] = []
for ch in s:
if allow_wildcards and ch in {"*", "?"}:
cleaned_chars.append(ch)
continue
cat = unicodedata.category(ch)
if cat in {"Cc", "Cf"}:
continue
cleaned_chars.append(ch)
s = "".join(cleaned_chars)
# Collapse any remaining unicode whitespace runs.
s = " ".join(s.split())
return s.strip().lower()
def _normalize_ext_filter(value: str) -> str:
v = str(value or "").strip().lower().lstrip('.')
v = "".join(ch for ch in v if ch.isalnum())
@@ -648,8 +676,9 @@ class Folder(Store):
tag_lower = str(tag_val).lower()
if not tag_lower.startswith(f"{namespace}:"):
continue
value = tag_lower[len(namespace)+1:]
if fnmatch(value, pattern):
value = _normalize_namespace_text(tag_lower[len(namespace) + 1 :], allow_wildcards=False)
pat = _normalize_namespace_text(pattern, allow_wildcards=True)
if fnmatch(value, pat):
matched.add(file_hash)
return matched
@@ -838,8 +867,9 @@ class Folder(Store):
for tag in tags:
tag_lower = tag.lower()
if tag_lower.startswith(f"{namespace}:"):
value = tag_lower[len(namespace)+1:]
if fnmatch(value, pattern):
value = _normalize_namespace_text(tag_lower[len(namespace) + 1 :], allow_wildcards=False)
pat = _normalize_namespace_text(pattern, allow_wildcards=True)
if fnmatch(value, pat):
if ext_hashes is not None and file_hash not in ext_hashes:
break
file_path = Path(file_path_str)
@@ -1636,20 +1666,43 @@ class Folder(Store):
"""
from API.folder import API_folder_store
try:
file_path = Path(file_identifier)
# Delete from database
with API_folder_store(Path(self._location)) as db:
db.delete_file(file_path)
# Delete the actual file from disk
if file_path.exists():
file_path.unlink()
debug(f"Deleted file: {file_path}")
return True
else:
debug(f"File not found on disk: {file_path}")
return True # Already gone
if not self._location:
return False
raw = str(file_identifier or "").strip()
if not raw:
return False
store_root = Path(self._location).expanduser()
# Support deletion by hash (common for store items where `path` is the hash).
file_hash = _normalize_hash(raw)
resolved_path: Optional[Path] = None
with API_folder_store(store_root) as db:
if file_hash:
resolved_path = db.search_hash(file_hash)
else:
p = Path(raw)
resolved_path = p if p.is_absolute() else (store_root / p)
if resolved_path is None:
debug(f"delete_file: could not resolve identifier: {raw}")
return False
# Delete from database (also cleans up relationship backlinks).
db.delete_file(resolved_path)
# Delete the actual file from disk (best-effort).
try:
if resolved_path.exists():
resolved_path.unlink()
debug(f"Deleted file: {resolved_path}")
else:
debug(f"File not found on disk: {resolved_path}")
except Exception:
pass
return True
except Exception as exc:
debug(f"delete_file failed: {exc}")
return False