1'use strict' 2 3const { Blob, File: NativeFile } = require('buffer') 4const { types } = require('util') 5const { kState } = require('./symbols') 6const { isBlobLike } = require('./util') 7const { webidl } = require('./webidl') 8const { parseMIMEType, serializeAMimeType } = require('./dataURL') 9const { kEnumerableProperty } = require('../core/util') 10const encoder = new TextEncoder() 11 12class File extends Blob { 13 constructor (fileBits, fileName, options = {}) { 14 // The File constructor is invoked with two or three parameters, depending 15 // on whether the optional dictionary parameter is used. When the File() 16 // constructor is invoked, user agents must run the following steps: 17 webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) 18 19 fileBits = webidl.converters['sequence<BlobPart>'](fileBits) 20 fileName = webidl.converters.USVString(fileName) 21 options = webidl.converters.FilePropertyBag(options) 22 23 // 1. Let bytes be the result of processing blob parts given fileBits and 24 // options. 25 // Note: Blob handles this for us 26 27 // 2. Let n be the fileName argument to the constructor. 28 const n = fileName 29 30 // 3. Process FilePropertyBag dictionary argument by running the following 31 // substeps: 32 33 // 1. If the type member is provided and is not the empty string, let t 34 // be set to the type dictionary member. If t contains any characters 35 // outside the range U+0020 to U+007E, then set t to the empty string 36 // and return from these substeps. 37 // 2. Convert every character in t to ASCII lowercase. 38 let t = options.type 39 let d 40 41 // eslint-disable-next-line no-labels 42 substep: { 43 if (t) { 44 t = parseMIMEType(t) 45 46 if (t === 'failure') { 47 t = '' 48 // eslint-disable-next-line no-labels 49 break substep 50 } 51 52 t = serializeAMimeType(t).toLowerCase() 53 } 54 55 // 3. If the lastModified member is provided, let d be set to the 56 // lastModified dictionary member. If it is not provided, set d to the 57 // current date and time represented as the number of milliseconds since 58 // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). 59 d = options.lastModified 60 } 61 62 // 4. Return a new File object F such that: 63 // F refers to the bytes byte sequence. 64 // F.size is set to the number of total bytes in bytes. 65 // F.name is set to n. 66 // F.type is set to t. 67 // F.lastModified is set to d. 68 69 super(processBlobParts(fileBits, options), { type: t }) 70 this[kState] = { 71 name: n, 72 lastModified: d, 73 type: t 74 } 75 } 76 77 get name () { 78 webidl.brandCheck(this, File) 79 80 return this[kState].name 81 } 82 83 get lastModified () { 84 webidl.brandCheck(this, File) 85 86 return this[kState].lastModified 87 } 88 89 get type () { 90 webidl.brandCheck(this, File) 91 92 return this[kState].type 93 } 94} 95 96class FileLike { 97 constructor (blobLike, fileName, options = {}) { 98 // TODO: argument idl type check 99 100 // The File constructor is invoked with two or three parameters, depending 101 // on whether the optional dictionary parameter is used. When the File() 102 // constructor is invoked, user agents must run the following steps: 103 104 // 1. Let bytes be the result of processing blob parts given fileBits and 105 // options. 106 107 // 2. Let n be the fileName argument to the constructor. 108 const n = fileName 109 110 // 3. Process FilePropertyBag dictionary argument by running the following 111 // substeps: 112 113 // 1. If the type member is provided and is not the empty string, let t 114 // be set to the type dictionary member. If t contains any characters 115 // outside the range U+0020 to U+007E, then set t to the empty string 116 // and return from these substeps. 117 // TODO 118 const t = options.type 119 120 // 2. Convert every character in t to ASCII lowercase. 121 // TODO 122 123 // 3. If the lastModified member is provided, let d be set to the 124 // lastModified dictionary member. If it is not provided, set d to the 125 // current date and time represented as the number of milliseconds since 126 // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). 127 const d = options.lastModified ?? Date.now() 128 129 // 4. Return a new File object F such that: 130 // F refers to the bytes byte sequence. 131 // F.size is set to the number of total bytes in bytes. 132 // F.name is set to n. 133 // F.type is set to t. 134 // F.lastModified is set to d. 135 136 this[kState] = { 137 blobLike, 138 name: n, 139 type: t, 140 lastModified: d 141 } 142 } 143 144 stream (...args) { 145 webidl.brandCheck(this, FileLike) 146 147 return this[kState].blobLike.stream(...args) 148 } 149 150 arrayBuffer (...args) { 151 webidl.brandCheck(this, FileLike) 152 153 return this[kState].blobLike.arrayBuffer(...args) 154 } 155 156 slice (...args) { 157 webidl.brandCheck(this, FileLike) 158 159 return this[kState].blobLike.slice(...args) 160 } 161 162 text (...args) { 163 webidl.brandCheck(this, FileLike) 164 165 return this[kState].blobLike.text(...args) 166 } 167 168 get size () { 169 webidl.brandCheck(this, FileLike) 170 171 return this[kState].blobLike.size 172 } 173 174 get type () { 175 webidl.brandCheck(this, FileLike) 176 177 return this[kState].blobLike.type 178 } 179 180 get name () { 181 webidl.brandCheck(this, FileLike) 182 183 return this[kState].name 184 } 185 186 get lastModified () { 187 webidl.brandCheck(this, FileLike) 188 189 return this[kState].lastModified 190 } 191 192 get [Symbol.toStringTag] () { 193 return 'File' 194 } 195} 196 197Object.defineProperties(File.prototype, { 198 [Symbol.toStringTag]: { 199 value: 'File', 200 configurable: true 201 }, 202 name: kEnumerableProperty, 203 lastModified: kEnumerableProperty 204}) 205 206webidl.converters.Blob = webidl.interfaceConverter(Blob) 207 208webidl.converters.BlobPart = function (V, opts) { 209 if (webidl.util.Type(V) === 'Object') { 210 if (isBlobLike(V)) { 211 return webidl.converters.Blob(V, { strict: false }) 212 } 213 214 if ( 215 ArrayBuffer.isView(V) || 216 types.isAnyArrayBuffer(V) 217 ) { 218 return webidl.converters.BufferSource(V, opts) 219 } 220 } 221 222 return webidl.converters.USVString(V, opts) 223} 224 225webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter( 226 webidl.converters.BlobPart 227) 228 229// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag 230webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ 231 { 232 key: 'lastModified', 233 converter: webidl.converters['long long'], 234 get defaultValue () { 235 return Date.now() 236 } 237 }, 238 { 239 key: 'type', 240 converter: webidl.converters.DOMString, 241 defaultValue: '' 242 }, 243 { 244 key: 'endings', 245 converter: (value) => { 246 value = webidl.converters.DOMString(value) 247 value = value.toLowerCase() 248 249 if (value !== 'native') { 250 value = 'transparent' 251 } 252 253 return value 254 }, 255 defaultValue: 'transparent' 256 } 257]) 258 259/** 260 * @see https://www.w3.org/TR/FileAPI/#process-blob-parts 261 * @param {(NodeJS.TypedArray|Blob|string)[]} parts 262 * @param {{ type: string, endings: string }} options 263 */ 264function processBlobParts (parts, options) { 265 // 1. Let bytes be an empty sequence of bytes. 266 /** @type {NodeJS.TypedArray[]} */ 267 const bytes = [] 268 269 // 2. For each element in parts: 270 for (const element of parts) { 271 // 1. If element is a USVString, run the following substeps: 272 if (typeof element === 'string') { 273 // 1. Let s be element. 274 let s = element 275 276 // 2. If the endings member of options is "native", set s 277 // to the result of converting line endings to native 278 // of element. 279 if (options.endings === 'native') { 280 s = convertLineEndingsNative(s) 281 } 282 283 // 3. Append the result of UTF-8 encoding s to bytes. 284 bytes.push(encoder.encode(s)) 285 } else if ( 286 types.isAnyArrayBuffer(element) || 287 types.isTypedArray(element) 288 ) { 289 // 2. If element is a BufferSource, get a copy of the 290 // bytes held by the buffer source, and append those 291 // bytes to bytes. 292 if (!element.buffer) { // ArrayBuffer 293 bytes.push(new Uint8Array(element)) 294 } else { 295 bytes.push( 296 new Uint8Array(element.buffer, element.byteOffset, element.byteLength) 297 ) 298 } 299 } else if (isBlobLike(element)) { 300 // 3. If element is a Blob, append the bytes it represents 301 // to bytes. 302 bytes.push(element) 303 } 304 } 305 306 // 3. Return bytes. 307 return bytes 308} 309 310/** 311 * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native 312 * @param {string} s 313 */ 314function convertLineEndingsNative (s) { 315 // 1. Let native line ending be be the code point U+000A LF. 316 let nativeLineEnding = '\n' 317 318 // 2. If the underlying platform’s conventions are to 319 // represent newlines as a carriage return and line feed 320 // sequence, set native line ending to the code point 321 // U+000D CR followed by the code point U+000A LF. 322 if (process.platform === 'win32') { 323 nativeLineEnding = '\r\n' 324 } 325 326 return s.replace(/\r?\n/g, nativeLineEnding) 327} 328 329// If this function is moved to ./util.js, some tools (such as 330// rollup) will warn about circular dependencies. See: 331// https://github.com/nodejs/undici/issues/1629 332function isFileLike (object) { 333 return ( 334 (NativeFile && object instanceof NativeFile) || 335 object instanceof File || ( 336 object && 337 (typeof object.stream === 'function' || 338 typeof object.arrayBuffer === 'function') && 339 object[Symbol.toStringTag] === 'File' 340 ) 341 ) 342} 343 344module.exports = { File, FileLike, isFileLike } 345