Skip to content

Commit

Permalink
Merge pull request #27 from decentraland/feat/add-support-for-array-b…
Browse files Browse the repository at this point in the history
…uffers

feat: Add support for array buffers, node buffers and auto-selecting file format
  • Loading branch information
LautaroPetaccio committed Feb 7, 2022
2 parents 93187d6 + 8bf41db commit 0330018
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 53 deletions.
32 changes: 30 additions & 2 deletions src/client/BuilderClient.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
60 changes: 49 additions & 11 deletions src/content/content.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array>
let content: RawContent<Uint8Array | ArrayBuffer>
let hashes: Record<string, string>
let expectedHashes: Record<string, string>
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)
})
})
})
Expand Down
9 changes: 7 additions & 2 deletions src/content/content.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -32,7 +33,7 @@ export async function computeHashes<T extends Content>(
*/
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
Expand All @@ -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'
Expand All @@ -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<T extends Blob | Uint8Array>(
export function sortContent<T extends Content>(
bodyShape: BodyShapeType,
contents: RawContent<T>
): SortedContent<T> {
Expand Down
2 changes: 1 addition & 1 deletion src/content/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Content = Uint8Array | Blob
export type Content = Uint8Array | Blob | ArrayBuffer | Buffer

export type SortedContent<T extends Content> = {
male: Record<string, T>
Expand Down
99 changes: 68 additions & 31 deletions src/files/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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<Uint8Array>(fileName, zipFileContent)
).rejects.toThrow(new InvalidAssetFileError())
return expect(loadFile(fileName, zipFileContent)).rejects.toThrow(
new InvalidAssetFileError()
)
})
})

Expand Down Expand Up @@ -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<Uint8Array>(fileName, zipFileContent)
).rejects.toThrow(
return expect(loadFile(fileName, zipFileContent)).rejects.toThrow(
new ModelInRepresentationNotFoundError('some-unkown-file.glb')
)
})
Expand Down Expand Up @@ -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<Uint8Array>(fileName, zipFileContent)
).rejects.toThrow(new FileNotFoundError('some-unkown-file.glb'))
return expect(loadFile(fileName, zipFileContent)).rejects.toThrow(
new FileNotFoundError('some-unkown-file.glb')
)
})
})

Expand Down Expand Up @@ -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<Uint8Array>(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
})
})
})
})
Expand All @@ -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<Uint8Array>(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)
)
})
})

Expand All @@ -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<Uint8Array>(fileName, zipFileContent)
).rejects.toThrow(new ModelFileNotFoundError())
return expect(loadFile(fileName, zipFileContent)).rejects.toThrow(
new ModelFileNotFoundError()
)
})
})

Expand All @@ -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<Uint8Array>(fileName, zipFileContent)
).resolves.toEqual({
return expect(loadFile(fileName, zipFileContent)).resolves.toEqual({
content: {
[mainModel]: modelContent,
[THUMBNAIL_PATH]: thumbnailContent
Expand Down
19 changes: 13 additions & 6 deletions src/files/files.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -69,23 +69,30 @@ function isModelPath(fileName: string) {
* @param zipFile - The ZIP file.
*/
async function handleZippedModelFiles<T extends Content>(
zipFile: T,
asBlob = false
zipFile: T
): Promise<LoadedFile<T>> {
const zip: JSZip = await JSZip.loadAsync(zipFile)

const fileNames: string[] = []
const promiseOfFileContents: Array<Promise<T>> = []
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 (
!basename(filePath).startsWith('.') &&
basename(filePath) !== ASSET_MANIFEST
) {
fileNames.push(filePath)
promiseOfFileContents.push(
file.async(asBlob ? 'blob' : 'uint8array') as Promise<T>
)
promiseOfFileContents.push(file.async(fileFormat) as Promise<T>)
}
})

Expand Down

0 comments on commit 0330018

Please sign in to comment.