1- import { createReadStream , existsSync } from 'fs'
1+ import { existsSync } from 'fs'
2+ import { readFile } from 'fs/promises'
23import { Readable } from 'stream'
34import { createLogger } from '@sim/logger'
45import { type Options , parse } from 'csv-parse'
56import { FileParserError } from '@/lib/file-parsers/errors'
67import type { FileParseResult , FileParser } from '@/lib/file-parsers/types'
7- import { sanitizeTextForUTF8 , truncationNotice } from '@/lib/file-parsers/utils'
8+ import {
9+ type DecodedText ,
10+ decodeTextBuffer ,
11+ sanitizeTextForUTF8 ,
12+ truncationNotice ,
13+ } from '@/lib/file-parsers/utils'
814
915const logger = createLogger ( 'CsvParser' )
1016
1117const CONFIG = {
1218 MAX_PREVIEW_ROWS : 1000 , // Only keep first 1000 rows for preview
1319 MAX_SAMPLE_ROWS : 100 , // Sample for metadata
1420 MAX_ERRORS : 100 , // Stop after 100 errors
15- STREAM_CHUNK_SIZE : 16384 , // 16KB chunks for streaming
1621}
1722
1823export class CsvParser implements FileParser {
24+ /**
25+ * Reads the whole file before parsing rather than streaming 16 KB chunks:
26+ * encoding detection needs the complete byte sequence (a BOM-less UTF-16 or
27+ * Windows-1252 file cannot be recognized per chunk, and a multi-byte UTF-8
28+ * sequence split across chunk boundaries would be misread). The upload size
29+ * caps already bound the file, and `parseBuffer` — the production path —
30+ * always held the full buffer.
31+ */
1932 async parseFile ( filePath : string ) : Promise < FileParseResult > {
2033 if ( ! filePath ) {
2134 throw new Error ( 'No file path provided' )
@@ -25,11 +38,7 @@ export class CsvParser implements FileParser {
2538 throw new Error ( `File not found: ${ filePath } ` )
2639 }
2740
28- const stream = createReadStream ( filePath , {
29- highWaterMark : CONFIG . STREAM_CHUNK_SIZE ,
30- } )
31-
32- return this . parseStream ( stream )
41+ return this . parseBuffer ( await readFile ( filePath ) )
3342 }
3443
3544 async parseBuffer ( buffer : Buffer ) : Promise < FileParseResult > {
@@ -38,14 +47,18 @@ export class CsvParser implements FileParser {
3847 `Parsing CSV buffer, size: ${ bufferSize } bytes (${ ( bufferSize / 1024 / 1024 ) . toFixed ( 2 ) } MB)`
3948 )
4049
50+ const decoded = decodeTextBuffer ( buffer )
4151 const stream = new Readable ( { read ( ) { } } )
42- stream . push ( buffer )
52+ stream . push ( decoded . text )
4353 stream . push ( null )
4454
45- return this . parseStream ( stream )
55+ return this . parseStream ( stream , decoded )
4656 }
4757
48- private parseStream ( inputStream : NodeJS . ReadableStream ) : Promise < FileParseResult > {
58+ private parseStream (
59+ inputStream : NodeJS . ReadableStream ,
60+ decoded : DecodedText
61+ ) : Promise < FileParseResult > {
4962 return new Promise ( ( resolve , reject ) => {
5063 let rowCount = 0
5164 let errorCount = 0
@@ -145,6 +158,8 @@ export class CsvParser implements FileParser {
145158 errors : errors . slice ( 0 , 10 ) ,
146159 truncated : rowCount > CONFIG . MAX_PREVIEW_ROWS ,
147160 sampledData : sampledRows ,
161+ encoding : decoded . encoding ,
162+ ...( decoded . warning ? { warning : decoded . warning } : { } ) ,
148163 } ,
149164 } )
150165 }
0 commit comments