"""Resuelve el path absoluto real de un attachment embebido en un vault Obsidian. Funcion impura: recorre el filesystem del vault. Obsidian resuelve los embeds ![[...]] por nombre de archivo unico (no por path), asi que esta funcion busca recursivamente un archivo cuyo basename coincida con el nombre del embed. """ import os # Directorios de maquinaria de Obsidian que nunca contienen attachments de usuario. _EXCLUDED_DIRS = {".obsidian", ".trash"} def resolve_obsidian_embed(vault_dir: str, embed_name: str) -> str: """Resuelve el path absoluto de un attachment embebido buscandolo por nombre. Obsidian resuelve los embeds `![[archivo.jpg]]` por nombre de archivo unico dentro del vault, no por ruta. Esta funcion replica ese comportamiento: recorre ``vault_dir`` recursivamente (una sola pasada con ``os.walk``) y devuelve el primer archivo cuyo basename coincida, de forma case-insensitive, con ``embed_name``. Si ``embed_name`` no trae extension (p.ej. ``"foto"``), se acepta cualquier archivo cuyo basename sin extension coincida (p.ej. ``foto.jpg``). Los directorios ``.obsidian/`` y ``.trash/`` se excluyen del recorrido. Impura: lee el filesystem. NO lanza si el embed no se encuentra (devuelve cadena vacia). SI lanza si ``vault_dir`` no existe. Args: vault_dir: Ruta a la raiz del vault Obsidian. embed_name: Nombre del attachment embebido (lo que devuelve ``extract_obsidian_embeds``), con o sin extension. Returns: El path absoluto del primer archivo que coincide, o cadena vacia ``""`` si ningun archivo del vault coincide. Raises: FileNotFoundError: si ``vault_dir`` no existe. NotADirectoryError: si ``vault_dir`` no es un directorio. """ if not os.path.exists(vault_dir): raise FileNotFoundError(f"vault path does not exist: {vault_dir}") if not os.path.isdir(vault_dir): raise NotADirectoryError(f"vault path is not a directory: {vault_dir}") target = embed_name.strip() if not target: return "" target_lower = target.lower() has_extension = os.path.splitext(target)[1] != "" target_stem_lower = os.path.splitext(target_lower)[0] for dirpath, dirnames, filenames in os.walk(vault_dir): # Podar maquinaria de Obsidian in-place para no descender en ella. dirnames[:] = [d for d in dirnames if d not in _EXCLUDED_DIRS] for filename in filenames: filename_lower = filename.lower() if has_extension: # Comparar basename completo, case-insensitive. if filename_lower == target_lower: return os.path.abspath(os.path.join(dirpath, filename)) else: # Sin extension en el embed: comparar solo el stem del archivo. stem_lower = os.path.splitext(filename_lower)[0] if stem_lower == target_stem_lower: return os.path.abspath(os.path.join(dirpath, filename)) return "" if __name__ == "__main__": import tempfile with tempfile.TemporaryDirectory() as tmp: os.makedirs(os.path.join(tmp, "attachments")) os.makedirs(os.path.join(tmp, ".obsidian")) with open(os.path.join(tmp, "attachments", "Foto.JPG"), "w") as f: f.write("x") with open(os.path.join(tmp, "doc.pdf"), "w") as f: f.write("y") # Match case-insensitive con extension. hit = resolve_obsidian_embed(tmp, "foto.jpg") assert hit.endswith(os.path.join("attachments", "Foto.JPG")), hit # Match sin extension en el embed. hit2 = resolve_obsidian_embed(tmp, "doc") assert hit2.endswith("doc.pdf"), hit2 # No existe -> "". assert resolve_obsidian_embed(tmp, "no_existe.png") == "" print("resolve_obsidian_embed smoke OK")