import asyncio import html as html_mod import json as _json import logging import os import re import secrets import shutil import string import urllib.request from contextlib import asynccontextmanager from pathlib import Path from typing import Any from urllib.parse import quote import frontmatter import mistune from fastapi import Body, Depends, FastAPI, HTTPException, Query, Request, WebSocket from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from starlette.middleware.base import BaseHTTPMiddleware from backend.attachment_indexer import get_attachment_stats, rescan_vault_attachments from backend.collab import authenticate_websocket, collab_manager from backend.history import ( get_bookmarks, record_open, remove_recent, toggle_bookmark, update_bookmarks_after_rename, update_history_after_rename, ) from backend.image_processor import preprocess_images from backend.indexer import ( _extract_tags, add_vault_to_index, build_index, find_file_in_index, get_backlinks, get_conflicts, get_vault_data, handle_file_move, index, parse_markdown_file, remove_single_file, remove_vault_from_index, update_single_file, ) from backend.media_thumbs import generate_thumbnail, is_decodable from backend.media_types import IMAGE_EXTENSIONS, is_audio, is_image, is_video, media_mime_type from backend.openapi_docs import ( API_DESCRIPTION, TAGS_METADATA, enrich_openapi_schema, render_api_landing, ) from backend.schemas import ( AIKeyDeleteResponse, AIKeysResponse, AIModelsResponse, AITestResponse, AllVaultSettingsResponse, AppConfigResponse, AttachmentRescanResponse, AttachmentStatsResponse, BacklinksResponse, BookmarksResponse, BookmarkToggleResponse, ConflictResolveResponse, ConflictsResponse, DashboardResponse, DiagnosticsResponse, PdfInfoResponse, RecentResponse, SavedSearch, StatusResponse, VaultActionResponse, VaultFilesResponse, VaultSettingsResponse, VaultsStatusResponse, VaultStatsResponse, ) from backend.search import ( init_inverted_index, ) from backend.semantic_search import init_semantic_index from backend.services.backups import get_backup_dir as service_get_backup_dir from backend.services.errors import ServiceError from backend.services.files import read_raw_file from backend.services.mutations import ( batch_upload_files as service_batch_upload_files, ) from backend.services.mutations import ( create_directory as service_create_directory, ) from backend.services.mutations import ( create_file as service_create_file, ) from backend.services.mutations import ( delete_directory as service_delete_directory, ) from backend.services.mutations import ( delete_file as service_delete_file, ) from backend.services.mutations import ( edit_file as service_edit_file, ) from backend.services.mutations import ( edit_xlsx_cells as service_edit_xlsx_cells, ) from backend.services.mutations import ( move_path as service_move_path, ) from backend.services.mutations import ( rename_directory as service_rename_directory, ) from backend.services.mutations import ( rename_file as service_rename_file, ) from backend.services.recent import humanize_mtime, list_recent from backend.services.sanitizer import sanitize_html from backend.services.vaults import ( browse_directory, list_accessible_vaults, list_all_files, ) from backend.vault_settings import ( get_vault_setting, update_vault_setting, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", ) logger = logging.getLogger("obsigate") # --------------------------------------------------------------------------- # Pydantic response models # --------------------------------------------------------------------------- class VaultInfo(BaseModel): """Summary information about a configured vault.""" name: str = Field(description="Display name of the vault") file_count: int = Field(description="Number of indexed files") tag_count: int = Field(description="Number of unique tags") type: str = Field(default="VAULT", description="Type of the vault mapping (VAULT or DIR)") class BrowseItem(BaseModel): """A single entry (file or directory) returned by the browse endpoint.""" name: str = Field(description="File or directory name") path: str = Field(description="Relative path within vault") type: str = Field(description="'file' or 'directory'") children_count: int | None = Field(default=None, description="Number of children (directories only)") size: int | None = Field(default=None, description="File size in bytes") extension: str | None = Field(default=None, description="File extension") class BrowseResponse(BaseModel): """Paginated directory listing for a vault.""" vault: str path: str items: list[BrowseItem] class FileContentResponse(BaseModel): """Rendered file content with metadata.""" vault: str = Field(description="Vault name") path: str = Field(description="Relative file path within the vault") title: str = Field(description="File title (from frontmatter or filename)") tags: list[str] = Field(description="Extracted tags from frontmatter and inline #tags") frontmatter: dict[str, Any] = Field(description="YAML frontmatter as key-value dict") html: str = Field(description="Rendered HTML content") raw_length: int = Field(description="Length of raw file content in characters") extension: str = Field(description="File extension (e.g. .md, .txt)") is_markdown: bool = Field(description="Whether the file is markdown") unsupported: bool | None = Field(default=False, description="True for binary/unsupported files") size_bytes: int | None = Field(default=None, description="File size in bytes (for unsupported files)") is_pdf: bool | None = Field(default=None, description="True for PDF files") is_image: bool | None = Field(default=None, description="True for image files") is_audio: bool | None = Field(default=None, description="True for audio files (HTML5