gateway/modules/routes/routeSharepoint.py

196 lines
7.9 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
SharePoint routes for folder browsing
Provides endpoints for listing SharePoint sites and browsing folders
"""
import logging
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Depends, Path, Query, Request, status
from modules.auth import limiter, getCurrentUser
from modules.datamodels.datamodelUam import User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface
from modules.serviceHub import getInterface as getServices
from modules.shared.i18nRegistry import apiRouteContext
routeApiMsg = apiRouteContext("routeSharepoint")
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/sharepoint",
tags=["SharePoint"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
500: {"description": "Internal server error"}
}
)
def _getUserConnectionByReference(interface, connectionReference: str, userId: str) -> Optional[UserConnection]:
"""
Get a user connection by reference string (format: connection:authority:username).
Args:
interface: Database interface
connectionReference: Reference string like 'connection:msft:user@email.com'
userId: User ID to verify ownership
Returns:
UserConnection if found and belongs to user, None otherwise
"""
try:
# Parse reference format: connection:{authority}:{username} [status:..., token:...]
# Remove state information if present
baseReference = connectionReference.split(' [')[0]
parts = baseReference.split(':')
if len(parts) < 3 or parts[0] != "connection":
logger.warning(f"Invalid connection reference format: {connectionReference}")
return None
authority = parts[1] # e.g., 'msft'
username = ':'.join(parts[2:]) # Handle usernames with colons
# Get user connections and find matching one
connections = interface.getUserConnections(userId)
for conn in connections:
connAuthority = conn.authority.value if hasattr(conn.authority, 'value') else str(conn.authority)
if connAuthority.lower() == authority.lower() and conn.externalUsername == username:
return conn
logger.debug(f"No connection found for reference: {connectionReference}")
return None
except Exception as e:
logger.error(f"Error getting user connection by reference: {str(e)}")
return None
def _extractSitePath(webUrl: str) -> str:
"""Extract site path from webUrl (e.g., https://company.sharepoint.com/sites/MySite -> /sites/MySite)"""
if "/sites/" in webUrl:
return "/sites/" + webUrl.split("/sites/")[1].split("/")[0]
return ""
# ============================================================================
# Universal folder-options endpoint (by connectionReference)
# ============================================================================
@router.get("/folder-options", response_model=List[Dict[str, Any]])
@limiter.limit("30/minute")
async def getSharepointFolderOptionsByReference(
request: Request,
connectionReference: str = Query(..., description="Connection reference string (e.g., 'connection:msft:user@email.com')"),
siteId: Optional[str] = Query(None, description="Specific site ID to browse (if omitted, returns sites only)"),
path: Optional[str] = Query(None, description="Folder path within site to browse"),
includeFiles: bool = Query(False, description="If true, also include files (not only folders) in the response"),
currentUser: User = Depends(getCurrentUser)
) -> List[Dict[str, Any]]:
"""
Get SharePoint folders formatted as dropdown options (universal endpoint).
Uses connectionReference instead of connectionId for easier integration.
Two modes:
1. If siteId is not provided: Returns list of sites (for site selection)
2. If siteId is provided: Returns folders within that site (optionally at specific path)
Args:
connectionReference: Connection reference string (e.g., 'connection:msft:user@email.com')
siteId: Optional site ID to browse folders within
path: Optional folder path within site
"""
try:
interface = getInterface(currentUser)
# Get the connection by reference
connection = _getUserConnectionByReference(interface, connectionReference, currentUser.id)
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Connection not found for reference: {connectionReference}"
)
# Verify it's a Microsoft connection
authority = connection.authority.value if hasattr(connection.authority, 'value') else str(connection.authority)
if authority.lower() != 'msft':
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Connection is not a Microsoft connection (authority: {authority})"
)
# Initialize services
services = getServices(currentUser, None)
# Set access token on SharePoint service
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
# Mode 1: Return sites list if no siteId specified
if not siteId:
sites = await services.sharepoint.discoverSites()
return [
{
"type": "site",
"value": site.get("id"),
"label": site.get("displayName", "Unknown Site"),
"siteId": site.get("id"),
"siteName": site.get("displayName", "Unknown Site"),
"webUrl": site.get("webUrl", ""),
"path": _extractSitePath(site.get("webUrl", ""))
}
for site in sites
]
# Mode 2: Return folders within specific site
folderPath = path or ""
items = await services.sharepoint.listFolderContents(siteId, folderPath)
if not items:
return []
folderOptions = []
for item in items:
itemType = item.get("type")
if itemType == "folder":
folderName = item.get("name", "")
itemPath = f"{folderPath}/{folderName}" if folderPath else folderName
folderOptions.append({
"type": "folder",
"value": itemPath,
"label": folderName,
"siteId": siteId,
"folderName": folderName,
"path": itemPath,
"hasChildren": True
})
elif includeFiles and itemType == "file":
fileName = item.get("name", "")
itemPath = f"{folderPath}/{fileName}" if folderPath else fileName
folderOptions.append({
"type": "file",
"value": itemPath,
"label": fileName,
"siteId": siteId,
"fileName": fileName,
"path": itemPath,
"mimeType": item.get("mimeType"),
"size": item.get("size"),
})
return folderOptions
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting SharePoint folder options by reference: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting SharePoint folder options: {str(e)}"
)