mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00

* feat(sharepoint): integrate SharePoint file picker and download workflow Introduces end‑to‑end SharePoint import support: * Token exchange with Microsoft Graph and scope management (`useSharePointToken`) * Re‑usable hooks: `useSharePointPicker`, `useSharePointDownload`, `useSharePointFileHandling` * FileSearch dropdown now offers **From Local Machine** / **From SharePoint** sources and gracefully falls back when SharePoint is disabled * Agent upload model, `AttachFileMenu`, and `DropdownPopup` extended for SharePoint files and sub‑menus * Blurry overlay with progress indicator and `maxSelectionCount` limit during downloads * Cache‑flush utility (`config/flush-cache.js`) supporting Redis & filesystem, with dry‑run and npm script * Updated `SharePointIcon` (uses `currentColor`) and new i18n keys * Bug fixes: placeholder syntax in progress message, picker event‑listener cleanup * Misc style and performance optimizations * Fix ESLint warnings --------- Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
232 lines
6.1 KiB
TypeScript
232 lines
6.1 KiB
TypeScript
import { useMutation } from '@tanstack/react-query';
|
|
import type { UseMutationResult } from '@tanstack/react-query';
|
|
|
|
export interface SharePointFile {
|
|
id: string;
|
|
name: string;
|
|
size: number;
|
|
webUrl: string;
|
|
downloadUrl: string;
|
|
driveId: string;
|
|
itemId: string;
|
|
sharePointItem: any;
|
|
}
|
|
|
|
export interface SharePointDownloadProgress {
|
|
fileId: string;
|
|
fileName: string;
|
|
loaded: number;
|
|
total: number;
|
|
progress: number;
|
|
}
|
|
|
|
export interface SharePointBatchProgress {
|
|
completed: number;
|
|
total: number;
|
|
currentFile?: string;
|
|
failed: string[];
|
|
}
|
|
|
|
export const useSharePointFileDownload = (): UseMutationResult<
|
|
File,
|
|
unknown,
|
|
{
|
|
file: SharePointFile;
|
|
accessToken: string;
|
|
onProgress?: (progress: SharePointDownloadProgress) => void;
|
|
}
|
|
> => {
|
|
return useMutation({
|
|
mutationFn: async ({ file, accessToken, onProgress }) => {
|
|
const downloadUrl =
|
|
file.downloadUrl ||
|
|
`https://graph.microsoft.com/v1.0/drives/${file.driveId}/items/${file.itemId}/content`;
|
|
|
|
const response = await fetch(downloadUrl, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const contentLength = parseInt(response.headers.get('content-length') || '0');
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
throw new Error('Failed to get response reader');
|
|
}
|
|
|
|
const chunks: Uint8Array[] = [];
|
|
let receivedLength = 0;
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) break;
|
|
|
|
chunks.push(value);
|
|
receivedLength += value.length;
|
|
|
|
if (onProgress) {
|
|
onProgress({
|
|
fileId: file.id,
|
|
fileName: file.name,
|
|
loaded: receivedLength,
|
|
total: contentLength || file.size,
|
|
progress: Math.round((receivedLength / (contentLength || file.size)) * 100),
|
|
});
|
|
}
|
|
}
|
|
|
|
const allChunks = new Uint8Array(receivedLength);
|
|
let position = 0;
|
|
for (const chunk of chunks) {
|
|
allChunks.set(chunk, position);
|
|
position += chunk.length;
|
|
}
|
|
|
|
const contentType =
|
|
response.headers.get('content-type') || getMimeTypeFromFileName(file.name);
|
|
|
|
const blob = new Blob([allChunks], { type: contentType });
|
|
const downloadedFile = new File([blob], file.name, {
|
|
type: contentType,
|
|
lastModified: Date.now(),
|
|
});
|
|
|
|
return downloadedFile;
|
|
},
|
|
retry: 2,
|
|
});
|
|
};
|
|
|
|
export const useSharePointBatchDownload = (): UseMutationResult<
|
|
File[],
|
|
unknown,
|
|
{
|
|
files: SharePointFile[];
|
|
accessToken: string;
|
|
onProgress?: (progress: SharePointBatchProgress) => void;
|
|
},
|
|
unknown
|
|
> => {
|
|
return useMutation({
|
|
mutationFn: async ({ files, accessToken, onProgress }) => {
|
|
const downloadedFiles: File[] = [];
|
|
const failed: string[] = [];
|
|
let completed = 0;
|
|
|
|
const concurrencyLimit = 3;
|
|
const chunks: SharePointFile[][] = [];
|
|
for (let i = 0; i < files.length; i += concurrencyLimit) {
|
|
chunks.push(files.slice(i, i + concurrencyLimit));
|
|
}
|
|
|
|
for (const chunk of chunks) {
|
|
const chunkPromises = chunk.map(async (file) => {
|
|
try {
|
|
const downloadUrl =
|
|
file.downloadUrl ||
|
|
`https://graph.microsoft.com/v1.0/drives/${file.driveId}/items/${file.itemId}/content`;
|
|
|
|
const response = await fetch(downloadUrl, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const contentType =
|
|
response.headers.get('content-type') || getMimeTypeFromFileName(file.name);
|
|
|
|
const downloadedFile = new File([blob], file.name, {
|
|
type: contentType,
|
|
lastModified: Date.now(),
|
|
});
|
|
|
|
completed++;
|
|
onProgress?.({
|
|
completed,
|
|
total: files.length,
|
|
currentFile: file.name,
|
|
failed,
|
|
});
|
|
|
|
return downloadedFile;
|
|
} catch (error) {
|
|
console.error(`Failed to download ${file.name}:`, error);
|
|
failed.push(file.name);
|
|
completed++;
|
|
onProgress?.({
|
|
completed,
|
|
total: files.length,
|
|
currentFile: `Error: ${file.name}`,
|
|
failed,
|
|
});
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
|
|
chunkResults.forEach((result) => {
|
|
if (result.status === 'fulfilled') {
|
|
downloadedFiles.push(result.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (failed.length > 0) {
|
|
console.warn(`Failed to download ${failed.length} files:`, failed);
|
|
}
|
|
|
|
return downloadedFiles;
|
|
},
|
|
retry: 1,
|
|
});
|
|
};
|
|
|
|
function getMimeTypeFromFileName(fileName: string): string {
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
|
const mimeTypes: Record<string, string> = {
|
|
// Documents
|
|
pdf: 'application/pdf',
|
|
doc: 'application/msword',
|
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
xls: 'application/vnd.ms-excel',
|
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
ppt: 'application/vnd.ms-powerpoint',
|
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
txt: 'text/plain',
|
|
csv: 'text/csv',
|
|
|
|
// Images
|
|
jpg: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
png: 'image/png',
|
|
gif: 'image/gif',
|
|
bmp: 'image/bmp',
|
|
svg: 'image/svg+xml',
|
|
webp: 'image/webp',
|
|
|
|
// Archives
|
|
zip: 'application/zip',
|
|
rar: 'application/x-rar-compressed',
|
|
|
|
// Media
|
|
mp4: 'video/mp4',
|
|
mp3: 'audio/mpeg',
|
|
wav: 'audio/wav',
|
|
};
|
|
|
|
return mimeTypes[extension || ''] || 'application/octet-stream';
|
|
}
|
|
|
|
export { getMimeTypeFromFileName };
|