269 lines
13 KiB
Python
269 lines
13 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
|
|
import logging
|
|
import time
|
|
import json
|
|
import urllib.parse
|
|
from typing import Dict, Any
|
|
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
async def uploadDocument(self, parameters: Dict[str, Any]) -> ActionResult:
|
|
operationId = None
|
|
try:
|
|
# Init progress logger
|
|
workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
|
|
operationId = f"sharepoint_upload_{workflowId}_{int(time.time())}"
|
|
|
|
# Start progress tracking
|
|
parentOperationId = parameters.get('parentOperationId')
|
|
self.services.chat.progressLogStart(
|
|
operationId,
|
|
"Upload Document",
|
|
"SharePoint Upload",
|
|
"Processing document list",
|
|
parentOperationId=parentOperationId
|
|
)
|
|
|
|
connectionReference = parameters.get("connectionReference")
|
|
documentList = parameters.get("documentList")
|
|
pathQuery = parameters.get("pathQuery")
|
|
if isinstance(documentList, str):
|
|
documentList = [documentList]
|
|
|
|
if not connectionReference:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="Connection reference is required")
|
|
|
|
if not documentList:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="Document list is required")
|
|
|
|
# Parse documentList to extract folder path and site information
|
|
uploadPath, sites, filesToUpload, errorMsg = await self.documentParsing.parseDocumentListForFolder(documentList)
|
|
if errorMsg:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error=errorMsg)
|
|
|
|
# If no folder path found from documentList, use pathQuery if provided
|
|
if not uploadPath and pathQuery and pathQuery.strip() != "" and pathQuery.strip() != "*":
|
|
uploadPath = pathQuery
|
|
logger.info(f"Using pathQuery for upload path: {uploadPath}")
|
|
# Resolve sites from pathQuery
|
|
sites, errorMsg = await self.siteDiscovery.resolveSitesFromPathQuery(pathQuery)
|
|
if errorMsg:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error=errorMsg)
|
|
|
|
# Validate required parameters
|
|
if not uploadPath:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="Either documentList must contain findDocumentPath result with folder information, or pathQuery must be provided. Use findDocumentPath first to get upload folder, or provide pathQuery directly.")
|
|
|
|
if not sites:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="Site information missing. Cannot determine target site for upload.")
|
|
|
|
if not filesToUpload:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="No files to upload found in documentList.")
|
|
|
|
# Get connection
|
|
self.services.chat.progressLogUpdate(operationId, 0.3, "Getting Microsoft connection")
|
|
connection = self.connection.getMicrosoftConnection(connectionReference)
|
|
if not connection:
|
|
if operationId:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
return ActionResult.isFailure(error="No valid Microsoft connection found for the provided connection reference")
|
|
|
|
# Process upload paths
|
|
uploadPaths = []
|
|
if uploadPath.startswith('01PPXICCB') or uploadPath.startswith('01'):
|
|
# It's a folder ID - use it directly
|
|
uploadPaths = [uploadPath]
|
|
logger.info(f"Using folder ID directly for upload: {uploadPath}")
|
|
else:
|
|
# It's a path - resolve it normally
|
|
uploadPaths = self.pathProcessing.resolvePathQuery(uploadPath)
|
|
|
|
# Process each document upload
|
|
uploadResults = []
|
|
|
|
# Extract file names from documents
|
|
fileNames = [doc.fileName for doc in filesToUpload]
|
|
logger.info(f"Using file names from documentList: {fileNames}")
|
|
|
|
self.services.chat.progressLogUpdate(operationId, 0.5, f"Uploading {len(filesToUpload)} document(s)")
|
|
|
|
for i, (chatDocument, fileName) in enumerate(zip(filesToUpload, fileNames)):
|
|
try:
|
|
fileId = chatDocument.fileId
|
|
fileData = self.services.chat.getFileData(fileId)
|
|
|
|
if not fileData:
|
|
logger.warning(f"File data not found for fileId: {fileId}")
|
|
uploadResults.append({
|
|
"fileName": fileName,
|
|
"fileId": fileId,
|
|
"error": "File data not found",
|
|
"uploadStatus": "failed"
|
|
})
|
|
continue
|
|
|
|
# Upload to the first available site (or could be made configurable)
|
|
uploadSuccessful = False
|
|
|
|
for site in sites:
|
|
siteId = site["id"]
|
|
siteName = site["displayName"]
|
|
siteUrl = site["webUrl"]
|
|
|
|
# Use the first upload path or default to Documents
|
|
uploadPath = uploadPaths[0] if uploadPaths else "/Documents"
|
|
|
|
# Handle wildcard paths - replace with default Documents folder
|
|
if uploadPath == "*":
|
|
uploadPath = "/Documents"
|
|
logger.warning(f"Wildcard path '*' detected, using default '/Documents' folder for upload")
|
|
|
|
# Check if uploadPath is a folder ID or a regular path
|
|
if uploadPath.startswith('01PPXICCB') or uploadPath.startswith('01'):
|
|
# It's a folder ID - use the folder-specific upload endpoint
|
|
uploadEndpoint = f"sites/{siteId}/drive/items/{uploadPath}:/{fileName}:/content"
|
|
logger.info(f"Using folder ID upload endpoint: {uploadEndpoint}")
|
|
else:
|
|
# It's a regular path - use the root-based upload endpoint
|
|
# Strip /sites/{siteName}/ prefix if present (Graph API path is relative to site's drive)
|
|
uploadPathForApi = uploadPath
|
|
if uploadPathForApi.startswith('/sites/'):
|
|
# Extract path after /sites/{siteName}/
|
|
parts = uploadPathForApi.split('/', 3) # ['', 'sites', 'sitename', 'rest/of/path']
|
|
if len(parts) >= 4:
|
|
uploadPathForApi = '/' + parts[3] # Keep the rest after /sites/sitename/
|
|
logger.info(f"Stripped site prefix from upload path: '{uploadPath}' -> '{uploadPathForApi}'")
|
|
|
|
uploadPathForApi = uploadPathForApi.rstrip('/') + '/' + fileName
|
|
uploadPathClean = uploadPathForApi.lstrip('/')
|
|
uploadEndpoint = f"sites/{siteId}/drive/root:/{uploadPathClean}:/content"
|
|
logger.info(f"Using path-based upload endpoint: {uploadEndpoint}")
|
|
|
|
# Upload endpoint for small files (< 4MB)
|
|
if len(fileData) < 4 * 1024 * 1024: # 4MB
|
|
|
|
# Upload the file
|
|
uploadResult = await self.apiClient.makeGraphApiCall(
|
|
uploadEndpoint,
|
|
method="PUT",
|
|
data=fileData
|
|
)
|
|
|
|
if "error" not in uploadResult:
|
|
uploadResults.append({
|
|
"fileName": fileName,
|
|
"fileId": fileId,
|
|
"uploadStatus": "success",
|
|
"siteName": siteName,
|
|
"siteUrl": siteUrl,
|
|
"uploadPath": uploadPath,
|
|
"uploadEndpoint": uploadEndpoint,
|
|
"sharepointFileId": uploadResult.get("id"),
|
|
"webUrl": uploadResult.get("webUrl"),
|
|
"size": uploadResult.get("size"),
|
|
"createdDateTime": uploadResult.get("createdDateTime")
|
|
})
|
|
uploadSuccessful = True
|
|
break
|
|
else:
|
|
logger.warning(f"Upload failed to site {siteName}: {uploadResult['error']}")
|
|
else:
|
|
# For large files, we would need to implement resumable upload
|
|
logger.warning(f"File too large ({len(fileData)} bytes) for site {siteName}")
|
|
continue
|
|
|
|
if not uploadSuccessful:
|
|
uploadResults.append({
|
|
"fileName": fileName,
|
|
"fileId": fileId,
|
|
"error": f"File too large ({len(fileData)} bytes) or upload failed to all sites. Files larger than 4MB require resumable upload (not implemented).",
|
|
"uploadStatus": "failed"
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error uploading document {fileName}: {str(e)}")
|
|
uploadResults.append({
|
|
"fileName": fileName,
|
|
"fileId": fileId,
|
|
"error": str(e),
|
|
"uploadStatus": "failed"
|
|
})
|
|
|
|
# Update progress for each file
|
|
self.services.chat.progressLogUpdate(operationId, 0.5 + (i * 0.4 / len(filesToUpload)), f"Uploaded {i + 1}/{len(filesToUpload)} file(s)")
|
|
|
|
# Create result data
|
|
resultData = {
|
|
"connectionReference": connectionReference,
|
|
"uploadPath": uploadPath,
|
|
"documentList": documentList,
|
|
"fileNames": fileNames,
|
|
"sitesAvailable": len(sites),
|
|
"uploadResults": uploadResults,
|
|
"connection": {
|
|
"id": connection["id"],
|
|
"authority": "microsoft",
|
|
"reference": connectionReference
|
|
},
|
|
"timestamp": self.services.utils.timestampGetUtc()
|
|
}
|
|
|
|
# Use default JSON format for output
|
|
outputExtension = ".json" # Default
|
|
outputMimeType = "application/json" # Default
|
|
|
|
validationMetadata = {
|
|
"actionType": "sharepoint.uploadDocument",
|
|
"connectionReference": connectionReference,
|
|
"uploadPath": uploadPath,
|
|
"fileNames": fileNames,
|
|
"uploadCount": len(uploadResults),
|
|
"successfulUploads": len([r for r in uploadResults if r.get("uploadStatus") == "success"]),
|
|
"failedUploads": len([r for r in uploadResults if r.get("uploadStatus") == "failed"])
|
|
}
|
|
|
|
successfulUploads = len([r for r in uploadResults if r.get("uploadStatus") == "success"])
|
|
self.services.chat.progressLogUpdate(operationId, 0.9, f"Uploaded {successfulUploads}/{len(uploadResults)} file(s)")
|
|
self.services.chat.progressLogFinish(operationId, successfulUploads > 0)
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
documents=[
|
|
ActionDocument(
|
|
documentName=self._generateMeaningfulFileName("sharepoint_upload", "json", None, "uploadDocument"),
|
|
documentData=json.dumps(resultData, indent=2),
|
|
mimeType=outputMimeType,
|
|
validationMetadata=validationMetadata
|
|
)
|
|
]
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error uploading to SharePoint: {str(e)}")
|
|
if operationId:
|
|
try:
|
|
self.services.chat.progressLogFinish(operationId, False)
|
|
except:
|
|
pass
|
|
return ActionResult(
|
|
success=False,
|
|
error=str(e)
|
|
)
|
|
|