""" Interface to Management database and AI Connectors. Uses the JSON connector for data access with added language support. """ import os import logging import base64 import hashlib import math from typing import Dict, Any, List, Optional, Union from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.security.rbac import RbacClass from modules.datamodels.datamodelRbac import AccessRuleContext from modules.datamodels.datamodelUam import AccessLevel from modules.datamodels.datamodelFiles import FilePreview, FileItem, FileData from modules.datamodels.datamodelUtils import Prompt from modules.datamodels.datamodelVoice import VoiceSettings from modules.datamodels.datamodelUam import User, Mandate from modules.shared.configuration import APP_CONFIG from modules.shared.timeUtils import getUtcTimestamp from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult logger = logging.getLogger(__name__) # Singleton factory for Management instances with AI service per context _instancesManagement = {} # Custom exceptions for file handling class FileError(Exception): """Base class for file handling exceptions.""" pass class FileNotFoundError(FileError): """Exception raised when a file is not found.""" pass class FileStorageError(FileError): """Exception raised when there's an error storing a file.""" pass class FilePermissionError(FileError): """Exception raised when there's a permission issue with a file.""" pass class FileDeletionError(FileError): """Exception raised when there's an error deleting a file.""" pass class ComponentObjects: """ Interface to Management database and AI Connectors. Uses the JSON connector for data access with added language support. """ def __init__(self): """Initializes the Management Interface.""" # Initialize variables first self.currentUser: Optional[User] = None self.userId: Optional[str] = None self.rbac: Optional[RbacClass] = None # RBAC interface # Initialize database self._initializeDatabase() # Initialize standard records if needed self._initRecords() def setUserContext(self, currentUser: User): """Sets the user context for the interface.""" if not currentUser: logger.info("Initializing interface without user context") return self.currentUser = currentUser # Store User object directly self.userId = currentUser.id if not self.userId: raise ValueError("Invalid user context: id is required") # Add language settings self.userLanguage = currentUser.language # Default user language # Initialize RBAC interface if not self.currentUser: raise ValueError("User context is required for RBAC") # Get DbApp connection for RBAC AccessRule queries from modules.interfaces.interfaceDbAppObjects import getRootInterface dbApp = getRootInterface().db self.rbac = RbacClass(self.db, dbApp=dbApp) # Update database context self.db.updateContext(self.userId) def __del__(self): """Cleanup method to close database connection.""" if hasattr(self, 'db') and self.db is not None: try: self.db.close() except Exception as e: logger.error(f"Error closing database connection: {e}") logger.debug(f"User context set: userId={self.userId}") def _initializeDatabase(self): """Initializes the database connection directly.""" try: # Get configuration values with defaults dbHost = APP_CONFIG.get("DB_MANAGEMENT_HOST", "_no_config_default_data") dbDatabase = APP_CONFIG.get("DB_MANAGEMENT_DATABASE", "management") dbUser = APP_CONFIG.get("DB_MANAGEMENT_USER") dbPassword = APP_CONFIG.get("DB_MANAGEMENT_PASSWORD_SECRET") dbPort = int(APP_CONFIG.get("DB_MANAGEMENT_PORT")) # Create database connector directly self.db = DatabaseConnector( dbHost=dbHost, dbDatabase=dbDatabase, dbUser=dbUser, dbPassword=dbPassword, dbPort=dbPort, userId=self.userId if hasattr(self, 'userId') else None ) # Initialize database system self.db.initDbSystem() logger.info("Database initialized successfully") except Exception as e: logger.error(f"Failed to initialize database: {str(e)}") raise def _initRecords(self): """Initializes standard records in the database if they don't exist.""" try: # Initialize standard prompts self._initializeStandardPrompts() # Add other record initializations here logger.info("Standard records initialized successfully") except Exception as e: logger.error(f"Failed to initialize standard records: {str(e)}") # Don't raise the error, just log it # This allows the interface to be created even if initialization fails def _initializeStandardPrompts(self): """Initializes standard prompts if they don't exist yet.""" try: # Check if any prompts exist existingPrompts = self.db.getRecordset(Prompt) if existingPrompts: logger.info("Prompts already exist, skipping initialization") return # Get the root interface to access the initial mandate ID from modules.interfaces.interfaceDbAppObjects import getRootInterface rootInterface = getRootInterface() # Get initial mandate ID through the root interface mandateId = rootInterface.getInitialId(Mandate) if not mandateId: logger.error("No initial mandate ID found") return # Get root user for initialization rootUser = rootInterface.getUserByUsername("admin") if not rootUser: logger.error("Root user not found for initialization") return # Store current user context if it exists currentUser = self.currentUser # Set user context to root user for initialization self.setUserContext(rootUser) # Define standard prompts standardPrompts = [ Prompt( name="Market Research", content="Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.", mandateId=mandateId ), Prompt( name="Data Analysis", content="Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.", mandateId=mandateId ), Prompt( name="Meeting Protocol", content="Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.", mandateId=mandateId ), Prompt( name="UI/UX Design", content="Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.", mandateId=mandateId ), Prompt( name="Primzahlen", content="Gib mir die ersten 1000 Primzahlen.", mandateId=mandateId ), Prompt( name="E-Mail", content="Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.", mandateId=mandateId ) ] # Create prompts for prompt in standardPrompts: self.db.recordCreate(Prompt, prompt) logger.info(f"Created standard prompt: {prompt.name}") # Restore original user context if it existed if currentUser: self.setUserContext(currentUser) else: self.currentUser = None self.userId = None self.db.updateContext("") # Reset database context except Exception as e: logger.error(f"Error initializing standard prompts: {str(e)}") # Ensure we restore user context even if there's an error if 'currentUser' in locals() and currentUser: self.setUserContext(currentUser) else: self.currentUser = None self.userId = None self.db.updateContext("") # Reset database context def checkRbacPermission( self, modelClass: type, operation: str, recordId: Optional[str] = None ) -> bool: """ Check RBAC permission for a specific operation on a table. Args: modelClass: Pydantic model class for the table operation: Operation to check ('create', 'update', 'delete', 'read') recordId: Optional record ID for specific record check Returns: Boolean indicating permission """ if not self.rbac or not self.currentUser: return False tableName = modelClass.__name__ permissions = self.rbac.getUserPermissions( self.currentUser, AccessRuleContext.DATA, tableName ) if operation == "create": return permissions.create != AccessLevel.NONE elif operation == "update": return permissions.update != AccessLevel.NONE elif operation == "delete": return permissions.delete != AccessLevel.NONE elif operation == "read": return permissions.read != AccessLevel.NONE else: return False def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]: """ Apply filter criteria to records. Supports: - General search: {"search": "text"} - searches across all text fields - Field-specific filters: - Simple: {"status": "running"} - equals match - With operator: {"status": {"operator": "equals", "value": "running"}} - Operators: equals, contains, gt, gte, lt, lte, in, notIn, startsWith, endsWith Args: records: List of record dictionaries to filter filters: Filter criteria dictionary Returns: Filtered list of records """ if not filters or not records: return records filtered = [] for record in records: matches = True # Handle general search across text fields if "search" in filters: search_term = str(filters["search"]).lower() if search_term: # Search in all string fields found = False for key, value in record.items(): if isinstance(value, str) and search_term in value.lower(): found = True break elif isinstance(value, (int, float)) and search_term in str(value): found = True break if not found: matches = False # Handle field-specific filters for field_name, filter_value in filters.items(): if field_name == "search": continue # Already handled above if field_name not in record: matches = False break record_value = record.get(field_name) # Handle simple value (equals operator) if not isinstance(filter_value, dict): if record_value != filter_value: matches = False break continue # Handle filter with operator operator = filter_value.get("operator", "equals") filter_val = filter_value.get("value") if operator in ["equals", "eq"]: if record_value != filter_val: matches = False break elif operator == "contains": record_str = str(record_value).lower() if record_value is not None else "" filter_str = str(filter_val).lower() if filter_val is not None else "" if filter_str not in record_str: matches = False break elif operator == "startsWith": record_str = str(record_value).lower() if record_value is not None else "" filter_str = str(filter_val).lower() if filter_val is not None else "" if not record_str.startswith(filter_str): matches = False break elif operator == "endsWith": record_str = str(record_value).lower() if record_value is not None else "" filter_str = str(filter_val).lower() if filter_val is not None else "" if not record_str.endswith(filter_str): matches = False break elif operator == "gt": try: record_num = float(record_value) if record_value is not None else float('-inf') filter_num = float(filter_val) if filter_val is not None else float('-inf') if record_num <= filter_num: matches = False break except (ValueError, TypeError): matches = False break elif operator == "gte": try: record_num = float(record_value) if record_value is not None else float('-inf') filter_num = float(filter_val) if filter_val is not None else float('-inf') if record_num < filter_num: matches = False break except (ValueError, TypeError): matches = False break elif operator == "lt": try: record_num = float(record_value) if record_value is not None else float('inf') filter_num = float(filter_val) if filter_val is not None else float('inf') if record_num >= filter_num: matches = False break except (ValueError, TypeError): matches = False break elif operator == "lte": try: record_num = float(record_value) if record_value is not None else float('inf') filter_num = float(filter_val) if filter_val is not None else float('inf') if record_num > filter_num: matches = False break except (ValueError, TypeError): matches = False break elif operator == "in": if not isinstance(filter_val, list): filter_val = [filter_val] if record_value not in filter_val: matches = False break elif operator == "notIn": if not isinstance(filter_val, list): filter_val = [filter_val] if record_value in filter_val: matches = False break else: # Unknown operator - default to equals if record_value != filter_val: matches = False break if matches: filtered.append(record) return filtered def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]: """Apply multi-level sorting to records using stable sort (sorts from least to most significant field).""" if not sortFields: return records # Start with a copy to avoid modifying original sortedRecords = list(records) # Sort from least significant to most significant field (reverse order) # Python's sort is stable, so this creates proper multi-level sorting for sortField in reversed(sortFields): # Handle both dict and object formats if isinstance(sortField, dict): fieldName = sortField.get("field") direction = sortField.get("direction", "asc") else: fieldName = getattr(sortField, "field", None) direction = getattr(sortField, "direction", "asc") if not fieldName: continue isDesc = (direction == "desc") def sortKey(record): value = record.get(fieldName) # Handle None values - place them at the end for both directions if value is None: # Use a special value that sorts last return (1, "") # (is_none_flag, empty_value) - sorts after (0, ...) else: # Return tuple with type indicator for proper comparison if isinstance(value, (int, float)): return (0, value) elif isinstance(value, str): return (0, value) elif isinstance(value, bool): return (0, value) else: return (0, str(value)) # Sort with reverse parameter for descending sortedRecords.sort(key=sortKey, reverse=isDesc) return sortedRecords # Utilities def getInitialId(self, model_class: type) -> Optional[str]: """Returns the initial ID for a table.""" return self.db.getInitialId(model_class) # Prompt methods def getAllPrompts(self, pagination: Optional[PaginationParams] = None) -> Union[List[Prompt], PaginatedResult]: """ Returns prompts based on user access level. Supports optional pagination, sorting, and filtering. Args: pagination: Optional pagination parameters. If None, returns all items. Returns: If pagination is None: List[Prompt] If pagination is provided: PaginatedResult with items and metadata """ try: # Use RBAC filtering filteredPrompts = self.db.getRecordsetWithRBAC( Prompt, self.currentUser ) # If no pagination requested, return all items if pagination is None: return [Prompt(**prompt) for prompt in filteredPrompts] # Apply filtering (if filters provided) if pagination.filters: filteredPrompts = self._applyFilters(filteredPrompts, pagination.filters) # Apply sorting (in order of sortFields) if pagination.sort: filteredPrompts = self._applySorting(filteredPrompts, pagination.sort) # Count total items after filters totalItems = len(filteredPrompts) totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0 # Apply pagination (skip/limit) startIdx = (pagination.page - 1) * pagination.pageSize endIdx = startIdx + pagination.pageSize pagedPrompts = filteredPrompts[startIdx:endIdx] # Convert to model objects items = [Prompt(**prompt) for prompt in pagedPrompts] return PaginatedResult( items=items, totalItems=totalItems, totalPages=totalPages ) except Exception as e: logger.error(f"Error getting prompts: {str(e)}") if pagination is None: return [] return PaginatedResult(items=[], totalItems=0, totalPages=0) def getPrompt(self, promptId: str) -> Optional[Prompt]: """Returns a prompt by ID if user has access.""" # Use RBAC filtering filteredPrompts = self.db.getRecordsetWithRBAC( Prompt, self.currentUser, recordFilter={"id": promptId} ) return Prompt(**filteredPrompts[0]) if filteredPrompts else None def createPrompt(self, promptData: Dict[str, Any]) -> Dict[str, Any]: """Creates a new prompt if user has permission.""" if not self.checkRbacPermission(Prompt, "create"): raise PermissionError("No permission to create prompts") # Create prompt record createdRecord = self.db.recordCreate(Prompt, promptData) if not createdRecord or not createdRecord.get("id"): raise ValueError("Failed to create prompt record") return createdRecord def updatePrompt(self, promptId: str, updateData: Dict[str, Any]) -> Dict[str, Any]: """Updates a prompt if user has access.""" try: # Get prompt prompt = self.getPrompt(promptId) if not prompt: raise ValueError(f"Prompt {promptId} not found") # Update prompt record directly with the update data self.db.recordModify(Prompt, promptId, updateData) # Clear cache to ensure fresh data # Get updated prompt updatedPrompt = self.getPrompt(promptId) if not updatedPrompt: raise ValueError("Failed to retrieve updated prompt") return updatedPrompt.model_dump() except Exception as e: logger.error(f"Error updating prompt: {str(e)}") raise ValueError(f"Failed to update prompt: {str(e)}") def deletePrompt(self, promptId: str) -> bool: """Deletes a prompt if user has access.""" # Check if the prompt exists and user has access prompt = self.getPrompt(promptId) if not prompt: return False if not self.checkRbacPermission(Prompt, "update", promptId): raise PermissionError(f"No permission to delete prompt {promptId}") # Delete prompt success = self.db.recordDelete(Prompt, promptId) return success # File Utilities def checkForDuplicateFile(self, fileHash: str, fileName: str = None) -> Optional[FileItem]: """Checks if a file with the same hash already exists for the current user and mandate. If fileName is provided, also checks for exact name+hash match. Only returns files the current user has access to.""" # Get files with the hash, filtered by RBAC accessibleFiles = self.db.getRecordsetWithRBAC( FileItem, self.currentUser, recordFilter={"fileHash": fileHash} ) if not accessibleFiles: return None # If fileName is provided, check for exact name+hash match first if fileName: for file in accessibleFiles: # Skip files without fileName key or with None/empty fileName if "fileName" not in file or not file["fileName"]: continue if file["fileName"] == fileName: return FileItem( id=file["id"], mandateId=file["mandateId"], fileName=file["fileName"], mimeType=file["mimeType"], fileHash=file["fileHash"], fileSize=file["fileSize"], creationDate=file["creationDate"] ) # Return first valid file with matching hash (for general duplicate detection) for file in accessibleFiles: # Skip files without fileName key or with None/empty fileName if "fileName" not in file or not file["fileName"]: continue # Use first valid file return FileItem( id=file["id"], mandateId=file["mandateId"], fileName=file["fileName"], mimeType=file["mimeType"], fileHash=file["fileHash"], fileSize=file["fileSize"], creationDate=file["creationDate"] ) # If no valid files found, return None return None def getMimeType(self, fileName: str) -> str: """Determines the MIME type based on the file extension.""" ext = os.path.splitext(fileName)[1].lower()[1:] extensionToMime = { "pdf": "application/pdf", "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "doc": "application/msword", "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xls": "application/vnd.ms-excel", "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "ppt": "application/vnd.ms-powerpoint", "csv": "text/csv", "txt": "text/plain", "json": "application/json", "xml": "application/xml", "html": "text/html", "htm": "text/html", "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "webp": "image/webp", "svg": "image/svg+xml", "py": "text/x-python", "js": "application/javascript", "css": "text/css" } return extensionToMime.get(ext.lower(), "application/octet-stream") def isTextMimeType(self, mimeType: str) -> bool: """Determines if a MIME type represents a text-based format.""" textMimeTypes = { 'text/plain', 'text/html', 'text/css', 'text/javascript', 'text/x-python', 'text/csv', 'text/xml', 'application/json', 'application/xml', 'application/javascript', 'application/x-python', 'application/x-httpd-php', 'application/x-sh', 'application/x-shellscript', 'application/x-yaml', 'application/x-toml', 'application/x-markdown', 'application/x-latex', 'application/x-tex', 'application/x-rst', 'application/x-asciidoc', 'application/x-markdown', 'application/x-httpd-php', 'application/x-httpd-php-source', 'application/x-httpd-php3', 'application/x-httpd-php4', 'application/x-httpd-php5', 'application/x-httpd-php7', 'application/x-httpd-php8', 'application/x-httpd-php-source', 'application/x-httpd-php3-source', 'application/x-httpd-php4-source', 'application/x-httpd-php5-source', 'application/x-httpd-php7-source', 'application/x-httpd-php8-source' } return mimeType.lower() in textMimeTypes # File methods - metadata-based operations def getAllFiles(self, pagination: Optional[PaginationParams] = None) -> Union[List[FileItem], PaginatedResult]: """ Returns files based on user access level. Supports optional pagination, sorting, and filtering. Args: pagination: Optional pagination parameters. If None, returns all items. Returns: If pagination is None: List[FileItem] If pagination is provided: PaginatedResult with items and metadata """ # Use RBAC filtering filteredFiles = self.db.getRecordsetWithRBAC( FileItem, self.currentUser ) # Convert database records to FileItem instances (for both paginated and non-paginated) def convertFileItems(files): fileItems = [] for file in files: try: # Ensure proper values, use defaults for invalid data creationDate = file.get("creationDate") if creationDate is None or not isinstance(creationDate, (int, float)) or creationDate <= 0: creationDate = getUtcTimestamp() fileName = file.get("fileName") if not fileName or fileName == "None": continue # Skip records with invalid fileName fileItem = FileItem( id=file.get("id"), mandateId=file.get("mandateId"), fileName=fileName, mimeType=file.get("mimeType"), fileHash=file.get("fileHash"), fileSize=file.get("fileSize"), creationDate=creationDate ) fileItems.append(fileItem) except Exception as e: logger.warning(f"Skipping invalid file record: {str(e)}") continue return fileItems # If no pagination requested, return all items if pagination is None: return convertFileItems(filteredFiles) # Apply filtering (if filters provided) if pagination.filters: filteredFiles = self._applyFilters(filteredFiles, pagination.filters) # Apply sorting (in order of sortFields) if pagination.sort: filteredFiles = self._applySorting(filteredFiles, pagination.sort) # Count total items after filters totalItems = len(filteredFiles) totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0 # Apply pagination (skip/limit) startIdx = (pagination.page - 1) * pagination.pageSize endIdx = startIdx + pagination.pageSize pagedFiles = filteredFiles[startIdx:endIdx] # Convert to model objects items = convertFileItems(pagedFiles) return PaginatedResult( items=items, totalItems=totalItems, totalPages=totalPages ) def getFile(self, fileId: str) -> Optional[FileItem]: """Returns a file by ID if user has access.""" # Use RBAC filtering filteredFiles = self.db.getRecordsetWithRBAC( FileItem, self.currentUser, recordFilter={"id": fileId} ) if not filteredFiles: return None file = filteredFiles[0] try: # Get creation date from record or use current time creationDate = file.get("creationDate") if not creationDate: creationDate = getUtcTimestamp() return FileItem( id=file.get("id"), mandateId=file.get("mandateId"), fileName=file.get("fileName"), mimeType=file.get("mimeType"), workflowId=file.get("workflowId"), fileHash=file.get("fileHash"), fileSize=file.get("fileSize"), creationDate=creationDate ) except Exception as e: logger.error(f"Error converting file record: {str(e)}") return None def _isfileNameUnique(self, fileName: str, excludeFileId: Optional[str] = None) -> bool: """Checks if a fileName is unique for the current user.""" # Get all files filtered by RBAC (will be filtered by user's access level) files = self.db.getRecordsetWithRBAC( FileItem, self.currentUser ) # Check if fileName exists (excluding the current file if updating) for file in files: # Skip files without fileName key or with None/empty fileName if "fileName" not in file or not file["fileName"]: continue if file["fileName"] == fileName and (excludeFileId is None or file["id"] != excludeFileId): return False return True def _generateUniquefileName(self, fileName: str, excludeFileId: Optional[str] = None) -> str: """Generates a unique fileName by adding a number if necessary.""" if self._isfileNameUnique(fileName, excludeFileId): return fileName # Split fileName into name and extension name, ext = os.path.splitext(fileName) counter = 1 # Try fileNames with increasing numbers until we find a unique one while True: newfileName = f"{name}_{counter}{ext}" if self._isfileNameUnique(newfileName, excludeFileId): return newfileName counter += 1 def createFile(self, name: str, mimeType: str, content: bytes) -> FileItem: """Creates a new file entry if user has permission. Computes fileHash and fileSize from content.""" if not self.checkRbacPermission(FileItem, "create"): raise PermissionError("No permission to create files") # Ensure fileName is unique uniqueName = self._generateUniquefileName(name) # Compute file size and hash fileSize = len(content) fileHash = hashlib.sha256(content).hexdigest() # Ensure mandateId is valid mandateId = self.currentUser.mandateId or "default" # Create FileItem instance fileItem = FileItem( mandateId=mandateId, fileName=uniqueName, mimeType=mimeType, fileSize=fileSize, fileHash=fileHash ) # Store in database self.db.recordCreate(FileItem, fileItem) return fileItem def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]: """Updates file metadata if user has access.""" # Check if the file exists and user has access file = self.getFile(fileId) if not file: raise FileNotFoundError(f"File with ID {fileId} not found") if not self.checkRbacPermission(FileItem, "update", fileId): raise PermissionError(f"No permission to update file {fileId}") # If fileName is being updated, ensure it's unique if "fileName" in updateData: updateData["fileName"] = self._generateUniquefileName(updateData["fileName"], fileId) # Update file success = self.db.recordModify(FileItem, fileId, updateData) return success def deleteFile(self, fileId: str) -> bool: """Deletes a file if user has access.""" try: # Check if the file exists and user has access file = self.getFile(fileId) if not file: raise FileNotFoundError(f"File with ID {fileId} not found") if not self.checkRbacPermission(FileItem, "update", fileId): raise PermissionError(f"No permission to delete file {fileId}") # Check for other references to this file (by hash) - use RBAC to only check files user has access to fileHash = file.fileHash if fileHash: allReferences = self.db.getRecordsetWithRBAC( FileItem, self.currentUser, recordFilter={"fileHash": fileHash} ) otherReferences = [f for f in allReferences if f["id"] != fileId] # Only delete associated fileData if no other references exist if not otherReferences: try: fileDataEntries = self.db.getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId}) if fileDataEntries: self.db.recordDelete(FileData, fileId) logger.debug(f"FileData for file {fileId} deleted") except Exception as e: logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}") # Delete the FileItem entry success = self.db.recordDelete(FileItem, fileId) # Clear cache to ensure fresh data return success except FileNotFoundError as e: raise except FilePermissionError as e: raise except Exception as e: logger.error(f"Error deleting file {fileId}: {str(e)}") raise FileDeletionError(f"Error deleting file: {str(e)}") # FileData methods - data operations def createFileData(self, fileId: str, data: bytes) -> bool: """Stores the binary data of a file in the database.""" try: # Check file access file = self.getFile(fileId) if not file: logger.error(f"File with ID {fileId} not found when storing data") return False # Determine if this is a text-based format mimeType = file.mimeType isTextFormat = self.isTextMimeType(mimeType) base64Encoded = False fileData = None if isTextFormat: # Try to decode as text try: textContent = data.decode('utf-8') fileData = textContent base64Encoded = False logger.debug(f"Stored file {fileId} as text") except UnicodeDecodeError: # Fallback to base64 if text decoding fails encodedData = base64.b64encode(data).decode('utf-8') fileData = encodedData base64Encoded = True logger.warning(f"Failed to decode text file {fileId}, falling back to base64") else: # Binary format - always use base64 encodedData = base64.b64encode(data).decode('utf-8') fileData = encodedData base64Encoded = True logger.debug(f"Stored file {fileId} as base64") # Create the fileData record with data and encoding flag fileDataObj = { "id": fileId, "data": fileData, "base64Encoded": base64Encoded } self.db.recordCreate(FileData, fileDataObj) # Clear cache to ensure fresh data logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})") return True except Exception as e: logger.error(f"Error storing data for file {fileId}: {str(e)}") return False def getFileData(self, fileId: str) -> Optional[bytes]: """Returns the binary data of a file if user has access.""" # Check file access file = self.getFile(fileId) if not file: logger.warning(f"No access to file ID {fileId}") return None fileDataEntries = self.db.getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId}) if not fileDataEntries: logger.warning(f"No data found for file ID {fileId}") return None fileDataEntry = fileDataEntries[0] if "data" not in fileDataEntry: logger.warning(f"No data field in file data for ID {fileId}") return None data = fileDataEntry["data"] base64Encoded = fileDataEntry.get("base64Encoded", False) try: if base64Encoded: # Decode base64 to bytes return base64.b64decode(data) else: # Check if this is supposed to be a binary file based on mime type mimeType = file.mimeType isTextFormat = self.isTextMimeType(mimeType) if isTextFormat: # This is a text file, encode to bytes as expected return data.encode('utf-8') else: # This is a binary file that was incorrectly stored as text # Try to decode it as if it was base64 (common fallback scenario) try: logger.warning(f"Binary file {fileId} ({mimeType}) was stored as text, attempting base64 decode") return base64.b64decode(data) except Exception as base64_error: logger.error(f"Failed to decode binary file {fileId} as base64: {str(base64_error)}") # Last resort: return the data as-is (might be corrupted) logger.warning(f"Returning raw data for file {fileId} - file may be corrupted") return data.encode('utf-8') if isinstance(data, str) else data except Exception as e: logger.error(f"Error processing file data for {fileId}: {str(e)}") return None def getFileContent(self, fileId: str) -> Optional[FilePreview]: """Returns the full file content if user has access.""" try: # Get file metadata file = self.getFile(fileId) if not file: logger.warning(f"No access to file ID {fileId}") return None # Get file content fileContent = self.getFileData(fileId) if not fileContent: logger.warning(f"No content found for file ID {fileId}") return None # Process content based on file type isText = False content = "" encoding = None # Use proper attribute access for FileItem object if file.mimeType.startswith("text/"): # For text files, return full content try: content = fileContent.decode('utf-8') isText = True encoding = 'utf-8' except UnicodeDecodeError: content = fileContent.decode('latin-1') isText = True encoding = 'latin-1' elif file.mimeType.startswith("image/"): # For images, return base64 content = base64.b64encode(fileContent).decode('utf-8') isText = False else: # For other files, return as base64 content = base64.b64encode(fileContent).decode('utf-8') isText = False return FilePreview( content=content, mimeType=file.mimeType, fileName=file.fileName, isText=isText, encoding=encoding, size=file.fileSize ) except Exception as e: logger.error(f"Error getting file content: {str(e)}") return None def saveUploadedFile(self, fileContent: bytes, fileName: str) -> tuple[FileItem, str]: """Saves an uploaded file if user has permission.""" try: # Check file creation permission if not self.checkRbacPermission(FileItem, "create"): raise PermissionError("No permission to upload files") logger.debug(f"Starting upload process for file: {fileName}") if not isinstance(fileContent, bytes): logger.error(f"Invalid fileContent type: {type(fileContent)}") raise ValueError(f"fileContent must be bytes, got {type(fileContent)}") # Compute file hash first to check for duplicates fileHash = hashlib.sha256(fileContent).hexdigest() # Check for exact name+hash match first (same name + same content) existingFile = self.checkForDuplicateFile(fileHash, fileName) if existingFile: logger.info(f"Exact duplicate detected: {fileName} with same hash. Returning existing file reference.") return existingFile, "exact_duplicate" # Check for hash-only match (same content, different name) existingFileWithSameHash = self.checkForDuplicateFile(fileHash) if existingFileWithSameHash: logger.info(f"Content duplicate detected: {fileName} has same content as {existingFileWithSameHash.fileName}") # Continue with upload - filename will be made unique if needed # Determine MIME type mimeType = self.getMimeType(fileName) # Save metadata and file (hash/size computed inside createFile) logger.debug(f"Saving file metadata to database for file: {fileName}") fileItem = self.createFile( name=fileName, mimeType=mimeType, content=fileContent ) # Save binary data logger.debug(f"Saving file content to database for file: {fileName}") self.createFileData(fileItem.id, fileContent) logger.debug(f"File upload process completed for: {fileName}") # Check if filename was modified (indicating name conflict) if fileItem.fileName != fileName: return fileItem, "name_conflict" else: return fileItem, "new_file" except Exception as e: logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True) raise FileStorageError(f"Error saving file: {str(e)}") # VoiceSettings methods def getVoiceSettings(self, userId: Optional[str] = None) -> Optional[VoiceSettings]: """Returns voice settings for a user if user has access.""" try: targetUserId = userId or self.userId if not targetUserId: logger.error("No user ID provided for voice settings") return None # Get voice settings for the user, filtered by RBAC filteredSettings = self.db.getRecordsetWithRBAC( VoiceSettings, self.currentUser, recordFilter={"userId": targetUserId} ) if not filteredSettings: logger.warning(f"No access to voice settings for user {targetUserId}") return None # Ensure timestamps are set for validation settings_data = filteredSettings[0] if not settings_data.get("creationDate"): settings_data["creationDate"] = getUtcTimestamp() if not settings_data.get("lastModified"): settings_data["lastModified"] = getUtcTimestamp() return VoiceSettings(**settings_data) except Exception as e: logger.error(f"Error getting voice settings: {str(e)}") return None def createVoiceSettings(self, settingsData: Dict[str, Any]) -> Dict[str, Any]: """Creates voice settings for a user if user has permission.""" try: if not self.checkRbacPermission(VoiceSettings, "update"): raise PermissionError("No permission to create voice settings") # Ensure userId is set if "userId" not in settingsData: settingsData["userId"] = self.userId # Ensure mandateId is set if "mandateId" not in settingsData: settingsData["mandateId"] = self.currentUser.mandateId if self.currentUser else "default" # Check if settings already exist for this user existingSettings = self.getVoiceSettings(settingsData["userId"]) if existingSettings: raise ValueError(f"Voice settings already exist for user {settingsData['userId']}") # Create voice settings record createdRecord = self.db.recordCreate(VoiceSettings, settingsData) if not createdRecord or not createdRecord.get("id"): raise ValueError("Failed to create voice settings record") logger.info(f"Created voice settings for user {settingsData['userId']}") return createdRecord except Exception as e: logger.error(f"Error creating voice settings: {str(e)}") raise def updateVoiceSettings(self, userId: str, updateData: Dict[str, Any]) -> Dict[str, Any]: """Updates voice settings for a user if user has access.""" try: # Get existing settings existingSettings = self.getVoiceSettings(userId) if not existingSettings: raise ValueError(f"Voice settings not found for user {userId}") # Update lastModified timestamp updateData["lastModified"] = getUtcTimestamp() # Update voice settings record success = self.db.recordModify(VoiceSettings, existingSettings.id, updateData) if not success: raise ValueError("Failed to update voice settings record") # Get updated settings updatedSettings = self.getVoiceSettings(userId) if not updatedSettings: raise ValueError("Failed to retrieve updated voice settings") logger.info(f"Updated voice settings for user {userId}") return updatedSettings.model_dump() except Exception as e: logger.error(f"Error updating voice settings: {str(e)}") raise def deleteVoiceSettings(self, userId: str) -> bool: """Deletes voice settings for a user if user has access.""" try: # Get existing settings existingSettings = self.getVoiceSettings(userId) if not existingSettings: logger.warning(f"Voice settings not found for user {userId}") return False # Delete voice settings success = self.db.recordDelete(VoiceSettings, existingSettings.id) if success: logger.info(f"Deleted voice settings for user {userId}") else: logger.error(f"Failed to delete voice settings for user {userId}") return success except Exception as e: logger.error(f"Error deleting voice settings: {str(e)}") return False def getOrCreateVoiceSettings(self, userId: Optional[str] = None) -> VoiceSettings: """Gets existing voice settings or creates default ones for a user.""" try: targetUserId = userId or self.userId if not targetUserId: raise ValueError("No user ID provided for voice settings") # Try to get existing settings existingSettings = self.getVoiceSettings(targetUserId) if existingSettings: return existingSettings # Create default settings defaultSettings = { "userId": targetUserId, "mandateId": self.currentUser.mandateId if self.currentUser else "default", "sttLanguage": "de-DE", "ttsLanguage": "de-DE", "ttsVoice": "de-DE-KatjaNeural", "translationEnabled": True, "targetLanguage": "en-US" } createdRecord = self.createVoiceSettings(defaultSettings) return VoiceSettings(**createdRecord) except Exception as e: logger.error(f"Error getting or creating voice settings: {str(e)}") raise def getInterface(currentUser: Optional[User] = None) -> 'ComponentObjects': """ Returns a ComponentObjects instance. If currentUser is provided, initializes with user context. Otherwise, returns an instance with only database access. """ # Create new instance if not exists if "default" not in _instancesManagement: _instancesManagement["default"] = ComponentObjects() interface = _instancesManagement["default"] if currentUser: interface.setUserContext(currentUser) else: logger.info("Returning interface without user context") return interface