169 lines
5.9 KiB
Python
169 lines
5.9 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 = []
|
|
pinned_paths = set()
|
|
|
|
for filename, title in ALLOWED_DOCS.items():
|
|
# Check root level only for pinned items (simpler, or check recursively and pin first?)
|
|
# Legacy behavior was pinned items are usually in root.
|
|
# Let's verify existence.
|
|
full_path = _find_file_recursive(base_path, filename)
|
|
if full_path:
|
|
# We want the relative path as the ID
|
|
rel_path = os.path.relpath(full_path, base_path)
|
|
pinned.append({"name": title, "file": rel_path})
|
|
pinned_paths.add(rel_path)
|
|
|
|
# 2. All other docs (dynamic scan)
|
|
library = []
|
|
|
|
if os.path.exists(base_path):
|
|
for root, dirs, files in os.walk(base_path):
|
|
for file in files:
|
|
if file.endswith(".md"):
|
|
full_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(full_path, base_path)
|
|
|
|
if rel_path not in pinned_paths:
|
|
# Improved Naming: File name + Parent Dir if nested
|
|
# e.g. crumbcodex/README.md -> "Crumbcodex / Readme"
|
|
parent = os.path.basename(root)
|
|
safe_name = file.replace(".md", "").replace("-", " ").title()
|
|
|
|
if parent and parent != os.path.basename(base_path) and parent != "docs":
|
|
# If deeper than root
|
|
display_name = f"{parent.title()} / {safe_name}"
|
|
else:
|
|
display_name = safe_name
|
|
|
|
library.append({"name": display_name, "file": rel_path})
|
|
|
|
# Combine
|
|
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/{file_path:path}", response_class=HTMLResponse)
|
|
async def view_doc(req: Request, file_path: str):
|
|
"""
|
|
Render a specific markdown file. specific via relative path.
|
|
"""
|
|
# Security: Prevent traversal up
|
|
if ".." in file_path:
|
|
raise HTTPException(400, "Invalid path.")
|
|
|
|
base_path = _get_docs_base_path()
|
|
full_path = os.path.join(base_path, file_path)
|
|
|
|
if not os.path.exists(full_path) or not os.path.isfile(full_path):
|
|
# Fallback: maybe it was a legacy link with just filename?
|
|
# Try finding it recursively if direct path fails
|
|
found = _find_file_recursive(base_path, os.path.basename(file_path))
|
|
if found:
|
|
full_path = found
|
|
else:
|
|
raise HTTPException(404, "File not on server.")
|
|
|
|
try:
|
|
with open(full_path, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
html_content = markdown.markdown(
|
|
content,
|
|
extensions=['tables', 'fenced_code', 'nl2br']
|
|
)
|
|
|
|
# Determine Title
|
|
filename = os.path.basename(full_path)
|
|
# Check if pinned to get nice title
|
|
title = filename.replace(".md", "").replace("-", " ").title()
|
|
for k, v in ALLOWED_DOCS.items():
|
|
if k == filename:
|
|
title = v
|
|
break
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"pages/doc_viewer.html",
|
|
doc_title=title,
|
|
doc_content=html_content,
|
|
filename=file_path # Keep full path for context? Or filename? Template uses it for back link maybe?
|
|
)
|
|
|
|
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
|