/** * Microsoft Graph helper for uploading files to SharePoint * Uses client credentials flow (app-only authentication) */ interface SharePointConfig { tenantId: string; clientId: string; clientSecret: string; sharepointUrl: string; // Full SharePoint folder URL } interface UploadResult { success: boolean; webUrl?: string; error?: string; } /** * Parse a SharePoint folder URL to extract site and drive path info * Supports URLs like: * https://tenant.sharepoint.com/sites/SiteName/Documents%20partages/FolderPath * https://tenant.sharepoint.com/:f:/r/sites/SiteName/Documents%20partages/FolderPath?... */ function parseSharePointUrl(url: string): { siteHostname: string; sitePath: string; folderPath: string } | null { try { const parsed = new URL(url); const hostname = parsed.hostname; // e.g. itinova.sharepoint.com // Remove query params and hash let pathname = parsed.pathname; // Handle /:f:/r/sites/... format (sharing links) pathname = pathname.replace(/^\/:f:\/r/, ''); // Decode URL encoding pathname = decodeURIComponent(pathname); // Extract site path: /sites/SiteName const siteMatch = pathname.match(/^(\/sites\/[^/]+)/i); if (!siteMatch) return null; const sitePath = siteMatch[1]; // e.g. /sites/ITINOVA-27.Factures // Everything after the site path is the folder path within the drive const folderPath = pathname.slice(sitePath.length); // e.g. /Documents partages/27. Factures/test return { siteHostname: hostname, sitePath, folderPath }; } catch { return null; } } /** * Get an access token using client credentials flow */ async function getAccessToken(tenantId: string, clientId: string, clientSecret: string): Promise { const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret, scope: 'https://graph.microsoft.com/.default', }); const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to get access token: ${error}`); } const data = await response.json() as { access_token: string }; return data.access_token; } /** * Upload a file to SharePoint using Microsoft Graph API */ export async function uploadToSharePoint( config: SharePointConfig, fileBuffer: Buffer, fileName: string ): Promise { try { const parsed = parseSharePointUrl(config.sharepointUrl); if (!parsed) { return { success: false, error: `URL SharePoint invalide: ${config.sharepointUrl}` }; } const { siteHostname, sitePath, folderPath } = parsed; // Get access token const accessToken = await getAccessToken(config.tenantId, config.clientId, config.clientSecret); const graphBase = 'https://graph.microsoft.com/v1.0'; const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }; // 1. Get the site ID const siteResponse = await fetch( `${graphBase}/sites/${siteHostname}:${sitePath}`, { headers } ); if (!siteResponse.ok) { const err = await siteResponse.text(); return { success: false, error: `Site SharePoint introuvable (${siteResponse.status}): ${err}` }; } const site = await siteResponse.json() as { id: string }; const siteId = site.id; // 2. Get the default drive const driveResponse = await fetch(`${graphBase}/sites/${siteId}/drive`, { headers }); if (!driveResponse.ok) { const err = await driveResponse.text(); return { success: false, error: `Drive SharePoint introuvable: ${err}` }; } const drive = await driveResponse.json() as { id: string }; const driveId = drive.id; // 3. Build the folder path for the upload URL // folderPath is like "/Documents partages/27. Factures/test" // We need to encode it properly for Graph API const cleanFolder = folderPath.replace(/^\//, '').replace(/\/$/, ''); const encodedFolder = cleanFolder.split('/').map(encodeURIComponent).join('/'); const encodedFileName = encodeURIComponent(fileName); // 4. Upload the file (simple upload for files < 4MB, resumable for larger) const uploadUrl = fileBuffer.length < 4 * 1024 * 1024 ? `${graphBase}/drives/${driveId}/root:/${encodedFolder}/${encodedFileName}:/content` : null; if (uploadUrl) { // Simple upload const uploadResponse = await fetch(uploadUrl, { method: 'PUT', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/pdf', }, body: new Uint8Array(fileBuffer), }); if (!uploadResponse.ok) { const err = await uploadResponse.text(); return { success: false, error: `Échec de l'upload (${uploadResponse.status}): ${err}` }; } const uploaded = await uploadResponse.json() as { webUrl?: string }; return { success: true, webUrl: uploaded.webUrl }; } else { // Resumable upload for large files const createSessionUrl = `${graphBase}/drives/${driveId}/root:/${encodedFolder}/${encodedFileName}:/createUploadSession`; const sessionResponse = await fetch(createSessionUrl, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ item: { '@microsoft.graph.conflictBehavior': 'replace' } }), }); if (!sessionResponse.ok) { const err = await sessionResponse.text(); return { success: false, error: `Échec de création de session d'upload: ${err}` }; } const session = await sessionResponse.json() as { uploadUrl: string }; const chunkSize = 320 * 1024 * 10; // 3.2MB chunks let start = 0; while (start < fileBuffer.length) { const end = Math.min(start + chunkSize, fileBuffer.length); const chunk = fileBuffer.slice(start, end); const chunkResponse = await fetch(session.uploadUrl, { method: 'PUT', headers: { 'Content-Range': `bytes ${start}-${end - 1}/${fileBuffer.length}`, 'Content-Length': chunk.length.toString(), }, body: new Uint8Array(chunk), }); if (!chunkResponse.ok && chunkResponse.status !== 202) { const err = await chunkResponse.text(); return { success: false, error: `Échec de l'upload du chunk: ${err}` }; } if (chunkResponse.status === 201 || chunkResponse.status === 200) { const uploaded = await chunkResponse.json() as { webUrl?: string }; return { success: true, webUrl: uploaded.webUrl }; } start = end; } return { success: false, error: 'Upload incomplet' }; } } catch (error) { return { success: false, error: `Erreur Microsoft Graph: ${error instanceof Error ? error.message : String(error)}` }; } }