     1	/**
     2	 * Busboy File Detector for HTTP Multipart Uploads
     3	 *
     4	 * This detector handles multipart/form-data uploads using the busboy library.
     5	 * It's suitable for both HTTP and Express connectors.
     6	 *
     7	 * Features:
     8	 * - Streaming parser (memory efficient)
     9	 * - Configurable file size limits
    10	 * - Automatic field parsing
    11	 * - No temporary files by default (keeps in memory)
    12	 *
    13	 * Usage:
    14	 * ```javascript
    15	 * import { createBusboyDetector } from 'jsonrestapi/plugins/core/lib/busboy-detector.js';
    16	 *
    17	 * api.use(HttpPlugin, {
    18	 *   fileParser: 'busboy',
    19	 *   fileParserOptions: {
    20	 *     limits: { fileSize: 10 * 1024 * 1024 } // 10MB
    21	 *   }
    22	 * });
    23	 * ```
    24	 */
    25	
    26	import { requirePackage } from 'hooked-api'
    27	import { Readable } from 'stream'
    28	
    29	let Busboy
    30	try {
    31	  Busboy = (await import('busboy')).default
    32	} catch (e) {
    33	  requirePackage('busboy', 'express/http-connector',
    34	    'Busboy is required for multipart/form-data file uploads. This is a peer dependency.')
    35	}
    36	
    37	/**
    38	 * Creates a busboy-based file detector
    39	 *
    40	 * @param {Object} options - Busboy configuration options
    41	 * @param {Object} options.limits - Size limits
    42	 * @param {number} options.limits.fileSize - Max file size in bytes
    43	 * @param {number} options.limits.files - Max number of files
    44	 * @param {number} options.limits.fields - Max number of fields
    45	 * @returns {Object} Detector object with detect() and parse() methods
    46	 */
    47	export function createBusboyDetector (options = {}) {
    48	  return {
    49	    name: 'busboy-multipart',
    50	
    51	    /**
    52	     * Check if this detector can handle the request
    53	     * @param {Object} params - Request parameters
    54	     * @returns {boolean} True if this is a multipart request
    55	     */
    56	    detect: (params) => {
    57	      const req = params._httpReq || params._expressReq
    58	      if (!req || !req.headers) return false
    59	
    60	      const contentType = req.headers['content-type'] || ''
    61	      return contentType.includes('multipart/form-data')
    62	    },
    63	
    64	    /**
    65	     * Parse multipart data from the request
    66	     * @param {Object} params - Request parameters
    67	     * @returns {Promise<{fields: Object, files: Object}>} Parsed data
    68	     */
    69	    parse: async (params) => {
    70	      const req = params._httpReq || params._expressReq
    71	
    72	      return new Promise((resolve, reject) => {
    73	        const busboy = new Busboy({
    74	          headers: req.headers,
    75	          ...options
    76	        })
    77	
    78	        const fields = {}
    79	        const files = {}
    80	        const filePromises = []
    81	
    82	        // Handle fields
    83	        busboy.on('field', (fieldname, val) => {
    84	          // Handle array notation (field[] or field[0])
    85	          const arrayMatch = fieldname.match(/^(.+)\[\d*\]$/)
    86	          if (arrayMatch) {
    87	            const baseName = arrayMatch[1]
    88	            if (!fields[baseName]) {
    89	              fields[baseName] = []
    90	            }
    91	            if (Array.isArray(fields[baseName])) {
    92	              fields[baseName].push(val)
    93	            }
    94	          } else {
    95	            fields[fieldname] = val
    96	          }
    97	        })
    98	
    99	        // Handle files
   100	        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
   101	          const chunks = []
   102	          let size = 0
   103	
   104	          const filePromise = new Promise((fileResolve, fileReject) => {
   105	            file.on('data', (chunk) => {
   106	              chunks.push(chunk)
   107	              size += chunk.length
   108	            })
   109	
   110	            file.on('limit', () => {
   111	              fileReject(new Error(`File size limit exceeded for field '${fieldname}'`))
   112	            })
   113	
   114	            file.on('end', () => {
   115	              files[fieldname] = {
   116	                filename,
   117	                mimetype,
   118	                encoding,
   119	                size,
   120	                data: Buffer.concat(chunks)
   121	              }
   122	              fileResolve()
   123	            })
   124	
   125	            file.on('error', fileReject)
   126	          })
   127	
   128	          filePromises.push(filePromise)
   129	        })
   130	
   131	        // Handle completion
   132	        busboy.on('finish', async () => {
   133	          try {
   134	            // Wait for all files to be fully read
   135	            await Promise.all(filePromises)
   136	            resolve({ fields, files })
   137	          } catch (error) {
   138	            reject(error)
   139	          }
   140	        })
   141	
   142	        // Handle errors
   143	        busboy.on('error', (error) => {
   144	          reject(error)
   145	        })
   146	
   147	        // Handle limit errors
   148	        busboy.on('partsLimit', () => {
   149	          reject(new Error('Parts limit exceeded'))
   150	        })
   151	
   152	        busboy.on('filesLimit', () => {
   153	          reject(new Error('Files limit exceeded'))
   154	        })
   155	
   156	        busboy.on('fieldsLimit', () => {
   157	          reject(new Error('Fields limit exceeded'))
   158	        })
   159	
   160	        // Pipe request to busboy
   161	        if (req.pipe) {
   162	          req.pipe(busboy)
   163	        } else if (req.on) {
   164	          // For already buffered requests
   165	          const stream = new Readable()
   166	          stream.push(req.body || req)
   167	          stream.push(null)
   168	          stream.pipe(busboy)
   169	        } else {
   170	          reject(new Error('Request object is not a stream'))
   171	        }
   172	      })
   173	    }
   174	  }
   175	}
