570 lines
23 KiB
Python
570 lines
23 KiB
Python
"""Media, PDF, export & vault-settings endpoints (ROADMAP #85, tranche 6c).
|
|
|
|
Handlers déplacés depuis :mod:`backend.main` sans changement de
|
|
comportement : mêmes chemins (``/api/file/*/pdf*``, ``/api/export/*``,
|
|
``/api/guide/download``, ``/api/image/*``, ``/api/media*``,
|
|
``/api/attachments/*``, ``/api/vaults/*/settings``, ``/api/vault/*/files``,
|
|
``/api/vaults/settings/all``), mêmes modèles de réponse, mêmes dépendances
|
|
d'authentification.
|
|
|
|
Adaptations strictement équivalentes :
|
|
- ``_resolve_safe_path`` → :mod:`backend.services.paths` (pass-through).
|
|
- ``_render_markdown`` vient de :mod:`backend.render` (#85 T9, sans cycle
|
|
d'import).
|
|
- ``_resolve_export_target`` / ``_safe_export_name`` (export uniquement)
|
|
sont définis ici ; ``stream_file_with_range`` vit dans
|
|
:mod:`backend.routers.helpers` (partagé).
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
|
|
from fastapi.responses import FileResponse, Response
|
|
|
|
from backend.attachment_indexer import get_attachment_stats, rescan_vault_attachments
|
|
from backend.auth.middleware import check_vault_access, require_admin, require_auth
|
|
from backend.export import ExportError, export_epub, export_html, export_md_bundle
|
|
from backend.history import record_open
|
|
from backend.indexer import get_vault_data, index, parse_markdown_file
|
|
from backend.media_thumbs import generate_thumbnail, is_decodable
|
|
from backend.media_types import is_audio, is_image, is_video, media_mime_type
|
|
from backend.render import _render_markdown
|
|
from backend.routers.helpers import media_max_inline_bytes, stream_file_with_range
|
|
from backend.schemas import (
|
|
AllVaultSettingsResponse,
|
|
AttachmentRescanResponse,
|
|
AttachmentStatsResponse,
|
|
PdfInfoResponse,
|
|
VaultFilesResponse,
|
|
VaultSettingsResponse,
|
|
)
|
|
from backend.secret_redactor import redact_file_content
|
|
from backend.services.paths import resolve_safe_path
|
|
from backend.services.vaults import list_all_files
|
|
from backend.vault_settings import get_vault_setting, update_vault_setting
|
|
|
|
logger = logging.getLogger("obsigate")
|
|
|
|
# Lazy import: WeasyPrint PDF export (requires GTK, may not be available everywhere)
|
|
try:
|
|
from backend.pdf_export import build_pdf_html, generate_pdf
|
|
except Exception: # pragma: no cover - WeasyPrint/GTK missing
|
|
generate_pdf = None # type: ignore[assignment]
|
|
build_pdf_html = None # type: ignore[assignment]
|
|
|
|
logging.getLogger("obsigate").warning("PDF export unavailable (WeasyPrint/GTK not found)")
|
|
|
|
router = APIRouter() # pas de tags : assignation par chemin via openapi_docs.tag_for_path (comme avant)
|
|
|
|
|
|
def _resolve_export_target(vault_name: str, path: str, current_user: dict) -> tuple[Path, Path]:
|
|
"""Resolve a vault + relative path into (vault_root, absolute file path).
|
|
|
|
Enforces auth (vault access) and path traversal protection.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
vault_root = Path(vault_data["path"])
|
|
target = resolve_safe_path(vault_root, path)
|
|
return vault_root, target
|
|
|
|
|
|
def _safe_export_name(name: str) -> str:
|
|
"""ASCII-safe, filename-safe download name (falls back to 'document')."""
|
|
cleaned = "".join(c for c in name if c.isascii() and (c.isalnum() or c in " _-.")).strip()
|
|
return cleaned or "document"
|
|
|
|
|
|
@router.get(
|
|
"/api/file/{vault_name}/pdf",
|
|
response_class=Response,
|
|
responses={200: {"content": {"application/pdf": {}}, "description": "PDF document"}},
|
|
)
|
|
async def api_file_pdf(vault_name: str, path: str = Query(..., description="Relative path to file"), current_user=Depends(require_auth)):
|
|
"""Download a markdown file as PDF."""
|
|
if generate_pdf is None:
|
|
raise HTTPException(501, "PDF export unavailable (WeasyPrint/GTK not available)")
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(404, f"Vault '{vault_name}' not found")
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
if not file_path.exists():
|
|
raise HTTPException(404, f"File not found: {path}")
|
|
try:
|
|
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
|
except Exception:
|
|
raise HTTPException(500, "Cannot read file")
|
|
record_open(current_user.get("username"), vault_name, path)
|
|
raw = redact_file_content(raw, str(file_path))
|
|
post = parse_markdown_file(raw)
|
|
html = _render_markdown(post.content, vault_name, file_path)
|
|
title = post.metadata.get("title", file_path.stem)
|
|
pdf_html = build_pdf_html(html, str(title))
|
|
pdf_bytes = generate_pdf(pdf_html, str(title))
|
|
safe_name = "".join(c for c in str(title) if c.isascii() and (c.isalnum() or c in " _-.")).strip() or "document"
|
|
return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="{safe_name}.pdf"'})
|
|
|
|
|
|
@router.get(
|
|
"/api/export/html",
|
|
response_class=Response,
|
|
responses={200: {"content": {"text/html": {}}, "description": "Standalone HTML file"}},
|
|
)
|
|
async def api_export_html(
|
|
vault: str = Query(..., description="Vault name"),
|
|
path: str = Query(..., description="Relative path to file"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Export a markdown note as a standalone HTML file."""
|
|
try:
|
|
vault_root, target = _resolve_export_target(vault, path, current_user)
|
|
html_bytes = export_html(vault_root, target)
|
|
except ExportError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
record_open(current_user.get("username"), vault, path)
|
|
safe_name = _safe_export_name(target.stem)
|
|
return Response(
|
|
content=html_bytes,
|
|
media_type="text/html; charset=utf-8",
|
|
headers={"Content-Disposition": f'attachment; filename="{safe_name}.html"'},
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/api/export/md-bundle",
|
|
response_class=Response,
|
|
responses={200: {"content": {"application/zip": {}}, "description": "Markdown ZIP bundle"}},
|
|
)
|
|
async def api_export_md_bundle(
|
|
vault: str = Query(..., description="Vault name"),
|
|
path: str = Query(..., description="Relative path to directory or file"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Export a directory (or single file) of markdown as a ZIP bundle."""
|
|
try:
|
|
vault_root, target = _resolve_export_target(vault, path, current_user)
|
|
zip_bytes = export_md_bundle(vault_root, target)
|
|
except ExportError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
safe_name = _safe_export_name(target.name)
|
|
return Response(
|
|
content=zip_bytes,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{safe_name}.zip"'},
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/api/export/epub",
|
|
response_class=Response,
|
|
responses={200: {"content": {"application/epub+zip": {}}, "description": "ePub document"}},
|
|
)
|
|
async def api_export_epub(
|
|
vault: str = Query(..., description="Vault name"),
|
|
path: str = Query(..., description="Relative path to file"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Export a markdown note as an ePub document."""
|
|
try:
|
|
vault_root, target = _resolve_export_target(vault, path, current_user)
|
|
epub_bytes = export_epub(vault_root, target)
|
|
except ExportError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
record_open(current_user.get("username"), vault, path)
|
|
safe_name = _safe_export_name(target.stem)
|
|
return Response(
|
|
content=epub_bytes,
|
|
media_type="application/epub+zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{safe_name}.epub"'},
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/api/guide/download",
|
|
response_class=Response,
|
|
responses={200: {"content": {"application/pdf": {}, "text/markdown": {}}}},
|
|
)
|
|
async def api_guide_download(
|
|
format: str = Query("md", description="Download format: 'md' or 'pdf'"),
|
|
lang: str = Query("fr", description="Guide language: 'fr' or 'en'"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Download the in-app user guide as Markdown or PDF (#105).
|
|
|
|
The document is generated from the live help modal in index.html resolved
|
|
through the locale files, so it always mirrors exactly what the user sees.
|
|
"""
|
|
from backend.guide_export import get_guide_document
|
|
|
|
if format not in ("md", "pdf"):
|
|
raise HTTPException(status_code=400, detail="format doit être 'md' ou 'pdf'")
|
|
try:
|
|
payload, media, fname = get_guide_document(format, lang)
|
|
except Exception as e: # weasyprint/reportlab unavailable
|
|
logger.exception("guide export failed")
|
|
raise HTTPException(status_code=500, detail=f"Export impossible: {e}") from e
|
|
return Response(
|
|
content=payload,
|
|
media_type=media,
|
|
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
|
)
|
|
|
|
|
|
@router.get("/api/file/{vault_name}/pdf/stream", response_class=FileResponse)
|
|
async def api_pdf_stream(
|
|
request: Request,
|
|
vault_name: str,
|
|
path: str = Query(...),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Stream a PDF file with Content-Type: application/pdf for inline browser viewing.
|
|
|
|
Supports HTTP Range requests (206 Partial Content) so browsers can
|
|
progressively render large PDFs in the native viewer.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
if file_path.suffix.lower() != ".pdf":
|
|
raise HTTPException(status_code=400, detail="Not a PDF file")
|
|
|
|
return stream_file_with_range(file_path, request, "application/pdf")
|
|
|
|
|
|
@router.get("/api/file/{vault_name}/pdf/info", response_model=PdfInfoResponse)
|
|
async def api_pdf_info(
|
|
vault_name: str,
|
|
path: str = Query(..., description="Relative path to PDF file"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Return PDF metadata (pages, title, author, size) without the document content.
|
|
|
|
Lets the UI display file info before loading a heavy PDF into the viewer.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
|
if file_path.suffix.lower() != ".pdf":
|
|
raise HTTPException(status_code=400, detail="Not a PDF file")
|
|
|
|
from backend.pdf_reader import extract_pdf_metadata
|
|
meta = extract_pdf_metadata(file_path)
|
|
stat = file_path.stat()
|
|
return {
|
|
"vault": vault_name,
|
|
"path": path,
|
|
"pages": meta.get("pages", 0),
|
|
"title": meta.get("title") or file_path.name,
|
|
"author": meta.get("author", ""),
|
|
"size_bytes": stat.st_size,
|
|
}
|
|
|
|
|
|
@router.get(
|
|
"/api/image/{vault_name}",
|
|
response_class=Response,
|
|
responses={200: {"content": {"application/octet-stream": {}}, "description": "Image bytes"}},
|
|
)
|
|
async def api_image(vault_name: str, path: str = Query(..., description="Relative path to image"), current_user=Depends(require_auth)):
|
|
"""Serve an image file with proper MIME type.
|
|
|
|
Args:
|
|
vault_name: Name of the vault.
|
|
path: Relative file path within the vault.
|
|
|
|
Returns:
|
|
Image file with appropriate content-type header.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail=f"Image not found: {path}")
|
|
|
|
mime_type = media_mime_type(str(file_path))
|
|
|
|
# #108-B3 — a standalone SVG opened in a tab executes its embedded JS
|
|
# (same-origin XSS). ``sandbox`` forces a unique opaque origin with no
|
|
# script execution; inside an <img> tag the header is irrelevant.
|
|
headers = {"X-Content-Type-Options": "nosniff"}
|
|
if file_path.suffix.lower() == ".svg":
|
|
headers["Content-Security-Policy"] = "sandbox"
|
|
|
|
try:
|
|
# Read and return the image file
|
|
content = file_path.read_bytes()
|
|
return Response(content=content, media_type=mime_type, headers=headers)
|
|
except PermissionError:
|
|
raise HTTPException(status_code=403, detail="Permission denied")
|
|
except Exception as e:
|
|
logger.error(f"Error serving image {vault_name}/{path}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Error serving image: {e!s}")
|
|
|
|
|
|
@router.get("/api/media/{vault_name}", response_class=FileResponse)
|
|
async def api_media_stream(
|
|
request: Request,
|
|
vault_name: str,
|
|
path: str = Query(..., description="Relative path to audio/video file"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Stream an audio/video file with HTTP Range support (roadmap #109-A2).
|
|
|
|
Serves the bytes with the correct MIME type and honours ``Range`` requests
|
|
(``206 Partial Content`` + ``Content-Range``/``Accept-Ranges``), which is
|
|
what enables scrubbing in ``<audio>``/``<video>`` and is required by Safari
|
|
for MP4. Files above ``OBSIGATE_MEDIA_MAX_INLINE_MB`` (default 500 MB) are
|
|
refused with ``413`` — the viewer falls back to the download button.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail=f"Media not found: {path}")
|
|
|
|
ext = file_path.suffix.lower()
|
|
if not (is_audio(ext) or is_video(ext)):
|
|
raise HTTPException(status_code=400, detail="Not an audio/video file")
|
|
|
|
if file_path.stat().st_size > media_max_inline_bytes():
|
|
raise HTTPException(status_code=413, detail="Media too large for inline streaming")
|
|
|
|
return stream_file_with_range(file_path, request, media_mime_type(str(file_path)))
|
|
|
|
|
|
@router.get("/api/media/{vault_name}/thumb", response_class=FileResponse)
|
|
async def api_media_thumb(
|
|
vault_name: str,
|
|
path: str = Query(..., description="Relative path to image"),
|
|
size: int = Query(256, ge=32, le=1024, description="Max thumbnail edge in pixels"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""Serve a cached WebP thumbnail of an image (roadmap #108-C).
|
|
|
|
SVG (and any format Pillow cannot decode) falls back to the original
|
|
bytes. Generation runs in a thread and is capped at 2 s; on timeout or
|
|
failure the original is served so the UI never breaks.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
vault_root = Path(vault_data["path"])
|
|
file_path = resolve_safe_path(vault_root, path)
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail=f"Image not found: {path}")
|
|
if not is_image(file_path.suffix.lower()):
|
|
raise HTTPException(status_code=400, detail="Not an image file")
|
|
|
|
mime_type = media_mime_type(str(file_path))
|
|
if not is_decodable(file_path):
|
|
# SVG: never let a standalone navigation execute embedded JS (#108-B3).
|
|
svg_headers = {"X-Content-Type-Options": "nosniff"}
|
|
if file_path.suffix.lower() == ".svg":
|
|
svg_headers["Content-Security-Policy"] = "sandbox"
|
|
return FileResponse(str(file_path), media_type=mime_type, headers=svg_headers)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
thumb: Path | None = None
|
|
try:
|
|
thumb = await asyncio.wait_for(
|
|
loop.run_in_executor(None, generate_thumbnail, file_path, size),
|
|
timeout=2.0,
|
|
)
|
|
except Exception:
|
|
thumb = None
|
|
|
|
if thumb is not None and thumb.exists():
|
|
return FileResponse(str(thumb), media_type="image/webp")
|
|
return FileResponse(str(file_path), media_type=mime_type)
|
|
|
|
|
|
@router.post("/api/attachments/rescan/{vault_name}", response_model=AttachmentRescanResponse)
|
|
async def api_rescan_attachments(vault_name: str, current_user=Depends(require_admin)):
|
|
"""Rescan attachments for a specific vault.
|
|
|
|
Args:
|
|
vault_name: Name of the vault to rescan.
|
|
|
|
Returns:
|
|
Dict with status and attachment count.
|
|
"""
|
|
vault_data = get_vault_data(vault_name)
|
|
if not vault_data:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
vault_path = vault_data["path"]
|
|
count = await rescan_vault_attachments(vault_name, vault_path)
|
|
|
|
logger.info(f"Rescanned attachments for vault '{vault_name}': {count} attachments")
|
|
return {"status": "ok", "vault": vault_name, "attachment_count": count}
|
|
|
|
|
|
@router.get("/api/attachments/stats", response_model=AttachmentStatsResponse)
|
|
async def api_attachment_stats(vault: str | None = Query(None, description="Vault filter"), current_user=Depends(require_auth)):
|
|
"""Get attachment statistics for vaults.
|
|
|
|
Args:
|
|
vault: Optional vault name to filter stats.
|
|
|
|
Returns:
|
|
Dict with vault names as keys and attachment counts as values.
|
|
"""
|
|
stats = get_attachment_stats(vault)
|
|
return {"vaults": stats}
|
|
|
|
|
|
@router.get("/api/vaults/{vault_name}/settings", response_model=VaultSettingsResponse)
|
|
async def api_get_vault_settings(vault_name: str, current_user=Depends(require_auth)):
|
|
"""Get UI display settings for a specific vault.
|
|
|
|
Args:
|
|
vault_name: Name of the vault.
|
|
|
|
Returns:
|
|
Dict with vault settings including hideHiddenFiles.
|
|
"""
|
|
if vault_name not in index:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
# Get persisted settings
|
|
persisted = get_vault_setting(vault_name) or {}
|
|
|
|
# Default settings
|
|
settings = {
|
|
"hideHiddenFiles": False,
|
|
}
|
|
settings.update(persisted)
|
|
|
|
return settings
|
|
|
|
|
|
@router.post("/api/vaults/{vault_name}/settings", response_model=VaultSettingsResponse)
|
|
async def api_update_vault_settings(vault_name: str, body: dict = Body(...), current_user=Depends(require_admin)):
|
|
"""Update UI display settings for a specific vault.
|
|
|
|
Args:
|
|
vault_name: Name of the vault.
|
|
body: Dict with settings to update (hideHiddenFiles).
|
|
|
|
Returns:
|
|
Updated settings dict.
|
|
"""
|
|
if vault_name not in index:
|
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
|
|
|
# Validate settings
|
|
settings_to_update = {}
|
|
|
|
if "hideHiddenFiles" in body:
|
|
if not isinstance(body["hideHiddenFiles"], bool):
|
|
raise HTTPException(status_code=400, detail="hideHiddenFiles must be a boolean")
|
|
settings_to_update["hideHiddenFiles"] = body["hideHiddenFiles"]
|
|
|
|
# Update persisted settings
|
|
try:
|
|
updated = update_vault_setting(vault_name, settings_to_update)
|
|
except PermissionError as e:
|
|
logger.error(f"Permission error saving settings for vault '{vault_name}': {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Permission denied: Cannot write to settings file. Check /app/data permissions."
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error saving settings for vault '{vault_name}': {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to save settings: {e!s}"
|
|
)
|
|
|
|
logger.info(f"Updated settings for vault '{vault_name}': {settings_to_update}")
|
|
|
|
return updated
|
|
|
|
|
|
@router.get("/api/vault/{vault_name}/files", response_model=VaultFilesResponse)
|
|
async def api_vault_recent_files(
|
|
vault_name: str,
|
|
dir: str = Query("", description="Directory path within the vault (empty = root)"),
|
|
limit: int = Query(200, description="Maximum number of files to return"),
|
|
recursive: bool = Query(True, description="If true, list files recursively from directory and all subdirectories"),
|
|
current_user=Depends(require_auth),
|
|
):
|
|
"""List files in a vault directory sorted by modification time (newest first).
|
|
|
|
Returns file metadata suitable for a vault home page display.
|
|
Unlike /api/browse, this endpoint sorts by mtime and returns
|
|
additional metadata (size, modified time, extension).
|
|
|
|
When recursive=True (default), lists files from the directory
|
|
AND all its subdirectories, with a ``rel_dir`` field indicating
|
|
the subdirectory path relative to the requested directory.
|
|
|
|
Args:
|
|
vault_name: Name of the vault.
|
|
dir: Relative directory path within the vault (empty for root).
|
|
limit: Maximum files to return (default 200).
|
|
recursive: If true, recursively list files in subdirectories (default true).
|
|
|
|
Returns:
|
|
JSON with vault, directory, count, recursive flag, and list of file entries.
|
|
"""
|
|
if not check_vault_access(vault_name, current_user):
|
|
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
|
|
|
return list_all_files(vault_name, dir=dir, limit=limit, recursive=recursive)
|
|
|
|
|
|
@router.get("/api/vaults/settings/all", response_model=AllVaultSettingsResponse)
|
|
async def api_get_all_vault_settings(current_user=Depends(require_auth)):
|
|
"""Get UI display settings for all vaults.
|
|
|
|
Returns:
|
|
Dict mapping vault names to their settings.
|
|
"""
|
|
all_settings = {}
|
|
|
|
for vault_name in index:
|
|
persisted = get_vault_setting(vault_name) or {}
|
|
|
|
settings = {
|
|
"hideHiddenFiles": False,
|
|
}
|
|
settings.update(persisted)
|
|
all_settings[vault_name] = settings
|
|
|
|
return all_settings
|