1 | 'use strict'
|
---|
2 |
|
---|
3 | const { Blob, File: NativeFile } = require('buffer')
|
---|
4 | const { types } = require('util')
|
---|
5 | const { kState } = require('./symbols')
|
---|
6 | const { isBlobLike } = require('./util')
|
---|
7 | const { webidl } = require('./webidl')
|
---|
8 | const { parseMIMEType, serializeAMimeType } = require('./dataURL')
|
---|
9 | const { kEnumerableProperty } = require('../core/util')
|
---|
10 | const encoder = new TextEncoder()
|
---|
11 |
|
---|
12 | class 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 |
|
---|
96 | class 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 |
|
---|
197 | Object.defineProperties(File.prototype, {
|
---|
198 | [Symbol.toStringTag]: {
|
---|
199 | value: 'File',
|
---|
200 | configurable: true
|
---|
201 | },
|
---|
202 | name: kEnumerableProperty,
|
---|
203 | lastModified: kEnumerableProperty
|
---|
204 | })
|
---|
205 |
|
---|
206 | webidl.converters.Blob = webidl.interfaceConverter(Blob)
|
---|
207 |
|
---|
208 | webidl.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 |
|
---|
225 | webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
|
---|
226 | webidl.converters.BlobPart
|
---|
227 | )
|
---|
228 |
|
---|
229 | // https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
|
---|
230 | webidl.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 | */
|
---|
264 | function 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 | */
|
---|
314 | function 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
|
---|
332 | function 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 |
|
---|
344 | module.exports = { File, FileLike, isFileLike }
|
---|