fava_client: resolve relative target_file paths against the ledger root
Fava's /api/source endpoint rejects relative paths with HTTP 500 (NonSourceFileError: "Trying to read a non-source file at '...'"). The include-aware `_infer_target_file` helper returns relative paths (e.g. "accounts/users.beancount"), so add a `_resolve_target_file` hook that prepends the ledger root directory. The dirname is derived from a one-time GET /api/options and cached on the FavaClient instance (which is a module-level singleton), guarded by an asyncio.Lock so concurrent first-callers don't double-fetch. Absolute paths pass through unchanged, so the admin endpoint that explicitly passes target_file="accounts/chart.beancount" works the same as one that passes "/var/lib/fava/accounts/chart.beancount". Verified against aio-demo's live fava: relative paths now produce HTTP 200 reads on options.beancount, accounts/chart.beancount, accounts/users.beancount, and transactions.beancount. Refs: aiolabs/libra#28
This commit is contained in:
parent
34ecb3f249
commit
d82443d040
1 changed files with 43 additions and 0 deletions
|
|
@ -80,6 +80,46 @@ class FavaClient:
|
||||||
# Per-user locks for user-specific operations (reduces contention)
|
# Per-user locks for user-specific operations (reduces contention)
|
||||||
self._user_locks: Dict[str, asyncio.Lock] = {}
|
self._user_locks: Dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
|
# Cached absolute dirname of the root ledger file, derived from
|
||||||
|
# GET /api/options on first need. Used by `_resolve_target_file` to
|
||||||
|
# turn relative include paths (e.g. "accounts/users.beancount") into
|
||||||
|
# the absolute paths fava's /api/source endpoint requires.
|
||||||
|
self._main_dir_cache: Optional[str] = None
|
||||||
|
self._main_dir_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def _resolve_target_file(self, target_file: str) -> str:
|
||||||
|
"""
|
||||||
|
Turn a relative include path into the absolute path fava expects.
|
||||||
|
|
||||||
|
Fava's /api/source endpoint refuses relative paths with HTTP 500
|
||||||
|
(NonSourceFileError). Resolve any non-absolute target_file by
|
||||||
|
prepending the directory of the root ledger file (cached after
|
||||||
|
the first GET /api/options).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_file: Relative (e.g. "accounts/users.beancount") or
|
||||||
|
absolute path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Absolute path under fava's ledger root.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
if os.path.isabs(target_file):
|
||||||
|
return target_file
|
||||||
|
|
||||||
|
if self._main_dir_cache is None:
|
||||||
|
async with self._main_dir_lock:
|
||||||
|
if self._main_dir_cache is None:
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
|
resp = await client.get(f"{self.base_url}/options")
|
||||||
|
resp.raise_for_status()
|
||||||
|
main_file = resp.json()["data"]["beancount_options"]["filename"]
|
||||||
|
self._main_dir_cache = os.path.dirname(main_file)
|
||||||
|
logger.debug(f"Cached fava ledger root dir: {self._main_dir_cache}")
|
||||||
|
|
||||||
|
return os.path.join(self._main_dir_cache, target_file)
|
||||||
|
|
||||||
def get_user_lock(self, user_id: str) -> asyncio.Lock:
|
def get_user_lock(self, user_id: str) -> asyncio.Lock:
|
||||||
"""
|
"""
|
||||||
Get or create a lock for a specific user.
|
Get or create a lock for a specific user.
|
||||||
|
|
@ -1560,6 +1600,9 @@ class FavaClient:
|
||||||
if target_file is None:
|
if target_file is None:
|
||||||
target_file = _infer_target_file(account_name)
|
target_file = _infer_target_file(account_name)
|
||||||
|
|
||||||
|
# Fava's /api/source requires absolute paths; convert if needed.
|
||||||
|
target_file = await self._resolve_target_file(target_file)
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue