From 103a3584c95a7b7f57fa62d47b86520d5ec32303 Mon Sep 17 00:00:00 2001 From: DD Date: Sun, 17 Jul 2022 19:55:25 +0300 Subject: [PATCH] refactor(rest): add content-type(s) to uploads (#8290) --- .../src/structures/MessagePayload.js | 4 +- packages/discord.js/src/util/DataResolver.js | 17 ++++-- packages/discord.js/typings/index.d.ts | 7 ++- packages/rest/package.json | 1 + packages/rest/src/lib/RequestManager.ts | 19 ++++++- yarn.lock | 57 ++++++++++++++++++- 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 896df62d2..4a1239471 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -255,8 +255,8 @@ class MessagePayload { name = fileLike.name ?? findName(attachment); } - const data = await DataResolver.resolveFile(attachment); - return { data, name }; + const { data, contentType } = await DataResolver.resolveFile(attachment); + return { data, name, contentType }; } /** diff --git a/packages/discord.js/src/util/DataResolver.js b/packages/discord.js/src/util/DataResolver.js index bfc0f6afc..1ece0660d 100644 --- a/packages/discord.js/src/util/DataResolver.js +++ b/packages/discord.js/src/util/DataResolver.js @@ -100,32 +100,37 @@ class DataResolver extends null { * @see {@link https://nodejs.org/api/stream.html} */ + /** + * @typedef {Object} ResolvedFile + * @property {Buffer} data Buffer containing the file data + * @property {string} [contentType] Content type of the file + */ + /** * Resolves a BufferResolvable to a Buffer. * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve - * @returns {Promise} + * @returns {Promise} */ static async resolveFile(resource) { - if (!resource) return null; - if (Buffer.isBuffer(resource)) return resource; + if (Buffer.isBuffer(resource)) return { data: resource }; if (typeof resource[Symbol.asyncIterator] === 'function') { const buffers = []; for await (const data of resource) buffers.push(data); - return Buffer.concat(buffers); + return { data: Buffer.concat(buffers) }; } if (typeof resource === 'string') { if (/^https?:\/\//.test(resource)) { const res = await fetch(resource); - return Buffer.from(await res.arrayBuffer()); + return { data: Buffer.from(await res.arrayBuffer()), contentType: res.headers.get('content-type') }; } const file = path.resolve(resource); const stats = await fs.stat(file); if (!stats.isFile()) throw new DiscordError(ErrorCodes.FileNotFound, file); - return fs.readFile(file); + return { data: await fs.readFile(file) }; } throw new TypeError(ErrorCodes.ReqResourceType); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 9ad8ba8e1..83a412a29 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1039,11 +1039,16 @@ export class ContextMenuCommandInteraction private resolveContextMenuOptions(data: APIApplicationCommandInteractionData): CommandInteractionOption[]; } +export interface ResolvedFile { + data: Buffer; + contentType?: string; +} + export class DataResolver extends null { private constructor(); public static resolveBase64(data: Base64Resolvable): string; public static resolveCode(data: string, regex: RegExp): string; - public static resolveFile(resource: BufferResolvable | Stream): Promise; + public static resolveFile(resource: BufferResolvable | Stream): Promise; public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise; public static resolveInviteCode(data: InviteResolvable): string; public static resolveGuildTemplateCode(data: GuildTemplateResolvable): string; diff --git a/packages/rest/package.json b/packages/rest/package.json index ca3a0fac3..136ddb49e 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -55,6 +55,7 @@ "@sapphire/async-queue": "^1.3.2", "@sapphire/snowflake": "^3.2.2", "discord-api-types": "^0.36.1", + "file-type": "^17.1.2", "tslib": "^2.4.0", "undici": "^5.6.0" }, diff --git a/packages/rest/src/lib/RequestManager.ts b/packages/rest/src/lib/RequestManager.ts index fb2e8df2f..bdc0364b2 100644 --- a/packages/rest/src/lib/RequestManager.ts +++ b/packages/rest/src/lib/RequestManager.ts @@ -9,6 +9,12 @@ import { SequentialHandler } from './handlers/SequentialHandler'; import { DefaultRestOptions, DefaultUserAgent, RESTEvents } from './utils/constants'; import { resolveBody } from './utils/utils'; +// Make this a lazy dynamic import as file-type is a pure ESM package +const getFileType = (): Promise => { + let cached: Promise; + return (cached ??= import('file-type')); +}; + /** * Represents a file to be added to the request */ @@ -27,6 +33,10 @@ export interface RawFile { * The actual data for the file */ data: string | number | boolean | Buffer; + /** + * Content-Type of the file + */ + contentType?: string; } /** @@ -401,11 +411,14 @@ export class RequestManager extends EventEmitter { // FormData.append only accepts a string or Blob. // https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#parameters // The Blob constructor accepts TypedArray/ArrayBuffer, strings, and Blobs. - if (Buffer.isBuffer(file.data) || typeof file.data === 'string') { - formData.append(fileKey, new Blob([file.data]), file.name); + if (Buffer.isBuffer(file.data)) { + // Try to infer the content type from the buffer if one isn't passed + const { fileTypeFromBuffer } = await getFileType(); + const contentType = file.contentType ?? (await fileTypeFromBuffer(file.data))?.mime; + formData.append(fileKey, new Blob([file.data], { type: contentType }), file.name); } else { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - formData.append(fileKey, new Blob([`${file.data}`]), file.name); + formData.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.name); } } diff --git a/yarn.lock b/yarn.lock index d1c8ef433..ad5431ffa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3003,6 +3003,7 @@ __metadata: c8: ^7.11.3 discord-api-types: ^0.36.1 eslint: ^8.19.0 + file-type: ^17.1.2 prettier: ^2.7.1 tslib: ^2.4.0 tsup: ^6.1.3 @@ -4188,6 +4189,13 @@ __metadata: languageName: node linkType: hard +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1 + languageName: node + linkType: hard + "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -9702,6 +9710,17 @@ dts-critic@latest: languageName: node linkType: hard +"file-type@npm:^17.1.2": + version: 17.1.2 + resolution: "file-type@npm:17.1.2" + dependencies: + readable-web-to-node-stream: ^3.0.2 + strtok3: ^7.0.0-alpha.7 + token-types: ^5.0.0-alpha.2 + checksum: 22103084b47d1fdc82e84b979512a2e9e488643f975b04cfd39acb2a9ab212438274a4f06039061631ca01be030f174c387c4a3ab9fe3417a1a199cb59079cb8 + languageName: node + linkType: hard + "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -10997,7 +11016,7 @@ dts-critic@latest: languageName: node linkType: hard -"ieee754@npm:^1.1.13": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e @@ -15009,6 +15028,13 @@ dts-critic@latest: languageName: node linkType: hard +"peek-readable@npm:^5.0.0-alpha.5": + version: 5.0.0-alpha.5 + resolution: "peek-readable@npm:5.0.0-alpha.5" + checksum: cab949ed457dac95ae191dd412c6a0ba05e8db4842fd51704ccf2c8c16d6f3ceeefc997e8caea584a0395f229e468c0203a38a8d0ec68cfef8bacc157a006dcb + languageName: node + linkType: hard + "peek-stream@npm:^1.1.0": version: 1.1.3 resolution: "peek-stream@npm:1.1.3" @@ -15671,6 +15697,15 @@ dts-critic@latest: languageName: node linkType: hard +"readable-web-to-node-stream@npm:^3.0.2": + version: 3.0.2 + resolution: "readable-web-to-node-stream@npm:3.0.2" + dependencies: + readable-stream: ^3.6.0 + checksum: 8c56cc62c68513425ddfa721954875b382768f83fa20e6b31e365ee00cbe7a3d6296f66f7f1107b16cd3416d33aa9f1680475376400d62a081a88f81f0ea7f9c + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -17239,6 +17274,16 @@ dts-critic@latest: languageName: node linkType: hard +"strtok3@npm:^7.0.0-alpha.7": + version: 7.0.0-alpha.8 + resolution: "strtok3@npm:7.0.0-alpha.8" + dependencies: + "@tokenizer/token": ^0.3.0 + peek-readable: ^5.0.0-alpha.5 + checksum: 00e5c9ed0c5de537839cf443d5628f0ae88d2956ca1fdcbd45cd97372045d7179a40ec99f6d06b02c59ec2141e362142ad0a87c59506d401dbd3bd1ee242abaa + languageName: node + linkType: hard + "style-to-object@npm:^0.3.0": version: 0.3.0 resolution: "style-to-object@npm:0.3.0" @@ -17737,6 +17782,16 @@ dts-critic@latest: languageName: node linkType: hard +"token-types@npm:^5.0.0-alpha.2": + version: 5.0.0-alpha.2 + resolution: "token-types@npm:5.0.0-alpha.2" + dependencies: + "@tokenizer/token": ^0.3.0 + ieee754: ^1.2.1 + checksum: ee23eeed6f383b1072d99781d62fc7840f1296a96d47e636e36fca757debd7eb4274d31fcd2d56997606eede00b12b1e61a64610fe0ed7807d6b1c4dcf5ccc6b + languageName: node + linkType: hard + "toml@npm:^3.0.0": version: 3.0.0 resolution: "toml@npm:3.0.0"