import SparkMD5 from 'spark-md5' import { BrowserFile, DuplicatedFileError, IFileServiceImplementation, PreparedFiles, } from '../../interfaces' import { CONTENT_TYPES, FILES_API } from '../../services' import { ApisauceInstance } from '../../services/api/apisauce' import { FileToSign } from '../../services/edocu/batch' import { DuplicateError, SignError } from '../fileServiceProvider' export default class FileService implements IFileServiceImplementation { api: ApisauceInstance constructor(api: ApisauceInstance) { this.api = api } async uploadFiles( type: string, hash: string, organization: string, files: PreparedFiles[], workRecordHash?: string, ) { // console.log('0. uploadFiles', files) // 1. SIGN const fileHashes: Record = {} const filesData = await files.reduce( async (accFilesP: Promise, input) => { const accFiles = await accFilesP const inputData = await input.value.reduce( async (accInputP: Promise, file) => { const accInput = await accInputP const fileHash = await this.getFileHash(file.data) const mimeType = this.mimeType(file.data.file) fileHashes[fileHash] = file.data.file const fileSize = file.data.file.size const fileAttribute = typeof input.attribute === 'string' ? input.attribute : input.attribute.id accInput.push({ file: { filename: file.data.file.name, file_size: fileSize, md5: fileHash, contentType: mimeType, }, attribute: { id: fileAttribute, hash: workRecordHash || undefined, }, }) return accInput }, Promise.resolve([]), ) const files: FileToSign[] = [] return files.concat(accFiles, inputData) }, Promise.resolve([]), ) // console.log('1. FilesData', filesData) // console.log('1. fileHashes', fileHashes) const signRequest = await this.api.post< Record, DuplicatedFileError[] >( `${FILES_API}/credentials/${type}/${hash}/put`, { organization: { id: organization }, files: filesData, }, { headers: { 'Content-Type': CONTENT_TYPES.CREDENTIAL, }, }, ) if (!signRequest.ok) { if (signRequest.status === 409) { throw new DuplicateError( 'Duplicated files', signRequest?.originalError.response?.data || [], ) } else { throw new SignError('Cannot sign files') } } if (signRequest.data) { // 2. UPLOAD const uploadPromises = Object.entries(signRequest.data).map( ([fileHash, fileData]) => { console.log( `2. upload file ${fileHash} to ${fileData.url} at path ${fileHashes[fileHash]}`, ) return this.uploadFile(fileData.url, fileHashes[fileHash]) }, ) console.log('2. uploadPromises', uploadPromises) await Promise.all(uploadPromises) } // 3. BATCH const batchData = [ { element: { type, hash }, files: filesData, organization: { id: organization }, }, ] return batchData } // method to upload single file async uploadFile(url: string, file: File) { console.log('url uploadFile', url) console.log('file uploadFile', file) const uploadPromise = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open('PUT', url, true) xhr.setRequestHeader('Content-Type', file.type) xhr.upload.addEventListener('load', resolve) xhr.upload.addEventListener('error', reject) xhr.upload.addEventListener('abort', reject) xhr.upload.addEventListener('progress', (event) => { console.log( 'upload progres', Math.floor((event.loaded / event.total) * 100), ) }) xhr.send(file) }) return uploadPromise } mimeType(file: File) { return file.type } getFileHash = (fileData: BrowserFile): Promise => { const file = fileData.file const chunkSize = 2097152 // read in chunks of 2MB const chunks = Math.ceil(file.size / chunkSize) let currentChunk = 0 const spark = new SparkMD5.ArrayBuffer() return new Promise((resolve) => { const onLoad = (e: ProgressEvent) => { spark.append((e.target as FileReader).result as ArrayBuffer) // append array buffer currentChunk++ if (currentChunk < chunks) { loadNext() } else { resolve(spark.end()) // compute md5 hash } } function loadNext() { const fileReader = new FileReader() fileReader.onload = onLoad const start = currentChunk * chunkSize const end = start + chunkSize >= file.size ? file.size : start + chunkSize fileReader.readAsArrayBuffer(file.slice(start, end)) } loadNext() }) } }