From d9f437f63e9b86095bf9c346446ba5bcfc69955c Mon Sep 17 00:00:00 2001 From: Ida Date: Fri, 17 Apr 2026 14:09:33 +0200 Subject: [PATCH] bugfix(FIL-01 + files verschwunden nach hochladen und reload --- modules/interfaces/interfaceDbManagement.py | 47 +++++++++++++-------- modules/interfaces/interfaceRbac.py | 14 ++++++ modules/routes/routeDataFiles.py | 41 +++++++++++++++--- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/modules/interfaces/interfaceDbManagement.py b/modules/interfaces/interfaceDbManagement.py index 9589f7d6..3b93b780 100644 --- a/modules/interfaces/interfaceDbManagement.py +++ b/modules/interfaces/interfaceDbManagement.py @@ -1087,29 +1087,32 @@ class ComponentObjects: return newfileName counter += 1 - def createFile(self, name: str, mimeType: str, content: bytes) -> FileItem: + def createFile(self, name: str, mimeType: str, content: bytes, folderId: Optional[str] = None) -> FileItem: """Creates a new file entry if user has permission. Computes fileHash and fileSize from content. - + Duplicate check: if a file with the same user + fileHash + fileName already exists, the existing file is returned instead of creating a new one. Same hash with different name is allowed (intentional copy by user). + + Args: + folderId: Optional parent folder ID. None/empty means the root folder. """ if not self.checkRbacPermission(FileItem, "create"): raise PermissionError("No permission to create files") - + # Compute file size and hash fileSize = len(content) fileHash = hashlib.sha256(content).hexdigest() - + # Duplicate check: same user + same hash + same fileName → return existing existingFile = self.checkForDuplicateFile(fileHash, name) if existingFile: logger.info(f"Duplicate file detected in createFile: '{name}' (hash={fileHash[:12]}...) for user {self.userId} — returning existing file {existingFile.id}") return existingFile - + # Ensure fileName is unique uniqueName = self._generateUniquefileName(name) - + mandateId = self.mandateId or "" featureInstanceId = self.featureInstanceId or "" @@ -1120,6 +1123,11 @@ class ComponentObjects: else: scope = "personal" + # Normalize folderId: treat empty string as "no folder" (= root) – NULL in DB + normalizedFolderId: Optional[str] = folderId + if isinstance(normalizedFolderId, str) and not normalizedFolderId.strip(): + normalizedFolderId = None + fileItem = FileItem( mandateId=mandateId, featureInstanceId=featureInstanceId, @@ -1128,7 +1136,7 @@ class ComponentObjects: mimeType=mimeType, fileSize=fileSize, fileHash=fileHash, - folderId="", + folderId=normalizedFolderId, ) # Store in database @@ -1842,39 +1850,44 @@ class ComponentObjects: 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.""" + def saveUploadedFile(self, fileContent: bytes, fileName: str, folderId: Optional[str] = None) -> tuple[FileItem, str]: + """Saves an uploaded file if user has permission. + + Args: + folderId: Optional parent folder ID. None means root folder. + """ 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}") - + + logger.debug(f"Starting upload process for file: {fileName} (folderId={folderId!r})") + 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 to check for duplicates before any DB writes fileHash = hashlib.sha256(fileContent).hexdigest() - + # Duplicate check: same user + same fileHash + same fileName → return existing file # Same hash with different name is allowed (intentional copy by user) existingFile = self.checkForDuplicateFile(fileHash, fileName) if existingFile: logger.info(f"Duplicate detected for user {self.userId}: '{fileName}' with hash {fileHash[:12]}... — returning existing file {existingFile.id}") return existingFile, "exact_duplicate" - + # Determine MIME type mimeType = self.getMimeType(fileName) - + # createFile handles its own duplicate check (for calls from other code paths) # Here we already checked, so this will create a new file logger.debug(f"Saving file metadata to database for file: {fileName}") fileItem = self.createFile( name=fileName, mimeType=mimeType, - content=fileContent + content=fileContent, + folderId=folderId, ) # Save binary data diff --git a/modules/interfaces/interfaceRbac.py b/modules/interfaces/interfaceRbac.py index b8a87ba9..14953ef1 100644 --- a/modules/interfaces/interfaceRbac.py +++ b/modules/interfaces/interfaceRbac.py @@ -393,6 +393,13 @@ def getRecordsetPaginatedWithRBAC( continue if key not in validColumns: continue + if val is None: + # val=None in pagination.filters means "match empty/null" + # (same convention as connectorDbPostgre._buildPaginationClauses). + # Covers both historical empty-string values and true NULLs + # e.g. root-folder files where folderId may be "" or NULL. + whereConditions.append(f'("{key}" IS NULL OR "{key}"::TEXT = \'\')') + continue if isinstance(val, dict): op = val.get("operator", "equals") v = val.get("value", "") @@ -569,6 +576,13 @@ def getDistinctColumnValuesWithRBAC( continue if key not in validColumns: continue + if val is None: + # val=None in pagination.filters means "match empty/null" + # (same convention as connectorDbPostgre._buildPaginationClauses). + # Covers both historical empty-string values and true NULLs + # e.g. root-folder files where folderId may be "" or NULL. + whereConditions.append(f'("{key}" IS NULL OR "{key}"::TEXT = \'\')') + continue if isinstance(val, dict): op = val.get("operator", "equals") v = val.get("value", "") diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py index e989fb2e..e2842480 100644 --- a/modules/routes/routeDataFiles.py +++ b/modules/routes/routeDataFiles.py @@ -243,8 +243,16 @@ def get_files( recordFilter = None if paginationParams and paginationParams.filters and "folderId" in paginationParams.filters: - fVal = paginationParams.filters.pop("folderId") - recordFilter = {"folderId": fVal} + fVal = paginationParams.filters.get("folderId") + # For a concrete folderId we use recordFilter (exact equality). + # For null / empty (= "root") we keep it in pagination.filters so the + # connector applies `IS NULL OR = ''` – files predating the folderId + # fix were stored with an empty string instead of NULL. + if fVal is None or (isinstance(fVal, str) and fVal.strip() == ""): + paginationParams.filters["folderId"] = None + else: + paginationParams.filters.pop("folderId") + recordFilter = {"folderId": fVal} result = managementInterface.getAllFiles(pagination=paginationParams, recordFilter=recordFilter) @@ -282,13 +290,19 @@ async def upload_file( file: UploadFile = File(...), workflowId: Optional[str] = Form(None), featureInstanceId: Optional[str] = Form(None), - currentUser: User = Depends(getCurrentUser) + folderId: Optional[str] = Form(None), + currentUser: User = Depends(getCurrentUser), + context: RequestContext = Depends(getRequestContext), ) -> JSONResponse: # Add fileName property to UploadFile for consistency with backend model file.fileName = file.filename """Upload a file""" try: - managementInterface = interfaceDbManagement.getInterface(currentUser) + managementInterface = interfaceDbManagement.getInterface( + currentUser, + mandateId=str(context.mandateId) if context.mandateId else None, + featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None, + ) # Read file fileContent = await file.read() @@ -301,12 +315,29 @@ async def upload_file( detail=f"File too large. Maximum size: {interfaceDbManagement.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) + # Normalize folderId: empty string / "null" / "root" → None (root folder) + normalizedFolderId: Optional[str] = folderId + if isinstance(normalizedFolderId, str): + trimmed = normalizedFolderId.strip() + if not trimmed or trimmed.lower() in {"null", "none", "root"}: + normalizedFolderId = None + else: + normalizedFolderId = trimmed + # Save file via LucyDOM interface in the database - fileItem, duplicateType = managementInterface.saveUploadedFile(fileContent, file.filename) + fileItem, duplicateType = managementInterface.saveUploadedFile( + fileContent, file.filename, folderId=normalizedFolderId + ) if featureInstanceId and not fileItem.featureInstanceId: managementInterface.updateFile(fileItem.id, {"featureInstanceId": featureInstanceId}) fileItem.featureInstanceId = featureInstanceId + + # For exact duplicates we keep the existing record, but move it into the + # target folder so the user actually sees their upload land where they expect. + if duplicateType == "exact_duplicate" and normalizedFolderId != getattr(fileItem, "folderId", None): + managementInterface.updateFile(fileItem.id, {"folderId": normalizedFolderId}) + fileItem.folderId = normalizedFolderId # Determine response message based on duplicate type if duplicateType == "exact_duplicate":