Created
March 23, 2022 17:53
-
-
Save 3cL1p5e7/6e5aa3620913ee6020252ef11871502c to your computer and use it in GitHub Desktop.
IC certified assets uploader hook
This file contains hidden or 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
import {ActorSubclass} from '@dfinity/agent'; | |
import { useCallback, useEffect, useState } from 'react'; | |
import cryptoJS from 'crypto-js'; | |
import type { _SERVICE } from 'PATH_TO_SERTIFIED_ASSETS_TS_IDL'; | |
export interface UseUploaderProps { | |
canister: ActorSubclass<_SERVICE>; | |
chunkSize?: number; | |
chunksPerOperation?: number; | |
progress?(stats: Stats): any; | |
} | |
export type Stats = { | |
uploaded: number; | |
of: number; | |
}; | |
export interface UploadProps { | |
file: File; | |
name?: string; | |
prefix?: string; | |
replace?: boolean; | |
withHash?: boolean; | |
} | |
const MAX_CHUNK_SIZE = 1900000; | |
const CHUNKS_PER_OPERATION = 8; | |
export const useUploader = ({ | |
canister, | |
chunkSize = MAX_CHUNK_SIZE, | |
chunksPerOperation = CHUNKS_PER_OPERATION, | |
progress = () => {} | |
}: UseUploaderProps) => { | |
const [loaded, setLoaded] = useState(false); | |
const [files, setFiles] = useState<{key: string, content_type: string}[]>([]); | |
const [fetching, setFetching] = useState(false); | |
useEffect(() => { | |
canister.list({}) | |
.then((files) => { | |
setFiles(files); | |
setLoaded(true); | |
}); | |
}, []); | |
const upload = useCallback(async ({ | |
file, | |
name = file.name, | |
prefix = '', | |
replace = false, | |
withHash = false | |
}: UploadProps) => { | |
if (!loaded) { | |
throw 'File list was not loaded'; | |
} | |
const key = encodeURI(`/${prefix.replace(/^\//, '')}/${name}`); | |
const fileExists = !!files.find(f => f.key == key); | |
if (!replace && fileExists) { | |
throw 'File with same name already exist. Try to use "replace" option or set different "name"'; | |
} | |
setFetching(true); | |
const from = Date.now(); | |
const payload = { key, source: [] }; | |
const { batch_id } = await canister.create_batch({}); | |
const hasher = cryptoJS.algo.SHA256.create(); | |
const numberOfChunks = Math.ceil(file.size / chunkSize); | |
const numberOfOperations = Math.ceil(numberOfChunks / chunksPerOperation); | |
console.log({ numberOfChunks, numberOfOperations }); | |
let stats: Stats = { | |
uploaded: 0, | |
of: numberOfChunks, | |
}; | |
progress(stats); | |
let start = 0; | |
const chunk_ids: bigint[] = []; | |
while (chunk_ids.length != numberOfChunks) { | |
const chunksPerOperationArr = Array(chunksPerOperation).fill(undefined); | |
const results = await Promise.all( | |
chunksPerOperationArr | |
.map(async (_, index) => { | |
const localStart = start + chunkSize * index; | |
if (localStart >= file.size) { | |
return null; | |
} | |
const end = start + chunkSize * (index + 1); | |
const chunk = file.slice(localStart, end); | |
const buffer = await chunk.arrayBuffer(); | |
const uintarr = new Uint8Array(buffer); | |
const content = [...uintarr]; | |
const { chunk_id } = await canister.create_chunk({ batch_id, content, sha256: [] }); | |
stats.uploaded += 1; | |
progress({...stats}); | |
return { chunk_id, word: arrayBufferToWordArray(content) }; | |
}) | |
); | |
start = start + chunkSize * (results.filter(r => r).length); | |
results.forEach((chunkRes) => { | |
if (!chunkRes) { | |
return; | |
} | |
const {word, chunk_id} = chunkRes; | |
chunk_ids.push(chunk_id); | |
withHash && hasher.update(word); | |
}) | |
} | |
let hash: [number[]] | [] = []; | |
if (withHash) { | |
const wa = hasher.finalize(); | |
let buffer1 = Buffer.from(wa.toString(cryptoJS.enc.Hex), 'hex'); | |
let array = new Uint8Array(buffer1); | |
hash = [[...array]]; | |
} | |
const preOperations = []; | |
if (replace && fileExists) { | |
preOperations.push({ DeleteAsset: { key } }); | |
} | |
await canister.commit_batch({ batch_id, operations: [ | |
...preOperations, | |
{ | |
CreateAsset: { | |
key: payload.key, | |
content_type: file.type, | |
}, | |
}, | |
{ | |
SetAssetContent: { | |
key: payload.key, | |
sha256: hash, | |
chunk_ids, | |
content_encoding: 'identity', | |
} | |
} | |
] }); | |
const to = Date.now(); | |
const duration = to - from; | |
console.log(`Time result ${(duration) / 1000} s = ${duration / 1000 / 60} m`); | |
canister.list({}).then(setFiles); | |
setFetching(false); | |
}, [setFetching, setFiles, files, loaded]); | |
return { | |
fetching, | |
loaded, | |
upload, | |
files, | |
}; | |
}; | |
function arrayBufferToWordArray(ab: any) { | |
var i8a = new Uint8Array(ab); | |
var a = []; | |
for (var i = 0; i < i8a.length; i += 4) { | |
a.push(i8a[i] << 24 | i8a[i + 1] << 16 | i8a[i + 2] << 8 | i8a[i + 3]); | |
} | |
return cryptoJS.lib.WordArray.create(a, i8a.length); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment