Skip to content

Instantly share code, notes, and snippets.

@3cL1p5e7
Created March 23, 2022 17:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 3cL1p5e7/6e5aa3620913ee6020252ef11871502c to your computer and use it in GitHub Desktop.
Save 3cL1p5e7/6e5aa3620913ee6020252ef11871502c to your computer and use it in GitHub Desktop.
IC certified assets uploader hook
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