Files
pkm/app/services/vault.py
2025-08-18 21:40:41 +02:00

63 lines
2.1 KiB
Python

from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
@dataclass(frozen=True)
class VaultPaths:
root: str
notes: str
attachments: str
kb: str
class Vault:
def __init__(self, root_path: str):
root = Path(root_path).expanduser().resolve()
object.__setattr__(
self,
"paths",
VaultPaths(
root=str(root),
notes=str(root / "notes"),
attachments=str(root / "attachments"),
kb=str(root / ".kb"),
),
)
def ensure_structure(self) -> None:
Path(self.paths.notes).mkdir(parents=True, exist_ok=True)
Path(self.paths.attachments).mkdir(parents=True, exist_ok=True)
Path(self.paths.kb).mkdir(parents=True, exist_ok=True)
def abspath(self, rel_path: str) -> str:
# If rel_path is absolute, Path(root) / rel_path will return rel_path as-is.
return str((Path(self.paths.root) / rel_path).resolve())
def relpath(self, abs_path: str) -> str:
return str(Path(abs_path).resolve().relative_to(Path(self.paths.root).resolve()))
def iter_markdown_files(self) -> Iterator[str]:
"""
Yield absolute paths to .md files under <vault>/notes recursively.
- Allows the vault root to be hidden or not.
- Skips hidden subdirectories within notes/ (names starting with '.').
- Skips hidden files (names starting with '.').
"""
notes_dir = Path(self.paths.notes)
if not notes_dir.exists():
return iter(())
# Walk manually to filter hidden dirs/files
for dirpath, dirnames, filenames in os.walk(notes_dir):
# Remove hidden subdirectories in-place (prevents os.walk from entering them)
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
for fname in filenames:
if fname.startswith("."):
continue
if not fname.endswith(".md"):
continue
yield str(Path(dirpath, fname).resolve())