Files
Crumb-Core-v.1/app/routers/docs_reader.py

154 lines
5.3 KiB
Python

import os
import markdown
from fastapi import APIRouter, Request, HTTPException, Depends
from fastapi.responses import HTMLResponse
from deps import current_user
router = APIRouter()
# Whitelist allowed files.
# We look for them in the root directory relative to 'app' parent.
# Since app is in /app inside docker, the root volume mount is usually at /app or mapped.
# Code Tour Step 828 shows 'app/main.py' path analysis.
# If workdir is /app (docker), and files are in /app (root of repo mapped to /app), then "README.md" is at "./README.md".
ALLOWED_DOCS = {
"README.md": "Start Guide (Readme)",
"QUICKSTART.md": "Schnellstart",
"HANDBUCH.md": "Benutzerhandbuch",
"CODE_TOUR.md": "Code Tour (Dev)",
"ARCHITECTURE_ROLES_GROUPS.md": "Architektur",
"DIARY_RAG_README.md": "Tagebuch & RAG Info",
"CrumbTech.md": "Technische Details",
"QDRANT_ACCESS.md": "Vektor DB Access",
"docs_git.md": "Guide - Git & Versionierung",
"deploy_security_fixes.sh": "Security Script (Source)" # Maybe viewing scripts is cool too? Let's stick to MD for now.
}
from config import get_settings
@router.get("/docs", response_class=HTMLResponse)
async def list_docs(req: Request):
"""
List available documentation files dynamically.
"""
base_path = _get_docs_base_path()
# 1. Pinned Docs (from whitelist)
pinned = []
# 2. All other docs (dynamic scan)
library = []
# helper to check if already in pinned
pinned_filenames = set()
for filename, title in ALLOWED_DOCS.items():
full_path = _find_file(base_path, filename) # Helper now needs to look deeper?
if full_path:
pinned.append({"name": title, "file": filename})
pinned_filenames.add(filename)
# Scan for all .md files recursively
if os.path.exists(base_path):
for root, dirs, files in os.walk(base_path):
for file in files:
if file.endswith(".md"):
# Calculate relative path or just filename.
# If we use just filename, distinct files with same name in diff folders conflict.
# Current view_doc takes {filename}. Let's assume filenames are unique enough or we handle paths.
# For now, let's just show them if they aren't pinned.
if file not in pinned_filenames:
# Try to get a nice name (Title from first line?) or filename
name = file.replace(".md", "").replace("-", " ").title()
library.append({"name": name, "file": file})
# Combine or separate? Let's just return all in 'docs' for now to fit the template expectation.
# The template iterates 'docs'.
all_docs = pinned + sorted(library, key=lambda x: x["name"])
return req.app.state.render(
req,
"pages/docs_index.html",
docs=all_docs,
page_title="Dokumentation"
)
@router.get("/docs/{filename}", response_class=HTMLResponse)
async def view_doc(req: Request, filename: str):
"""
Render a specific markdown file. Now supports any MD file found.
"""
# Security: basic check to prevent directory traversal outside allowable scope
if ".." in filename or "/" in filename: # filename param is usually just the last part if caught by path param?
# Actually FastAPI path param "{filename}" stops at slashes unless defined as "{path:path}".
# So 'filename' here is just the leaf name.
pass
base_path = _get_docs_base_path()
# Use recursive find
file_path = _find_file_recursive(base_path, filename)
if not file_path:
raise HTTPException(404, "File not on server.")
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
html_content = markdown.markdown(
content,
extensions=['tables', 'fenced_code', 'nl2br']
)
# Determine Title
title = ALLOWED_DOCS.get(filename, filename.replace(".md", "").replace("-", " ").title())
return req.app.state.render(
req,
"pages/doc_viewer.html",
doc_title=title,
doc_content=html_content,
filename=filename
)
except Exception as e:
raise HTTPException(500, f"Error rendering document: {e}")
def _get_docs_base_path():
try:
settings = get_settings()
base_path = settings.docs_path
except:
base_path = "docs"
if not os.path.isabs(base_path) and not os.path.exists(base_path):
if os.path.exists("/docs_root"):
base_path = "/docs_root"
else:
base_path = "."
return base_path
def _find_file(base_path, filename):
"""Legacy helper: find in base or level 1 subdirs."""
return _find_file_recursive(base_path, filename)
def _find_file_recursive(base_path, filename):
"""Find file recursively in base_path."""
# 1. Direct check
direct = os.path.join(base_path, filename)
if os.path.exists(direct) and os.path.isfile(direct):
return direct
# 2. Walk
for root, dirs, files in os.walk(base_path):
if filename in files:
return os.path.join(root, filename)
return None