From 12187efdbd968b90a340f77c4b433f9b4220cb20 Mon Sep 17 00:00:00 2001 From: thepheer <5144598+thepheer@users.noreply.github.com> Date: Fri, 17 Apr 2020 13:03:50 +0300 Subject: [PATCH] feat(DataResolver): prefer streams over buffers (#4075) * feat(DataResolver): prefer streams over buffers * feat(DataResolver): add `resolveFileAsBuffer` Add `resolveFileAsBuffer` to use it in `resolveImage` which still requires Buffers to work. * fix(DataResolver): make sure `resolveFile` always returns a Promise * refactor(DataResolver): use for-await-of * fix(DataResolver): use forked form-data which supports custom streams * fix(APIRequest): use forked form-data in code too Co-authored-by: - <5144598+-@users.noreply.github.com> Co-authored-by: SpaceEEC --- package.json | 2 +- src/rest/APIRequest.js | 2 +- src/util/DataResolver.js | 68 ++++++++++++++++++++++------------------ typings/index.d.ts | 7 +++-- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 4eed0b3c7..1009ed7db 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "unpkg": "./webpack/discord.min.js", "dependencies": { "@discordjs/collection": "^0.1.5", + "@discordjs/form-data": "^3.0.1", "abort-controller": "^3.0.0", - "form-data": "^3.0.0", "node-fetch": "^2.6.0", "prism-media": "^1.2.0", "setimmediate": "^1.0.5", diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 3a9d6294a..36ad0d72c 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,8 +1,8 @@ 'use strict'; const https = require('https'); +const FormData = require('@discordjs/form-data'); const AbortController = require('abort-controller'); -const FormData = require('form-data'); const fetch = require('node-fetch'); const { browser, UserAgent } = require('../util/Constants'); diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index e5606f3f9..71f690c9a 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); +const stream = require('stream'); const fetch = require('node-fetch'); const { Error: DiscordError, TypeError } = require('../errors'); const { browser } = require('../util/Constants'); @@ -45,7 +46,7 @@ class DataResolver { if (typeof image === 'string' && image.startsWith('data:')) { return image; } - const file = await this.resolveFile(image); + const file = await this.resolveFileAsBuffer(image); return DataResolver.resolveBase64(file); } @@ -79,42 +80,47 @@ class DataResolver { * @see {@link https://nodejs.org/api/stream.html} */ + /** + * Resolves a BufferResolvable to a Buffer or a Stream. + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve + * @returns {Promise} + */ + static async resolveFile(resource) { + if (!browser && Buffer.isBuffer(resource)) return resource; + if (browser && resource instanceof ArrayBuffer) return Util.convertToBuffer(resource); + if (resource instanceof stream.Readable) return resource; + + if (typeof resource === 'string') { + if (/^https?:\/\//.test(resource)) { + const res = await fetch(resource); + return browser ? res.blob() : res.body; + } else if (!browser) { + return new Promise((resolve, reject) => { + const file = path.resolve(resource); + fs.stat(file, (err, stats) => { + if (err) return reject(err); + if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); + return resolve(fs.createReadStream(file)); + }); + }); + } + } + + throw new TypeError('REQ_RESOURCE_TYPE'); + } + /** * Resolves a BufferResolvable to a Buffer. * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve * @returns {Promise} */ - static resolveFile(resource) { - if (!browser && Buffer.isBuffer(resource)) return Promise.resolve(resource); - if (browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); + static async resolveFileAsBuffer(resource) { + const file = await this.resolveFile(resource); + if (Buffer.isBuffer(file)) return file; - if (typeof resource === 'string') { - if (/^https?:\/\//.test(resource)) { - return fetch(resource).then(res => (browser ? res.blob() : res.buffer())); - } else if (!browser) { - return new Promise((resolve, reject) => { - const file = browser ? resource : path.resolve(resource); - fs.stat(file, (err, stats) => { - if (err) return reject(err); - if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); - fs.readFile(file, (err2, data) => { - if (err2) reject(err2); - else resolve(data); - }); - return null; - }); - }); - } - } else if (typeof resource.pipe === 'function') { - return new Promise((resolve, reject) => { - const buffers = []; - resource.once('error', reject); - resource.on('data', data => buffers.push(data)); - resource.once('end', () => resolve(Buffer.concat(buffers))); - }); - } - - return Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); + const buffers = []; + for await (const data of file) buffers.push(data); + return Buffer.concat(buffers); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 2f18caf1c..0470ca65f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -11,10 +11,10 @@ declare enum ChannelType { declare module 'discord.js' { import BaseCollection from '@discordjs/collection'; - import { EventEmitter } from 'events'; - import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; + import { EventEmitter } from 'events'; import { PathLike } from 'fs'; + import { Readable, Stream, Writable } from 'stream'; import * as WebSocket from 'ws'; export const version: string; @@ -539,7 +539,8 @@ declare module 'discord.js' { export class DataResolver { public static resolveBase64(data: Base64Resolvable): string; - public static resolveFile(resource: BufferResolvable | Stream): Promise; + public static resolveFile(resource: BufferResolvable | Stream): Promise; + public static resolveFileAsBuffer(resource: BufferResolvable | Stream): Promise; public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise; public static resolveInviteCode(data: InviteResolvable): string; }