Skip to content

Instantly share code, notes, and snippets.

@schickling
Created April 1, 2023 09:52
Show Gist options
  • Save schickling/586fdc6e260b52d22f083553b0f602cf to your computer and use it in GitHub Desktop.
Save schickling/586fdc6e260b52d22f083553b0f602cf to your computer and use it in GitHub Desktop.
blob <> file[] encoding
// export const encodeFilesToBlob = (files: File[]): Promise<Blob> => {
// const formData = new FormData()
// files.forEach((file) => {
// formData.append('files[]', file, file.name)
// })
// return new Response(formData).blob()
// }
// export const decodeBlobToFiles = async (blob: Blob): Promise<File[]> => {
// const resp = new Response(blob, { headers: { 'Content-Type': blob.type } })
// const formData = await resp.formData()
// const formFiles = formData.getAll('files[]') as File[]
// return formFiles
// }
// export const encodeFilesToBlob = (files: File[]): Blob => {
// const blob = new Blob(files, { type: 'application/octet-stream' })
// return blob
// }
/**
* Structure:
* First 4 bytes: number of files
* Middle section: Array of file sizes (in bytes) relative to the order of the `files` array
* End section: Array of file name lengths (in bytes) relative to the order of the `files` array
*
* Array of file sizes (in bytes) relative to the order of the `files` array with the first element being the number of files.
* Encoded as a Uint32Array (so each element has to be less than 2^32 ~ 4GB)
*
* e.g. for files = [
* file-a (size 300b, filename = 'dog.jpg')
* file-b (size 200b, filename = 'human.jpg')
* file-c (size 400b, filename = 'fish.jpg')
* ] => [3, 300, 200, 400, 7, 9, 8]
*/
type EncodedOffsetInfo = Uint32Array
const getEncodedOffsetInfo = (files: readonly File[], fileNameSizes: number[]): EncodedOffsetInfo => {
const encodedOffsetInfo = new Uint32Array(files.length * 2 + 1)
encodedOffsetInfo[0] = files.length
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < files.length; i++) {
encodedOffsetInfo[i + 1] = files[i]!.size
}
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < files.length; i++) {
encodedOffsetInfo[i + files.length + 1] = fileNameSizes[i]!
}
return encodedOffsetInfo
}
/**
* Structure:
* Header: EncodedOffsetInfo
* Main section: Array of tuples: [file, file name]
*/
type EncodedBlob = Blob
/** Note that the file type is not preserved. */
export const encodeFilesToBlob = (files: readonly File[]): EncodedBlob => {
const textEncoder = new TextEncoder()
const encodedFileNames = files.map((file) => textEncoder.encode(file.name))
const fileNameSizes = encodedFileNames.map((encodedFileName) => encodedFileName.length)
const encodedOffsetInfo = getEncodedOffsetInfo(files, fileNameSizes)
const encodedOffsetInfoBlob = new Blob([encodedOffsetInfo], { type: 'application/octet-stream' })
const blobParts: BlobPart[] = [encodedOffsetInfoBlob]
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < files.length; i++) {
blobParts.push(files[i]!, encodedFileNames[i]!)
}
return new Blob(blobParts, { type: 'application/octet-stream' })
}
export const decodeBlobToFiles = async (blob: EncodedBlob): Promise<File[]> => {
// debugger
const arrayBuffer = await blob.arrayBuffer()
const dataView = new DataView(arrayBuffer)
const textDecoder = new TextDecoder()
const numberOfFiles = dataView.getUint32(0, true)
const fileSizes = []
for (let i = 0; i < numberOfFiles; i++) {
fileSizes.push(dataView.getUint32(4 * (i + 1), true))
}
const fileNameSizes = []
for (let i = 0; i < numberOfFiles; i++) {
fileNameSizes.push(dataView.getUint32(4 * (numberOfFiles + i + 1), true))
}
let offset = 4 * (numberOfFiles * 2 + 1)
const files = []
for (let i = 0; i < numberOfFiles; i++) {
const fileSize = fileSizes[i]!
const fileData = new Uint8Array(arrayBuffer, offset, fileSize)
offset += fileSize
const fileNameSize = fileNameSizes[i]!
const fileNameData = new Uint8Array(arrayBuffer, offset, fileNameSize)
const fileName = textDecoder.decode(fileNameData)
offset += fileNameSize
const file = new File([fileData], fileName)
files.push(file)
}
return files
}
// const getFilesOffsetInfo
// export const decodeBlobToFiles = (blob: Blob): Promise<File[]> => {
// return new Promise((resolve, reject) => {
// const reader = new FileReader();
// reader.onload = function() {
// const arrayBuffer = reader.result;
// const files = [];
// const dataView = new DataView(arrayBuffer);
// let offset = 0;
// while (offset < arrayBuffer.byteLength) {
// const length = dataView.getUint32(offset, true);
// offset += 4;
// const name = new TextDecoder().decode(new Uint8Array(arrayBuffer.slice(offset, offset + length)));
// offset += length;
// const typeLength = dataView.getUint32(offset, true);
// offset += 4;
// const type = new TextDecoder().decode(new Uint8Array(arrayBuffer.slice(offset, offset + typeLength)));
// offset += typeLength;
// const blob = new Blob([arrayBuffer.slice(offset, offset + dataView.getUint32(offset, true))], { type: type });
// offset += 4;
// files.push(new File([blob], name, { type: type }));
// }
// console.log(files); // array of File objects
// };
// reader.readAsArrayBuffer(blob);
// })
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment