import { api } from 'boot/services'; import { getUploadMaxFilesize } from 'boot/wpapi'; import { INITIAL_SETTINGS } from '../main/constants'; import uploadsState from './state'; import type { UploadJob } from './state'; /** Fraction of the WP upload limit used as chunk size (leaves room for multipart overhead). */ const CHUNK_RATIO = 0.8; /** Minimum chunk size in bytes (1 MB). */ const MIN_CHUNK_SIZE = 1024 * 1024; export default { /** * Upload a large file in binary chunks, assemble and parse on the server, * then create the table DB record. The operation runs in the background — * callers should NOT await this function. */ async startChunkedUpload( file: File, title: string, fileSize: string, tableType: string, options?: { settings?: string; originalType?: string; } ) { const uploadId: string = crypto.randomUUID(); const wpLimit = getUploadMaxFilesize(); const chunkSize = Math.max( Math.floor(wpLimit * CHUNK_RATIO), MIN_CHUNK_SIZE ); const totalChunks = Math.ceil(file.size / chunkSize); const fileExtension = (file.name.split('.').pop() ?? 'csv').toLowerCase(); const job: UploadJob = { id: uploadId, title, progress: 0, status: 'uploading', tableType, }; uploadsState.activeUploads.value.push(job); const setJobField = ( field: K, value: UploadJob[K] ) => { const idx = uploadsState.activeUploads.value.findIndex( (j) => j.id === uploadId ); if (idx !== -1) { uploadsState.activeUploads.value[idx][field] = value; } }; const isCancelled = () => { const j = uploadsState.activeUploads.value.find((j) => j.id === uploadId); return !j || j.status === 'cancelled'; }; try { for (let i = 0; i < totalChunks; i++) { if (isCancelled()) return; const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunkBlob = file.slice(start, end); const { error } = await api.uploadChunk({ uploadId, chunkIndex: i, totalChunks, filename: file.name, fileType: fileExtension, chunkBlob, }); if (error) { setJobField('status', 'error'); return; } if (isCancelled()) { await api.cancelChunkUpload(uploadId); return; } // Chunk upload is 0–90 % of total progress; finalize is 90–100 %. setJobField('progress', Math.round(((i + 1) / totalChunks) * 90)); } if (isCancelled()) { await api.cancelChunkUpload(uploadId); return; } setJobField('status', 'processing'); setJobField('progress', 95); const { response, error: finalizeError } = await api.finalizeUpload({ uploadId, filename: file.name, fileType: fileExtension, title, totalChunks, fileSize, settings: options?.settings ?? JSON.stringify(INITIAL_SETTINGS), originalType: options?.originalType, }); if (finalizeError) { setJobField('status', 'error'); return; } // The backend created the table in 'importing' state; marking the job // done lets IndexPage refresh the list and drive the row import. const data = response?.data as { table_id: number }; setJobField('progress', 100); setJobField('tableId', data?.table_id); setJobField('status', 'done'); // Keep the row visible briefly so the watcher in IndexPage can react, // then remove it from the list. setTimeout(() => { uploadsState.activeUploads.value = uploadsState.activeUploads.value.filter((j) => j.id !== uploadId); }, 2500); } catch { setJobField('status', 'error'); } }, /** * Cancel an in-progress upload: removes the chunks already stored on the * server. */ async cancelUpload(uploadId: string) { const idx = uploadsState.activeUploads.value.findIndex( (j) => j.id === uploadId ); if (idx === -1) return; const job = uploadsState.activeUploads.value[idx]; // Mark as cancelled so the upload loop stops. job.status = 'cancelled'; await api.cancelChunkUpload(uploadId); uploadsState.activeUploads.value = uploadsState.activeUploads.value.filter( (j) => j.id !== uploadId ); }, };