You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					112 lines
				
				2.8 KiB
			
		
		
			
		
	
	
					112 lines
				
				2.8 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								import {TextEncoder} from 'util';
							 | 
						||
| 
								 | 
							
								import {Readable} from 'stream';
							 | 
						||
| 
								 | 
							
								import utils from "../utils.js";
							 | 
						||
| 
								 | 
							
								import readBlob from "./readBlob.js";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const BOUNDARY_ALPHABET = utils.ALPHABET.ALPHA_DIGIT + '-_';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const textEncoder = new TextEncoder();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const CRLF = '\r\n';
							 | 
						||
| 
								 | 
							
								const CRLF_BYTES = textEncoder.encode(CRLF);
							 | 
						||
| 
								 | 
							
								const CRLF_BYTES_COUNT = 2;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class FormDataPart {
							 | 
						||
| 
								 | 
							
								  constructor(name, value) {
							 | 
						||
| 
								 | 
							
								    const {escapeName} = this.constructor;
							 | 
						||
| 
								 | 
							
								    const isStringValue = utils.isString(value);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let headers = `Content-Disposition: form-data; name="${escapeName(name)}"${
							 | 
						||
| 
								 | 
							
								      !isStringValue && value.name ? `; filename="${escapeName(value.name)}"` : ''
							 | 
						||
| 
								 | 
							
								    }${CRLF}`;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (isStringValue) {
							 | 
						||
| 
								 | 
							
								      value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF));
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      headers += `Content-Type: ${value.type || "application/octet-stream"}${CRLF}`
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.headers = textEncoder.encode(headers + CRLF);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.contentLength = isStringValue ? value.byteLength : value.size;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.size = this.headers.byteLength + this.contentLength + CRLF_BYTES_COUNT;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.name = name;
							 | 
						||
| 
								 | 
							
								    this.value = value;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  async *encode(){
							 | 
						||
| 
								 | 
							
								    yield this.headers;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const {value} = this;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if(utils.isTypedArray(value)) {
							 | 
						||
| 
								 | 
							
								      yield value;
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      yield* readBlob(value);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    yield CRLF_BYTES;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  static escapeName(name) {
							 | 
						||
| 
								 | 
							
								      return String(name).replace(/[\r\n"]/g, (match) => ({
							 | 
						||
| 
								 | 
							
								        '\r' : '%0D',
							 | 
						||
| 
								 | 
							
								        '\n' : '%0A',
							 | 
						||
| 
								 | 
							
								        '"' : '%22',
							 | 
						||
| 
								 | 
							
								      }[match]));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const formDataToStream = (form, headersHandler, options) => {
							 | 
						||
| 
								 | 
							
								  const {
							 | 
						||
| 
								 | 
							
								    tag = 'form-data-boundary',
							 | 
						||
| 
								 | 
							
								    size = 25,
							 | 
						||
| 
								 | 
							
								    boundary = tag + '-' + utils.generateString(size, BOUNDARY_ALPHABET)
							 | 
						||
| 
								 | 
							
								  } = options || {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if(!utils.isFormData(form)) {
							 | 
						||
| 
								 | 
							
								    throw TypeError('FormData instance required');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (boundary.length < 1 || boundary.length > 70) {
							 | 
						||
| 
								 | 
							
								    throw Error('boundary must be 10-70 characters long')
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
							 | 
						||
| 
								 | 
							
								  const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF + CRLF);
							 | 
						||
| 
								 | 
							
								  let contentLength = footerBytes.byteLength;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const parts = Array.from(form.entries()).map(([name, value]) => {
							 | 
						||
| 
								 | 
							
								    const part = new FormDataPart(name, value);
							 | 
						||
| 
								 | 
							
								    contentLength += part.size;
							 | 
						||
| 
								 | 
							
								    return part;
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  contentLength += boundaryBytes.byteLength * parts.length;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  contentLength = utils.toFiniteNumber(contentLength);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const computedHeaders = {
							 | 
						||
| 
								 | 
							
								    'Content-Type': `multipart/form-data; boundary=${boundary}`
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (Number.isFinite(contentLength)) {
							 | 
						||
| 
								 | 
							
								    computedHeaders['Content-Length'] = contentLength;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  headersHandler && headersHandler(computedHeaders);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return Readable.from((async function *() {
							 | 
						||
| 
								 | 
							
								    for(const part of parts) {
							 | 
						||
| 
								 | 
							
								      yield boundaryBytes;
							 | 
						||
| 
								 | 
							
								      yield* part.encode();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    yield footerBytes;
							 | 
						||
| 
								 | 
							
								  })());
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export default formDataToStream;
							 |