Add audit logging, rate limiting, secret redactor, and backlinks
Implement several security and feature improvements across the backend and frontend: - New IP-based rate limiter for authentication endpoints - New audit logging system for sensitive operations - New secret redactor to mask sensitive patterns in rendered content - Configurable token TTL and IGNORED_DIRS via environment variables - Add backlink index and API endpoint - Add preview tab support with single/double-click behavior in tree - Add file backup before write/delete operations
This commit is contained in:
@@ -30,6 +30,9 @@ _index_generation: int = 0
|
||||
# O(1) lookup table for wikilink resolution: {filename_lower: [{vault, path}, ...]}
|
||||
_file_lookup: Dict[str, List[Dict[str, str]]] = {}
|
||||
|
||||
# Backlink index: {vault_name: {relative_path: [{vault, path, title}, ...]}}
|
||||
_backlink_index: Dict[str, Dict[str, List[Dict[str, str]]]] = {}
|
||||
|
||||
# O(1) path index for tree filtering: {vault_name: [{path, name, type}, ...]}
|
||||
path_index: Dict[str, List[Dict[str, str]]] = {}
|
||||
|
||||
@@ -49,6 +52,12 @@ SUPPORTED_EXTENSIONS = {
|
||||
}
|
||||
|
||||
|
||||
# Ignored directories (configurable via OBSIGATE_IGNORED_DIRS env var)
|
||||
_DEFAULT_IGNORED = {'.obsidian', '.trash', '.git', '__pycache__', 'node_modules', '.obsigate-backup'}
|
||||
_env_ignored = os.environ.get("OBSIGATE_IGNORED_DIRS", "")
|
||||
IGNORED_DIRS = set(d.strip() for d in _env_ignored.split(",") if d.strip()) if _env_ignored else _DEFAULT_IGNORED.copy()
|
||||
|
||||
|
||||
def load_vault_config() -> Dict[str, Dict[str, Any]]:
|
||||
"""Read VAULT_N_* and DIR_N_* env vars and return vault configuration.
|
||||
|
||||
@@ -223,6 +232,10 @@ def _scan_vault(vault_name: str, vault_path: str, vault_cfg: Optional[Dict[str,
|
||||
return {"files": [], "tags": {}, "path": vault_path, "paths": []}
|
||||
|
||||
for fpath in vault_root.rglob("*"):
|
||||
# Skip ignored directories
|
||||
if any(part in IGNORED_DIRS for part in fpath.relative_to(vault_root).parts):
|
||||
continue
|
||||
|
||||
rel_path_str = str(fpath.relative_to(vault_root)).replace("\\", "/")
|
||||
|
||||
# Add all paths (files and directories) to path index
|
||||
@@ -270,6 +283,12 @@ def _scan_vault(vault_name: str, vault_path: str, vault_cfg: Optional[Dict[str,
|
||||
title = _extract_title(post, fpath)
|
||||
content_preview = post.content[:200].strip()
|
||||
|
||||
# Extract wikilinks for backlink index
|
||||
_extract_wikilinks_for_backlinks(
|
||||
vault_name, str(relative).replace("\\", "/"),
|
||||
title, post.content
|
||||
)
|
||||
|
||||
files.append({
|
||||
"path": str(relative).replace("\\", "/"),
|
||||
"title": title,
|
||||
@@ -849,3 +868,71 @@ def find_file_in_index(link_target: str, current_vault: str) -> Optional[Dict[st
|
||||
if c["vault"] == current_vault:
|
||||
return c
|
||||
return candidates[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backlink index: tracks which files link to which targets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extract_wikilinks_for_backlinks(
|
||||
vault_name: str, source_path: str, source_title: str, content: str
|
||||
):
|
||||
"""Extract wikilinks from markdown content and populate the backlink index.
|
||||
|
||||
For each `[[target]]` or `[[target|display]]` found in the content,
|
||||
adds the source file to the backlink index of the target.
|
||||
|
||||
Args:
|
||||
vault_name: The vault containing the source file.
|
||||
source_path: Relative path of the source file in the vault.
|
||||
source_title: Title of the source file.
|
||||
content: The markdown content to scan for wikilinks.
|
||||
"""
|
||||
global _backlink_index
|
||||
wikilink_pattern = re.compile(r'\[\[([^\]|#]+)(?:[|#][^\]]+)?\]\]')
|
||||
targets = set()
|
||||
for match in wikilink_pattern.finditer(content):
|
||||
target = match.group(1).strip()
|
||||
target_lower = target.lower()
|
||||
if not target_lower.endswith(".md"):
|
||||
target_lower += ".md"
|
||||
targets.add(target_lower)
|
||||
|
||||
if vault_name not in _backlink_index:
|
||||
_backlink_index[vault_name] = {}
|
||||
|
||||
for target in targets:
|
||||
if target not in _backlink_index[vault_name]:
|
||||
_backlink_index[vault_name][target] = []
|
||||
# Avoid duplicates for same source-target pair
|
||||
existing = [e for e in _backlink_index[vault_name][target] if e["path"] == source_path]
|
||||
if not existing:
|
||||
_backlink_index[vault_name][target].append({
|
||||
"vault": vault_name,
|
||||
"path": source_path,
|
||||
"title": source_title,
|
||||
})
|
||||
|
||||
|
||||
def get_backlinks(vault_name: str, file_path: str) -> List[Dict[str, str]]:
|
||||
"""Get all files that link to the given file via wikilinks.
|
||||
|
||||
Searches across all vaults for backlinks pointing to the target file.
|
||||
|
||||
Args:
|
||||
vault_name: The vault containing the target file.
|
||||
file_path: Relative path of the target file in the vault.
|
||||
|
||||
Returns:
|
||||
List of ``{vault, path, title}`` dicts for files linking to the target.
|
||||
"""
|
||||
global _backlink_index
|
||||
target_key = file_path.lower()
|
||||
if not target_key.endswith(".md"):
|
||||
target_key += ".md"
|
||||
|
||||
results = []
|
||||
for vname, vindex in _backlink_index.items():
|
||||
bl = vindex.get(target_key, [])
|
||||
results.extend(bl)
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user