344 lines
9.3 KiB
JavaScript
344 lines
9.3 KiB
JavaScript
'use strict'
|
||
|
||
const { Blob, File: NativeFile } = require('buffer')
|
||
const { types } = require('util')
|
||
const { kState } = require('./symbols')
|
||
const { isBlobLike } = require('./util')
|
||
const { webidl } = require('./webidl')
|
||
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
|
||
const { kEnumerableProperty } = require('../core/util')
|
||
const encoder = new TextEncoder()
|
||
|
||
class File extends Blob {
|
||
constructor (fileBits, fileName, options = {}) {
|
||
// The File constructor is invoked with two or three parameters, depending
|
||
// on whether the optional dictionary parameter is used. When the File()
|
||
// constructor is invoked, user agents must run the following steps:
|
||
webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
|
||
|
||
fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
|
||
fileName = webidl.converters.USVString(fileName)
|
||
options = webidl.converters.FilePropertyBag(options)
|
||
|
||
// 1. Let bytes be the result of processing blob parts given fileBits and
|
||
// options.
|
||
// Note: Blob handles this for us
|
||
|
||
// 2. Let n be the fileName argument to the constructor.
|
||
const n = fileName
|
||
|
||
// 3. Process FilePropertyBag dictionary argument by running the following
|
||
// substeps:
|
||
|
||
// 1. If the type member is provided and is not the empty string, let t
|
||
// be set to the type dictionary member. If t contains any characters
|
||
// outside the range U+0020 to U+007E, then set t to the empty string
|
||
// and return from these substeps.
|
||
// 2. Convert every character in t to ASCII lowercase.
|
||
let t = options.type
|
||
let d
|
||
|
||
// eslint-disable-next-line no-labels
|
||
substep: {
|
||
if (t) {
|
||
t = parseMIMEType(t)
|
||
|
||
if (t === 'failure') {
|
||
t = ''
|
||
// eslint-disable-next-line no-labels
|
||
break substep
|
||
}
|
||
|
||
t = serializeAMimeType(t).toLowerCase()
|
||
}
|
||
|
||
// 3. If the lastModified member is provided, let d be set to the
|
||
// lastModified dictionary member. If it is not provided, set d to the
|
||
// current date and time represented as the number of milliseconds since
|
||
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
|
||
d = options.lastModified
|
||
}
|
||
|
||
// 4. Return a new File object F such that:
|
||
// F refers to the bytes byte sequence.
|
||
// F.size is set to the number of total bytes in bytes.
|
||
// F.name is set to n.
|
||
// F.type is set to t.
|
||
// F.lastModified is set to d.
|
||
|
||
super(processBlobParts(fileBits, options), { type: t })
|
||
this[kState] = {
|
||
name: n,
|
||
lastModified: d,
|
||
type: t
|
||
}
|
||
}
|
||
|
||
get name () {
|
||
webidl.brandCheck(this, File)
|
||
|
||
return this[kState].name
|
||
}
|
||
|
||
get lastModified () {
|
||
webidl.brandCheck(this, File)
|
||
|
||
return this[kState].lastModified
|
||
}
|
||
|
||
get type () {
|
||
webidl.brandCheck(this, File)
|
||
|
||
return this[kState].type
|
||
}
|
||
}
|
||
|
||
class FileLike {
|
||
constructor (blobLike, fileName, options = {}) {
|
||
// TODO: argument idl type check
|
||
|
||
// The File constructor is invoked with two or three parameters, depending
|
||
// on whether the optional dictionary parameter is used. When the File()
|
||
// constructor is invoked, user agents must run the following steps:
|
||
|
||
// 1. Let bytes be the result of processing blob parts given fileBits and
|
||
// options.
|
||
|
||
// 2. Let n be the fileName argument to the constructor.
|
||
const n = fileName
|
||
|
||
// 3. Process FilePropertyBag dictionary argument by running the following
|
||
// substeps:
|
||
|
||
// 1. If the type member is provided and is not the empty string, let t
|
||
// be set to the type dictionary member. If t contains any characters
|
||
// outside the range U+0020 to U+007E, then set t to the empty string
|
||
// and return from these substeps.
|
||
// TODO
|
||
const t = options.type
|
||
|
||
// 2. Convert every character in t to ASCII lowercase.
|
||
// TODO
|
||
|
||
// 3. If the lastModified member is provided, let d be set to the
|
||
// lastModified dictionary member. If it is not provided, set d to the
|
||
// current date and time represented as the number of milliseconds since
|
||
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
|
||
const d = options.lastModified ?? Date.now()
|
||
|
||
// 4. Return a new File object F such that:
|
||
// F refers to the bytes byte sequence.
|
||
// F.size is set to the number of total bytes in bytes.
|
||
// F.name is set to n.
|
||
// F.type is set to t.
|
||
// F.lastModified is set to d.
|
||
|
||
this[kState] = {
|
||
blobLike,
|
||
name: n,
|
||
type: t,
|
||
lastModified: d
|
||
}
|
||
}
|
||
|
||
stream (...args) {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.stream(...args)
|
||
}
|
||
|
||
arrayBuffer (...args) {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.arrayBuffer(...args)
|
||
}
|
||
|
||
slice (...args) {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.slice(...args)
|
||
}
|
||
|
||
text (...args) {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.text(...args)
|
||
}
|
||
|
||
get size () {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.size
|
||
}
|
||
|
||
get type () {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].blobLike.type
|
||
}
|
||
|
||
get name () {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].name
|
||
}
|
||
|
||
get lastModified () {
|
||
webidl.brandCheck(this, FileLike)
|
||
|
||
return this[kState].lastModified
|
||
}
|
||
|
||
get [Symbol.toStringTag] () {
|
||
return 'File'
|
||
}
|
||
}
|
||
|
||
Object.defineProperties(File.prototype, {
|
||
[Symbol.toStringTag]: {
|
||
value: 'File',
|
||
configurable: true
|
||
},
|
||
name: kEnumerableProperty,
|
||
lastModified: kEnumerableProperty
|
||
})
|
||
|
||
webidl.converters.Blob = webidl.interfaceConverter(Blob)
|
||
|
||
webidl.converters.BlobPart = function (V, opts) {
|
||
if (webidl.util.Type(V) === 'Object') {
|
||
if (isBlobLike(V)) {
|
||
return webidl.converters.Blob(V, { strict: false })
|
||
}
|
||
|
||
if (
|
||
ArrayBuffer.isView(V) ||
|
||
types.isAnyArrayBuffer(V)
|
||
) {
|
||
return webidl.converters.BufferSource(V, opts)
|
||
}
|
||
}
|
||
|
||
return webidl.converters.USVString(V, opts)
|
||
}
|
||
|
||
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
|
||
webidl.converters.BlobPart
|
||
)
|
||
|
||
// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
|
||
webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
|
||
{
|
||
key: 'lastModified',
|
||
converter: webidl.converters['long long'],
|
||
get defaultValue () {
|
||
return Date.now()
|
||
}
|
||
},
|
||
{
|
||
key: 'type',
|
||
converter: webidl.converters.DOMString,
|
||
defaultValue: ''
|
||
},
|
||
{
|
||
key: 'endings',
|
||
converter: (value) => {
|
||
value = webidl.converters.DOMString(value)
|
||
value = value.toLowerCase()
|
||
|
||
if (value !== 'native') {
|
||
value = 'transparent'
|
||
}
|
||
|
||
return value
|
||
},
|
||
defaultValue: 'transparent'
|
||
}
|
||
])
|
||
|
||
/**
|
||
* @see https://www.w3.org/TR/FileAPI/#process-blob-parts
|
||
* @param {(NodeJS.TypedArray|Blob|string)[]} parts
|
||
* @param {{ type: string, endings: string }} options
|
||
*/
|
||
function processBlobParts (parts, options) {
|
||
// 1. Let bytes be an empty sequence of bytes.
|
||
/** @type {NodeJS.TypedArray[]} */
|
||
const bytes = []
|
||
|
||
// 2. For each element in parts:
|
||
for (const element of parts) {
|
||
// 1. If element is a USVString, run the following substeps:
|
||
if (typeof element === 'string') {
|
||
// 1. Let s be element.
|
||
let s = element
|
||
|
||
// 2. If the endings member of options is "native", set s
|
||
// to the result of converting line endings to native
|
||
// of element.
|
||
if (options.endings === 'native') {
|
||
s = convertLineEndingsNative(s)
|
||
}
|
||
|
||
// 3. Append the result of UTF-8 encoding s to bytes.
|
||
bytes.push(encoder.encode(s))
|
||
} else if (
|
||
types.isAnyArrayBuffer(element) ||
|
||
types.isTypedArray(element)
|
||
) {
|
||
// 2. If element is a BufferSource, get a copy of the
|
||
// bytes held by the buffer source, and append those
|
||
// bytes to bytes.
|
||
if (!element.buffer) { // ArrayBuffer
|
||
bytes.push(new Uint8Array(element))
|
||
} else {
|
||
bytes.push(
|
||
new Uint8Array(element.buffer, element.byteOffset, element.byteLength)
|
||
)
|
||
}
|
||
} else if (isBlobLike(element)) {
|
||
// 3. If element is a Blob, append the bytes it represents
|
||
// to bytes.
|
||
bytes.push(element)
|
||
}
|
||
}
|
||
|
||
// 3. Return bytes.
|
||
return bytes
|
||
}
|
||
|
||
/**
|
||
* @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
|
||
* @param {string} s
|
||
*/
|
||
function convertLineEndingsNative (s) {
|
||
// 1. Let native line ending be be the code point U+000A LF.
|
||
let nativeLineEnding = '\n'
|
||
|
||
// 2. If the underlying platform’s conventions are to
|
||
// represent newlines as a carriage return and line feed
|
||
// sequence, set native line ending to the code point
|
||
// U+000D CR followed by the code point U+000A LF.
|
||
if (process.platform === 'win32') {
|
||
nativeLineEnding = '\r\n'
|
||
}
|
||
|
||
return s.replace(/\r?\n/g, nativeLineEnding)
|
||
}
|
||
|
||
// If this function is moved to ./util.js, some tools (such as
|
||
// rollup) will warn about circular dependencies. See:
|
||
// https://github.com/nodejs/undici/issues/1629
|
||
function isFileLike (object) {
|
||
return (
|
||
(NativeFile && object instanceof NativeFile) ||
|
||
object instanceof File || (
|
||
object &&
|
||
(typeof object.stream === 'function' ||
|
||
typeof object.arrayBuffer === 'function') &&
|
||
object[Symbol.toStringTag] === 'File'
|
||
)
|
||
)
|
||
}
|
||
|
||
module.exports = { File, FileLike, isFileLike }
|