diff --git a/backend/export.py b/backend/export.py index cea3fd2..6bd2a28 100644 --- a/backend/export.py +++ b/backend/export.py @@ -8,6 +8,7 @@ replaced with placeholder HTML since they would make exports too large. """ import base64 +import logging import re from pathlib import Path from typing import Optional, Tuple @@ -16,6 +17,8 @@ import mimetypes # Import shared media type definitions and scanner from utils to avoid duplication from backend.utils import MEDIA_EXTENSIONS, get_media_type, scan_notes_fast_walk +logger = logging.getLogger("uvicorn.error") + def get_media_as_base64(media_path: Path) -> Optional[Tuple[str, str]]: """ @@ -41,7 +44,7 @@ def get_media_as_base64(media_path: Path) -> Optional[Tuple[str, str]]: base64_data = base64.b64encode(media_data).decode('utf-8') return (f"data:{mime_type};base64,{base64_data}", media_type) except Exception as e: - print(f"Failed to read media {media_path}: {e}") + logger.error("Failed to read media %s: %s", media_path, e) return None diff --git a/backend/note_index.py b/backend/note_index.py index 968b5ba..ebcc780 100644 --- a/backend/note_index.py +++ b/backend/note_index.py @@ -11,6 +11,7 @@ facades at the bottom of this file. from __future__ import annotations +import logging import os import re import threading @@ -21,6 +22,8 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Set, Tuple +logger = logging.getLogger("uvicorn.error") + # Below this many markdown files, parallel tag extraction adds more overhead # than it saves. @@ -899,7 +902,7 @@ def on_note_saved(notes_dir: str, full_path: Path, content: str) -> None: ) _index.update_note(record, extract_links_from_content(content), content=content) except Exception as e: - print(f"note_index: on_note_saved failed for {full_path}: {e}") + logger.error("note_index: on_note_saved failed for %s: %s", full_path, e) def on_note_deleted(notes_dir: str, full_path: Path) -> None: @@ -907,7 +910,7 @@ def on_note_deleted(notes_dir: str, full_path: Path) -> None: rel_path = full_path.relative_to(Path(notes_dir)).as_posix() _index.remove_note(rel_path) except Exception as e: - print(f"note_index: on_note_deleted failed for {full_path}: {e}") + logger.error("note_index: on_note_deleted failed for %s: %s", full_path, e) def on_note_renamed(notes_dir: str, old_full_path: Path, new_full_path: Path) -> None: @@ -918,7 +921,7 @@ def on_note_renamed(notes_dir: str, old_full_path: Path, new_full_path: Path) -> new_full_path.relative_to(base).as_posix(), ) except Exception as e: - print(f"note_index: on_note_renamed failed: {e}") + logger.error("note_index: on_note_renamed failed: %s", e) def on_folder_renamed(notes_dir: str, old_full_path: Path, new_full_path: Path) -> None: @@ -930,7 +933,7 @@ def on_folder_renamed(notes_dir: str, old_full_path: Path, new_full_path: Path) new_full_path.relative_to(base).as_posix(), ) except Exception as e: - print(f"note_index: on_folder_renamed failed: {e}") + logger.error("note_index: on_folder_renamed failed: %s", e) _index.invalidate() @@ -939,7 +942,7 @@ def on_folder_deleted(notes_dir: str, full_path: Path) -> None: rel_prefix = full_path.relative_to(Path(notes_dir)).as_posix() _index.remove_folder_prefix(rel_prefix) except Exception as e: - print(f"note_index: on_folder_deleted failed: {e}") + logger.error("note_index: on_folder_deleted failed: %s", e) _index.invalidate() @@ -952,7 +955,7 @@ def populate_from_scan( try: _index.bulk_set(notes_meta, folders, sources_raw) except Exception as e: - print(f"note_index: populate_from_scan failed: {e}") + logger.error("note_index: populate_from_scan failed: %s", e) def ensure_search_index(notes_dir: str) -> bool: diff --git a/backend/share.py b/backend/share.py index 01b79ff..1d0fde8 100644 --- a/backend/share.py +++ b/backend/share.py @@ -4,6 +4,7 @@ Handles creating, storing, and revoking share tokens for public note access. """ import json +import logging import secrets import string from pathlib import Path @@ -13,6 +14,8 @@ import threading from .utils import validate_path_security +logger = logging.getLogger("uvicorn.error") + # Thread lock for safe concurrent access _lock = threading.Lock() @@ -99,7 +102,7 @@ def save_tokens(data_dir: str, tokens: Dict[str, Dict[str, Any]]) -> bool: json.dump(tokens, f, indent=2, ensure_ascii=False) return True except IOError as e: - print(f"Failed to save share tokens: {e}") + logger.error("Failed to save share tokens: %s", e) return False diff --git a/backend/themes.py b/backend/themes.py index 32cd31b..8945393 100644 --- a/backend/themes.py +++ b/backend/themes.py @@ -2,10 +2,13 @@ Theme management for NoteDiscovery """ +import logging from pathlib import Path from typing import List, Dict import re +logger = logging.getLogger("uvicorn.error") + def parse_theme_metadata(theme_path: Path) -> Dict[str, str]: """Parse theme metadata from CSS file comments""" @@ -28,7 +31,7 @@ def parse_theme_metadata(theme_path: Path) -> Dict[str, str]: metadata["type"] = match.group(1) break except Exception as e: - print(f"Error parsing theme metadata from {theme_path}: {e}") + logger.error("Error parsing theme metadata from %s: %s", theme_path, e) return metadata diff --git a/backend/utils.py b/backend/utils.py index 2bf30c6..9f5e05f 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,6 +5,7 @@ Heavy work (backlinks, graph, full-text search, tag aggregation) is delegated to the in-memory index in note_index.py. """ +import logging import os import re import shutil @@ -21,6 +22,8 @@ from datetime import datetime, timezone from . import note_index from .note_index import NoteRecord, extract_links_from_content +logger = logging.getLogger("uvicorn.error") + # ============================================================================ # Pagination Support @@ -443,13 +446,13 @@ def delete_folder(notes_dir: str, folder_path: str) -> bool: full_path = Path(notes_dir) / folder_path if not validate_path_security(notes_dir, full_path): - print(f"Security: Path is outside notes directory: {full_path}") + logger.warning("Security: Path is outside notes directory: %s", full_path) return False if not full_path.exists(): - print(f"Folder does not exist: {full_path}") + logger.warning("Folder does not exist: %s", full_path) return False if not full_path.is_dir(): - print(f"Path is not a directory: {full_path}") + logger.warning("Path is not a directory: %s", full_path) return False _drop_prefix_caches(full_path) @@ -458,8 +461,8 @@ def delete_folder(notes_dir: str, folder_path: str) -> bool: _scan_cache_invalidate() return True except Exception as e: - print(f"Error deleting folder '{folder_path}': {e}") - traceback.print_exc() + logger.error("Error deleting folder '%s': %s", folder_path, e) + logger.error(traceback.format_exc()) return False @@ -734,7 +737,7 @@ def save_uploaded_image( f.write(file_data) return str(full_path.relative_to(base).as_posix()) except OSError as e: - print(f"Error saving image: {e}") + logger.error("Error saving image: %s", e) return None sanitized_name = sanitize_filename(filename) @@ -746,14 +749,14 @@ def save_uploaded_image( attachments_dir.mkdir(parents=True, exist_ok=True) full_path = attachments_dir / final_filename if not validate_path_security(notes_dir, full_path): - print(f"Security: Attempted to save image outside notes directory: {full_path}") + logger.warning("Security: Attempted to save image outside notes directory: %s", full_path) return None try: with open(full_path, "wb") as f: f.write(file_data) return str(full_path.relative_to(base).as_posix()) except OSError as e: - print(f"Error saving image: {e}") + logger.error("Error saving image: %s", e) return None @@ -869,8 +872,7 @@ def parse_tags(content: str) -> List[str]: return sorted(list(set(tags))) except Exception as e: - # If parsing fails, return empty list - print(f"Error parsing tags: {e}") + logger.error("Error parsing tags: %s", e) return [] @@ -923,15 +925,14 @@ def get_templates(notes_dir: str) -> List[Dict]: # Security check: ensure _templates folder is within notes directory if not validate_path_security(notes_dir, templates_path): - print(f"Security: Templates directory is outside notes directory: {templates_path}") + logger.warning("Security: Templates directory is outside notes directory: %s", templates_path) return templates try: for template_file in templates_path.glob("*.md"): try: - # Security check: ensure each template is within notes directory if not validate_path_security(notes_dir, template_file): - print(f"Security: Skipping template outside notes directory: {template_file}") + logger.warning("Security: Skipping template outside notes directory: %s", template_file) continue stat = template_file.stat() @@ -941,10 +942,10 @@ def get_templates(notes_dir: str) -> List[Dict]: "modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat() }) except Exception as e: - print(f"Error reading template {template_file}: {e}") + logger.error("Error reading template %s: %s", template_file, e) continue except Exception as e: - print(f"Error accessing templates directory: {e}") + logger.error("Error accessing templates directory: %s", e) return sorted(templates, key=lambda x: x['name']) @@ -967,14 +968,14 @@ def get_template_content(notes_dir: str, template_name: str) -> Optional[str]: # Security check: ensure template is within notes directory if not validate_path_security(notes_dir, template_path): - print(f"Security: Template path is outside notes directory: {template_path}") + logger.warning("Security: Template path is outside notes directory: %s", template_path) return None try: with open(template_path, 'r', encoding='utf-8') as f: return f.read() except Exception as e: - print(f"Error reading template {template_name}: {e}") + logger.error("Error reading template %s: %s", template_name, e) return None