const MEBIBYTE = 1024 * 1024 export const MAX_LOGICAL_UPLOAD_BYTES = 10 * 1024 * MEBIBYTE export const MAX_UPLOAD_CHUNK_BYTES = 20 * MEBIBYTE export const MAX_UPLOAD_CHUNKS = Math.ceil(MAX_LOGICAL_UPLOAD_BYTES / (10 * MEBIBYTE)) const LEGACY_BROWSER_CHUNK_SIZES = [10 * MEBIBYTE, 20 * MEBIBYTE] /** * Resolve and validate the physical chunk size used for one upload session. * Older already-open browser pages did not send chunkSize to init; only the two * historically supported 10/20 MiB layouts are inferred for those sessions. */ export function resolveUploadChunkSize( fileSize: number, totalChunks: number, rawChunkSize?: unknown, ): number | null { if ( !Number.isSafeInteger(fileSize) || fileSize <= 0 || fileSize > MAX_LOGICAL_UPLOAD_BYTES || !Number.isInteger(totalChunks) || totalChunks <= 0 || totalChunks > MAX_UPLOAD_CHUNKS ) { return null } const hasExplicitChunkSize = rawChunkSize !== undefined && rawChunkSize !== null && String(rawChunkSize).trim() !== '' if (hasExplicitChunkSize) { const chunkSize = Number(rawChunkSize) if ( !Number.isSafeInteger(chunkSize) || chunkSize <= 0 || chunkSize > MAX_UPLOAD_CHUNK_BYTES || Math.ceil(fileSize / chunkSize) !== totalChunks ) { return null } return chunkSize } return LEGACY_BROWSER_CHUNK_SIZES.find( (chunkSize) => Math.ceil(fileSize / chunkSize) === totalChunks, ) || null } export function getExpectedUploadChunkBytes(input: { fileSize: number totalChunks: number chunkSize: number chunkIndex: number }): number | null { const { fileSize, totalChunks, chunkSize, chunkIndex } = input if ( !Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= totalChunks || resolveUploadChunkSize(fileSize, totalChunks, chunkSize) !== chunkSize ) { return null } const expectedBytes = chunkIndex === totalChunks - 1 ? fileSize - chunkSize * (totalChunks - 1) : chunkSize return Number.isSafeInteger(expectedBytes) && expectedBytes > 0 && expectedBytes <= MAX_UPLOAD_CHUNK_BYTES ? expectedBytes : null }