Skip to content

Instantly share code, notes, and snippets.

@schickling
Created March 29, 2023 09:04
Show Gist options
  • Save schickling/c2820cc880b4679a83f2c447c966ef78 to your computer and use it in GitHub Desktop.
Save schickling/c2820cc880b4679a83f2c447c966ef78 to your computer and use it in GitHub Desktop.
/**
* 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