feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
- Add Docker image for the official Obsidian desktop app and CLI. - Start Obsidian headlessly with Xvfb and DBus setup. - Expose a safe structured HTTP command API without shell execution. - Add JWT-based vault, path, and command authorization. - Support single-vault and multi-vault container mounts. - Add TypeScript SDK helpers for Obsidian CLI commands. - Add n8n community node package with Obsidian operations. - Add docs, compose config, tests, and production image workflow.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import asyncio, uuid, time
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from app.config import CLI_BIN
|
||||
|
||||
def error(status_code:int, detail:str) -> JSONResponse:
|
||||
return JSONResponse({"detail": detail}, status_code=status_code)
|
||||
|
||||
async def execute_command(args:list[str], cwd:Path, stdin:str|None, timeout:float) -> JSONResponse:
|
||||
command_id = str(uuid.uuid4())
|
||||
command = [CLI_BIN, *args]
|
||||
started = time.perf_counter()
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
cwd=str(cwd),
|
||||
stdin=asyncio.subprocess.PIPE if stdin is not None else None,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return error(500, f"configured CLI binary was not found: {CLI_BIN}")
|
||||
|
||||
try:
|
||||
stdout_b, stderr_b = await asyncio.wait_for(
|
||||
process.communicate(None if stdin is None else stdin.encode()),
|
||||
timeout=timeout,
|
||||
)
|
||||
timed_out = False
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
stdout_b, stderr_b = await process.communicate()
|
||||
timed_out = True
|
||||
|
||||
duration_ms = int((time.perf_counter() - started) * 1000)
|
||||
stdout = stdout_b.decode(errors="replace")
|
||||
stderr = stderr_b.decode(errors="replace")
|
||||
exit_code = process.returncode if process.returncode is not None else -1
|
||||
|
||||
if timed_out:
|
||||
exit_code = 124
|
||||
stderr = (stderr + f"\nCommand timed out after {timeout}s").strip()
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"id": command_id,
|
||||
"command": command,
|
||||
"cwd": str(cwd),
|
||||
"exit_code": exit_code,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"duration_ms": duration_ms,
|
||||
"timed_out": timed_out,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user