bugfix(FIL-01 + files verschwunden nach hochladen und reload
This commit is contained in:
parent
18fb8e32b3
commit
d9f437f63e
3 changed files with 80 additions and 22 deletions
|
|
@ -1087,29 +1087,32 @@ class ComponentObjects:
|
||||||
return newfileName
|
return newfileName
|
||||||
counter += 1
|
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.
|
"""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,
|
Duplicate check: if a file with the same user + fileHash + fileName already exists,
|
||||||
the existing file is returned instead of creating a new one.
|
the existing file is returned instead of creating a new one.
|
||||||
Same hash with different name is allowed (intentional copy by user).
|
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"):
|
if not self.checkRbacPermission(FileItem, "create"):
|
||||||
raise PermissionError("No permission to create files")
|
raise PermissionError("No permission to create files")
|
||||||
|
|
||||||
# Compute file size and hash
|
# Compute file size and hash
|
||||||
fileSize = len(content)
|
fileSize = len(content)
|
||||||
fileHash = hashlib.sha256(content).hexdigest()
|
fileHash = hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
# Duplicate check: same user + same hash + same fileName → return existing
|
# Duplicate check: same user + same hash + same fileName → return existing
|
||||||
existingFile = self.checkForDuplicateFile(fileHash, name)
|
existingFile = self.checkForDuplicateFile(fileHash, name)
|
||||||
if existingFile:
|
if existingFile:
|
||||||
logger.info(f"Duplicate file detected in createFile: '{name}' (hash={fileHash[:12]}...) for user {self.userId} — returning existing file {existingFile.id}")
|
logger.info(f"Duplicate file detected in createFile: '{name}' (hash={fileHash[:12]}...) for user {self.userId} — returning existing file {existingFile.id}")
|
||||||
return existingFile
|
return existingFile
|
||||||
|
|
||||||
# Ensure fileName is unique
|
# Ensure fileName is unique
|
||||||
uniqueName = self._generateUniquefileName(name)
|
uniqueName = self._generateUniquefileName(name)
|
||||||
|
|
||||||
mandateId = self.mandateId or ""
|
mandateId = self.mandateId or ""
|
||||||
featureInstanceId = self.featureInstanceId or ""
|
featureInstanceId = self.featureInstanceId or ""
|
||||||
|
|
||||||
|
|
@ -1120,6 +1123,11 @@ class ComponentObjects:
|
||||||
else:
|
else:
|
||||||
scope = "personal"
|
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(
|
fileItem = FileItem(
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
featureInstanceId=featureInstanceId,
|
featureInstanceId=featureInstanceId,
|
||||||
|
|
@ -1128,7 +1136,7 @@ class ComponentObjects:
|
||||||
mimeType=mimeType,
|
mimeType=mimeType,
|
||||||
fileSize=fileSize,
|
fileSize=fileSize,
|
||||||
fileHash=fileHash,
|
fileHash=fileHash,
|
||||||
folderId="",
|
folderId=normalizedFolderId,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store in database
|
# Store in database
|
||||||
|
|
@ -1842,39 +1850,44 @@ class ComponentObjects:
|
||||||
logger.error(f"Error getting file content: {str(e)}")
|
logger.error(f"Error getting file content: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> tuple[FileItem, str]:
|
def saveUploadedFile(self, fileContent: bytes, fileName: str, folderId: Optional[str] = None) -> tuple[FileItem, str]:
|
||||||
"""Saves an uploaded file if user has permission."""
|
"""Saves an uploaded file if user has permission.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folderId: Optional parent folder ID. None means root folder.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Check file creation permission
|
# Check file creation permission
|
||||||
if not self.checkRbacPermission(FileItem, "create"):
|
if not self.checkRbacPermission(FileItem, "create"):
|
||||||
raise PermissionError("No permission to upload files")
|
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):
|
if not isinstance(fileContent, bytes):
|
||||||
logger.error(f"Invalid fileContent type: {type(fileContent)}")
|
logger.error(f"Invalid fileContent type: {type(fileContent)}")
|
||||||
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
|
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
|
||||||
|
|
||||||
# Compute file hash to check for duplicates before any DB writes
|
# Compute file hash to check for duplicates before any DB writes
|
||||||
fileHash = hashlib.sha256(fileContent).hexdigest()
|
fileHash = hashlib.sha256(fileContent).hexdigest()
|
||||||
|
|
||||||
# Duplicate check: same user + same fileHash + same fileName → return existing file
|
# Duplicate check: same user + same fileHash + same fileName → return existing file
|
||||||
# Same hash with different name is allowed (intentional copy by user)
|
# Same hash with different name is allowed (intentional copy by user)
|
||||||
existingFile = self.checkForDuplicateFile(fileHash, fileName)
|
existingFile = self.checkForDuplicateFile(fileHash, fileName)
|
||||||
if existingFile:
|
if existingFile:
|
||||||
logger.info(f"Duplicate detected for user {self.userId}: '{fileName}' with hash {fileHash[:12]}... — returning existing file {existingFile.id}")
|
logger.info(f"Duplicate detected for user {self.userId}: '{fileName}' with hash {fileHash[:12]}... — returning existing file {existingFile.id}")
|
||||||
return existingFile, "exact_duplicate"
|
return existingFile, "exact_duplicate"
|
||||||
|
|
||||||
# Determine MIME type
|
# Determine MIME type
|
||||||
mimeType = self.getMimeType(fileName)
|
mimeType = self.getMimeType(fileName)
|
||||||
|
|
||||||
# createFile handles its own duplicate check (for calls from other code paths)
|
# createFile handles its own duplicate check (for calls from other code paths)
|
||||||
# Here we already checked, so this will create a new file
|
# Here we already checked, so this will create a new file
|
||||||
logger.debug(f"Saving file metadata to database for file: {fileName}")
|
logger.debug(f"Saving file metadata to database for file: {fileName}")
|
||||||
fileItem = self.createFile(
|
fileItem = self.createFile(
|
||||||
name=fileName,
|
name=fileName,
|
||||||
mimeType=mimeType,
|
mimeType=mimeType,
|
||||||
content=fileContent
|
content=fileContent,
|
||||||
|
folderId=folderId,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save binary data
|
# Save binary data
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,13 @@ def getRecordsetPaginatedWithRBAC(
|
||||||
continue
|
continue
|
||||||
if key not in validColumns:
|
if key not in validColumns:
|
||||||
continue
|
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):
|
if isinstance(val, dict):
|
||||||
op = val.get("operator", "equals")
|
op = val.get("operator", "equals")
|
||||||
v = val.get("value", "")
|
v = val.get("value", "")
|
||||||
|
|
@ -569,6 +576,13 @@ def getDistinctColumnValuesWithRBAC(
|
||||||
continue
|
continue
|
||||||
if key not in validColumns:
|
if key not in validColumns:
|
||||||
continue
|
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):
|
if isinstance(val, dict):
|
||||||
op = val.get("operator", "equals")
|
op = val.get("operator", "equals")
|
||||||
v = val.get("value", "")
|
v = val.get("value", "")
|
||||||
|
|
|
||||||
|
|
@ -243,8 +243,16 @@ def get_files(
|
||||||
|
|
||||||
recordFilter = None
|
recordFilter = None
|
||||||
if paginationParams and paginationParams.filters and "folderId" in paginationParams.filters:
|
if paginationParams and paginationParams.filters and "folderId" in paginationParams.filters:
|
||||||
fVal = paginationParams.filters.pop("folderId")
|
fVal = paginationParams.filters.get("folderId")
|
||||||
recordFilter = {"folderId": fVal}
|
# 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)
|
result = managementInterface.getAllFiles(pagination=paginationParams, recordFilter=recordFilter)
|
||||||
|
|
||||||
|
|
@ -282,13 +290,19 @@ async def upload_file(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
workflowId: Optional[str] = Form(None),
|
workflowId: Optional[str] = Form(None),
|
||||||
featureInstanceId: 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:
|
) -> JSONResponse:
|
||||||
# Add fileName property to UploadFile for consistency with backend model
|
# Add fileName property to UploadFile for consistency with backend model
|
||||||
file.fileName = file.filename
|
file.fileName = file.filename
|
||||||
"""Upload a file"""
|
"""Upload a file"""
|
||||||
try:
|
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
|
# Read file
|
||||||
fileContent = await file.read()
|
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"
|
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
|
# 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:
|
if featureInstanceId and not fileItem.featureInstanceId:
|
||||||
managementInterface.updateFile(fileItem.id, {"featureInstanceId": featureInstanceId})
|
managementInterface.updateFile(fileItem.id, {"featureInstanceId": featureInstanceId})
|
||||||
fileItem.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
|
# Determine response message based on duplicate type
|
||||||
if duplicateType == "exact_duplicate":
|
if duplicateType == "exact_duplicate":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue