diff --git a/src/client/BuilderClient.ts b/src/client/BuilderClient.ts index 63844cc..5c7ba2d 100644 --- a/src/client/BuilderClient.ts +++ b/src/client/BuilderClient.ts @@ -1,8 +1,9 @@ import FormData from 'form-data' +import { Buffer } from 'buffer' import crossFetch from 'cross-fetch' import { Authenticator, AuthIdentity } from 'dcl-crypto' -import { Content } from '../content/types' import { RemoteItem, LocalItem } from '../item/types' +import { Content } from '../content/types' import { ClientError } from './BuilderClient.errors' import { ServerResponse } from './types' @@ -56,6 +57,30 @@ export class BuilderClient { return headers } + private convertToFormDataBinary(data: Content): Blob | Buffer { + const blobExists = globalThis.Blob !== undefined + const bufferExists = Buffer !== undefined + if ( + (blobExists && data instanceof globalThis.Blob) || + (bufferExists && Buffer.isBuffer(data)) + ) { + return data + } + + if ( + blobExists && + (data instanceof Uint8Array || data instanceof ArrayBuffer) + ) { + return new Blob([data]) + } else if (bufferExists && data instanceof Uint8Array) { + return Buffer.from(data.buffer) + } else if (bufferExists && data instanceof ArrayBuffer) { + return Buffer.from(data) + } + + throw new Error('Unsupported content type') + } + /** * Updates or inserts an item. * @param item - The item to insert or update. @@ -99,7 +124,10 @@ export class BuilderClient { if (Object.keys(newContent).length > 0) { const formData = new FormData() for (const path in newContent) { - formData.append(item.contents[path], newContent[path]) + formData.append( + item.contents[path], + this.convertToFormDataBinary(newContent[path]) + ) } let uploadResponse: Response diff --git a/src/content/content.spec.ts b/src/content/content.spec.ts index 0c7ce0a..c60462f 100644 --- a/src/content/content.spec.ts +++ b/src/content/content.spec.ts @@ -1,25 +1,63 @@ +import { TextEncoder } from 'util' import { THUMBNAIL_PATH } from '../item/constants' import { BodyShapeType } from '../item/types' import { computeHashes, prefixContentName, sortContent } from './content' import { RawContent } from './types' describe('when computing the hashes of raw content', () => { - let content: RawContent + let content: RawContent let hashes: Record + let expectedHashes: Record + let encoder: TextEncoder - beforeEach(async () => { - content = { - someThing: Buffer.from('aThing'), - someOtherThing: Buffer.from('someOtherThing') - } - hashes = await computeHashes(content) - }) - - it('should compute the hash of each Uint8Array', () => { - expect(hashes).toEqual({ + beforeEach(() => { + encoder = new TextEncoder() + expectedHashes = { someOtherThing: 'bafkreiatcrqxiuonjtnt2pml2kmfpct6veomwajfiz5x3t2icj22rpt5ly', someThing: 'bafkreig6f2q62jrqswtg4to2yr454aa2amhuevgsyl6oj3qrwshlazufcu' + } + }) + + describe('when the content is in the Uint8Array format', () => { + beforeEach(async () => { + content = { + someThing: encoder.encode('aThing'), + someOtherThing: encoder.encode('someOtherThing') + } + hashes = await computeHashes(content) + }) + + it('should compute the hash of each Uint8Array', () => { + expect(hashes).toEqual(expectedHashes) + }) + }) + + describe('when the content is in the ArrayBuffer format', () => { + beforeEach(async () => { + content = { + someThing: encoder.encode('aThing').buffer, + someOtherThing: encoder.encode('someOtherThing').buffer + } + hashes = await computeHashes(content) + }) + + it('should compute the hash of each ArrayBuffer', () => { + expect(hashes).toEqual(expectedHashes) + }) + }) + + describe('when the content is in the Buffer format', () => { + beforeEach(async () => { + content = { + someThing: Buffer.from('aThing'), + someOtherThing: Buffer.from('someOtherThing') + } + hashes = await computeHashes(content) + }) + + it('should compute the hash of each Buffer', () => { + expect(hashes).toEqual(expectedHashes) }) }) }) diff --git a/src/content/content.ts b/src/content/content.ts index 32775d9..c786784 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -1,4 +1,5 @@ import { Hashing } from 'dcl-catalyst-commons' +import { Buffer } from 'buffer' import { BodyShapeType } from '../item/types' import { THUMBNAIL_PATH } from '../item/constants' import { Content, HashedContent, RawContent, SortedContent } from './types' @@ -32,7 +33,7 @@ export async function computeHashes( */ async function makeContentFile( path: string, - content: string | Uint8Array | Blob + content: string | Uint8Array | Blob | ArrayBuffer | Buffer ): Promise<{ name: string; content: Uint8Array }> { if (typeof content === 'string') { // This must be polyfilled in the browser @@ -44,6 +45,10 @@ async function makeContentFile( return { name: path, content: new Uint8Array(buffer) } } else if (content instanceof Uint8Array) { return { name: path, content } + } else if (content instanceof ArrayBuffer) { + return { name: path, content: new Uint8Array(content) } + } else if (Buffer.isBuffer(content)) { + return { name: path, content: new Uint8Array(content) } } throw new Error( 'Unable to create ContentFile: content must be a string, a Blob or a Uint8Array' @@ -69,7 +74,7 @@ export function prefixContentName( * @param bodyShape - The body shaped used to sort the content. * @param contents - The contents to be sorted. */ -export function sortContent( +export function sortContent( bodyShape: BodyShapeType, contents: RawContent ): SortedContent { diff --git a/src/content/types.ts b/src/content/types.ts index a8f07c3..f9ad31a 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -1,4 +1,4 @@ -export type Content = Uint8Array | Blob +export type Content = Uint8Array | Blob | ArrayBuffer | Buffer export type SortedContent = { male: Record diff --git a/src/files/files.spec.ts b/src/files/files.spec.ts index 4d5e413..99ba6ff 100644 --- a/src/files/files.spec.ts +++ b/src/files/files.spec.ts @@ -52,7 +52,7 @@ describe('when loading an item file', () => { describe('and the file is a zip model file', () => { let zipFile: JSZip - let zipFileContent: Uint8Array + let zipFileContent: Uint8Array | ArrayBuffer let thumbnailContent: Uint8Array beforeEach(() => { @@ -72,9 +72,9 @@ describe('when loading an item file', () => { }) it('should throw an error signaling that the asset file is invalid', () => { - return expect( - loadFile(fileName, zipFileContent) - ).rejects.toThrow(new InvalidAssetFileError()) + return expect(loadFile(fileName, zipFileContent)).rejects.toThrow( + new InvalidAssetFileError() + ) }) }) @@ -103,9 +103,7 @@ describe('when loading an item file', () => { }) it("should throw an error signaling that the main file isn't included in the representation contents", () => { - return expect( - loadFile(fileName, zipFileContent) - ).rejects.toThrow( + return expect(loadFile(fileName, zipFileContent)).rejects.toThrow( new ModelInRepresentationNotFoundError('some-unkown-file.glb') ) }) @@ -135,9 +133,9 @@ describe('when loading an item file', () => { }) it('should throw an error signaling that the file in the representation contents was not found', () => { - return expect( - loadFile(fileName, zipFileContent) - ).rejects.toThrow(new FileNotFoundError('some-unkown-file.glb')) + return expect(loadFile(fileName, zipFileContent)).rejects.toThrow( + new FileNotFoundError('some-unkown-file.glb') + ) }) }) @@ -173,21 +171,62 @@ describe('when loading an item file', () => { zipFile.file(ASSET_MANIFEST, JSON.stringify(assetFileContent)) zipFile.file(modelFile, modelFileContent) zipFile.file(textureFile, textureFileContent) - zipFileContent = await zipFile.generateAsync({ - type: 'uint8array' + }) + + describe('and the zip file is in the Uint8Array format', () => { + beforeEach(async () => { + zipFileContent = await zipFile.generateAsync({ + type: 'uint8array' + }) + }) + + it('should build the LoadedFile with the zipped contents and the asset file with the same buffer format as the zip', () => { + return expect(loadFile(fileName, zipFileContent)).resolves.toEqual({ + content: { + [modelFile]: modelFileContent, + [textureFile]: textureFileContent, + [THUMBNAIL_PATH]: thumbnailContent + }, + asset: assetFileContent + }) }) }) - it('should build the LoadedFile with the zipped contents and the asset file', () => { - return expect( - loadFile(fileName, zipFileContent) - ).resolves.toEqual({ - content: { - [modelFile]: modelFileContent, - [textureFile]: textureFileContent, - [THUMBNAIL_PATH]: thumbnailContent - }, - asset: assetFileContent + describe('and the zip file is in the ArrayBuffer format', () => { + beforeEach(async () => { + zipFileContent = await zipFile.generateAsync({ + type: 'arraybuffer' + }) + }) + + it('should build the LoadedFile with the zipped contents and the asset file with the same buffer format as the zip', () => { + return expect(loadFile(fileName, zipFileContent)).resolves.toEqual({ + content: { + [modelFile]: modelFileContent.buffer, + [textureFile]: textureFileContent.buffer, + [THUMBNAIL_PATH]: thumbnailContent.buffer + }, + asset: assetFileContent + }) + }) + }) + + describe('and the zip file is in the Buffer format', () => { + beforeEach(async () => { + zipFileContent = await zipFile.generateAsync({ + type: 'nodebuffer' + }) + }) + + it('should build the LoadedFile with the zipped contents and the asset file with the same buffer format as the zip', () => { + return expect(loadFile(fileName, zipFileContent)).resolves.toEqual({ + content: { + [modelFile]: Buffer.from(modelFileContent.buffer), + [textureFile]: Buffer.from(textureFileContent.buffer), + [THUMBNAIL_PATH]: Buffer.from(thumbnailContent.buffer) + }, + asset: assetFileContent + }) }) }) }) @@ -204,9 +243,9 @@ describe('when loading an item file', () => { }) it('should throw an error signaling that the file is too big', () => { - return expect( - loadFile(fileName, zipFileContent) - ).rejects.toThrow(new FileTooBigError(modelFile, MAX_FILE_SIZE + 1)) + return expect(loadFile(fileName, zipFileContent)).rejects.toThrow( + new FileTooBigError(modelFile, MAX_FILE_SIZE + 1) + ) }) }) @@ -223,9 +262,9 @@ describe('when loading an item file', () => { }) it("should throw an error signaling that the zip doesn't contain a model file", () => { - return expect( - loadFile(fileName, zipFileContent) - ).rejects.toThrow(new ModelFileNotFoundError()) + return expect(loadFile(fileName, zipFileContent)).rejects.toThrow( + new ModelFileNotFoundError() + ) }) }) @@ -237,9 +276,7 @@ describe('when loading an item file', () => { }) it('should build the LoadedFile contents with the zipped files and the main model file path', () => { - return expect( - loadFile(fileName, zipFileContent) - ).resolves.toEqual({ + return expect(loadFile(fileName, zipFileContent)).resolves.toEqual({ content: { [mainModel]: modelContent, [THUMBNAIL_PATH]: thumbnailContent diff --git a/src/files/files.ts b/src/files/files.ts index d752b7d..883c1b5 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -1,4 +1,4 @@ -import JSZip from 'jszip' +import JSZip, { OutputType } from 'jszip' import { basename } from 'path' import Ajv from 'ajv' import addAjvFormats from 'ajv-formats' @@ -69,13 +69,22 @@ function isModelPath(fileName: string) { * @param zipFile - The ZIP file. */ async function handleZippedModelFiles( - zipFile: T, - asBlob = false + zipFile: T ): Promise> { const zip: JSZip = await JSZip.loadAsync(zipFile) const fileNames: string[] = [] const promiseOfFileContents: Array> = [] + let fileFormat: OutputType + if (globalThis.Blob && zipFile instanceof globalThis.Blob) { + fileFormat = 'blob' + } else if (Buffer.isBuffer(zipFile)) { + fileFormat = 'nodebuffer' + } else if (zipFile instanceof Uint8Array) { + fileFormat = 'uint8array' + } else if (zipFile instanceof ArrayBuffer) { + fileFormat = 'arraybuffer' + } zip.forEach((filePath, file) => { if ( @@ -83,9 +92,7 @@ async function handleZippedModelFiles( basename(filePath) !== ASSET_MANIFEST ) { fileNames.push(filePath) - promiseOfFileContents.push( - file.async(asBlob ? 'blob' : 'uint8array') as Promise - ) + promiseOfFileContents.push(file.async(fileFormat) as Promise) } })