Coverage for src / local_deep_research / research_library / utils / __init__.py: 46%
60 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1"""Shared utility functions for the Research Library."""
3import hashlib
4import os
5import subprocess
6import sys
7from pathlib import Path
9from flask import jsonify
10from loguru import logger
12from ...config.paths import get_library_directory
13from ...security.path_validator import PathValidator
16def get_url_hash(url: str) -> str:
17 """
18 Generate a SHA256 hash of a URL.
20 Args:
21 url: The URL to hash
23 Returns:
24 The SHA256 hash of the URL
25 """
26 return hashlib.sha256(url.lower().encode()).hexdigest()
29def get_library_storage_path(username: str) -> Path:
30 """
31 Get the storage path for a user's library.
33 Uses the settings system which respects environment variable overrides:
34 - research_library.storage_path: Base path for library storage
35 - research_library.shared_library: If true, all users share the same directory
37 Args:
38 username: The username
40 Returns:
41 Path to the library storage directory
42 """
43 from ...utilities.db_utils import get_settings_manager
45 settings = get_settings_manager()
47 # Get the base path from settings (uses centralized path, respects LDR_DATA_DIR)
48 base_path = Path(
49 settings.get_setting(
50 "research_library.storage_path",
51 str(get_library_directory()),
52 )
53 ).expanduser()
55 # Check if shared library mode is enabled
56 shared_library = settings.get_setting(
57 "research_library.shared_library", False
58 )
60 if shared_library:
61 # Shared mode: all users use the same directory
62 base_path.mkdir(parents=True, exist_ok=True)
63 return base_path
64 else:
65 # Default: user isolation with subdirectories
66 user_dir = base_path / username
67 user_dir.mkdir(parents=True, exist_ok=True)
68 return user_dir
71def open_file_location(file_path: str) -> bool:
72 """
73 Open the file location in the system file manager.
75 Args:
76 file_path: Path to the file
78 Returns:
79 True if successful, False otherwise
80 """
81 try:
82 # Validate path is safe (blocks system dirs, path traversal)
83 validated = PathValidator.validate_local_filesystem_path(file_path)
84 folder = str(validated.parent)
85 if sys.platform == "win32": 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 os.startfile(folder)
87 elif sys.platform == "darwin": # macOS 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 result = subprocess.run(
89 ["open", folder], capture_output=True, text=True, shell=False
90 )
91 if result.returncode != 0:
92 logger.error(f"Failed to open folder on macOS: {result.stderr}")
93 return False
94 else: # Linux
95 result = subprocess.run(
96 ["xdg-open", folder],
97 capture_output=True,
98 text=True,
99 shell=False,
100 )
101 if result.returncode != 0: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 logger.error(f"Failed to open folder on Linux: {result.stderr}")
103 return False
104 return True
105 except Exception:
106 logger.exception("Failed to open file location")
107 return False
110def get_relative_library_path(absolute_path: str, username: str) -> str:
111 """
112 Get the relative path from the library root.
114 Args:
115 absolute_path: The absolute file path
116 username: The username
118 Returns:
119 The relative path from the library root
120 """
121 try:
122 library_root = get_library_storage_path(username)
123 return str(Path(absolute_path).relative_to(library_root))
124 except ValueError:
125 # Path is not relative to library root
126 return Path(absolute_path).name
129def get_absolute_library_path(relative_path: str, username: str) -> Path:
130 """
131 Get the absolute path from a relative library path.
133 Args:
134 relative_path: The relative path from library root
135 username: The username
137 Returns:
138 The absolute path
139 """
140 library_root = get_library_storage_path(username)
141 return library_root / relative_path
144def get_absolute_path_from_settings(relative_path: str) -> Path:
145 """
146 Get absolute path using settings manager for library root.
148 Args:
149 relative_path: The relative path from library root
151 Returns:
152 The absolute path
153 """
154 from ...utilities.db_utils import get_settings_manager
156 settings = get_settings_manager()
157 library_root = Path(
158 settings.get_setting(
159 "research_library.storage_path",
160 str(get_library_directory()),
161 )
162 ).expanduser()
164 if not relative_path:
165 return library_root
167 return library_root / relative_path
170def handle_api_error(operation: str, error: Exception, status_code: int = 500):
171 """
172 Handle API errors consistently - log internally, return generic message to user.
174 This prevents information exposure by logging full error details internally
175 while returning a generic message to the user.
177 Args:
178 operation: Description of the operation that failed (for logging)
179 error: The exception that occurred
180 status_code: HTTP status code to return (default: 500)
182 Returns:
183 Flask JSON response tuple (response, status_code)
184 """
185 # Log the full error internally with stack trace
186 logger.exception(f"Error during {operation}")
188 # Return generic message to user (no internal details exposed)
189 return jsonify(
190 {
191 "success": False,
192 "error": "An internal error occurred. Please try again or contact support.",
193 }
194 ), status_code