| 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> = { |
| |
| 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', |
|
|
| |
| jpg: 'image/jpeg', |
| jpeg: 'image/jpeg', |
| png: 'image/png', |
| gif: 'image/gif', |
| bmp: 'image/bmp', |
| svg: 'image/svg+xml', |
| webp: 'image/webp', |
|
|
| |
| zip: 'application/zip', |
| rar: 'application/x-rar-compressed', |
|
|
| |
| mp4: 'video/mp4', |
| mp3: 'audio/mpeg', |
| wav: 'audio/wav', |
| }; |
|
|
| return mimeTypes[extension || ''] || 'application/octet-stream'; |
| } |
|
|
| export { getMimeTypeFromFileName }; |
|
|