Created
March 29, 2023 09:04
-
-
Save schickling/c2820cc880b4679a83f2c447c966ef78 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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: 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 | |
export const encodeFilesToBlob = (files: 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[]> => { | |
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.slice(offset, offset + fileSize)) | |
offset += fileSize | |
const fileNameSize = fileNameSizes[i]! | |
const fileNameData = new Uint8Array(arrayBuffer.slice(offset, offset + fileNameSize)) | |
const fileName = textDecoder.decode(fileNameData) | |
offset += fileNameSize | |
const file = new File([fileData], fileName) | |
files.push(file) | |
} | |
return files | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment