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

1"""Shared utility functions for the Research Library.""" 

2 

3import hashlib 

4import os 

5import subprocess 

6import sys 

7from pathlib import Path 

8 

9from flask import jsonify 

10from loguru import logger 

11 

12from ...config.paths import get_library_directory 

13from ...security.path_validator import PathValidator 

14 

15 

16def get_url_hash(url: str) -> str: 

17 """ 

18 Generate a SHA256 hash of a URL. 

19 

20 Args: 

21 url: The URL to hash 

22 

23 Returns: 

24 The SHA256 hash of the URL 

25 """ 

26 return hashlib.sha256(url.lower().encode()).hexdigest() 

27 

28 

29def get_library_storage_path(username: str) -> Path: 

30 """ 

31 Get the storage path for a user's library. 

32 

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 

36 

37 Args: 

38 username: The username 

39 

40 Returns: 

41 Path to the library storage directory 

42 """ 

43 from ...utilities.db_utils import get_settings_manager 

44 

45 settings = get_settings_manager() 

46 

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() 

54 

55 # Check if shared library mode is enabled 

56 shared_library = settings.get_setting( 

57 "research_library.shared_library", False 

58 ) 

59 

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 

69 

70 

71def open_file_location(file_path: str) -> bool: 

72 """ 

73 Open the file location in the system file manager. 

74 

75 Args: 

76 file_path: Path to the file 

77 

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 

108 

109 

110def get_relative_library_path(absolute_path: str, username: str) -> str: 

111 """ 

112 Get the relative path from the library root. 

113 

114 Args: 

115 absolute_path: The absolute file path 

116 username: The username 

117 

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 

127 

128 

129def get_absolute_library_path(relative_path: str, username: str) -> Path: 

130 """ 

131 Get the absolute path from a relative library path. 

132 

133 Args: 

134 relative_path: The relative path from library root 

135 username: The username 

136 

137 Returns: 

138 The absolute path 

139 """ 

140 library_root = get_library_storage_path(username) 

141 return library_root / relative_path 

142 

143 

144def get_absolute_path_from_settings(relative_path: str) -> Path: 

145 """ 

146 Get absolute path using settings manager for library root. 

147 

148 Args: 

149 relative_path: The relative path from library root 

150 

151 Returns: 

152 The absolute path 

153 """ 

154 from ...utilities.db_utils import get_settings_manager 

155 

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() 

163 

164 if not relative_path: 

165 return library_root 

166 

167 return library_root / relative_path 

168 

169 

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. 

173 

174 This prevents information exposure by logging full error details internally 

175 while returning a generic message to the user. 

176 

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) 

181 

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}") 

187 

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