diff --git a/src/app/digest.ts b/src/app/digest.ts index 85e584d0..0773b26f 100644 --- a/src/app/digest.ts +++ b/src/app/digest.ts @@ -1,19 +1,34 @@ import { Canceler, RequestConfig, Uploader } from 'ngx-uploadx'; +export function readBlob(body: Blob, canceler?: Canceler): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + canceler && (canceler.onCancel = () => reject('aborted' && reader.abort())); + reader.onload = () => resolve(reader.result as ArrayBuffer); + reader.onerror = reject; + reader.readAsArrayBuffer(body); + }); +} + +export function bufferToHex(buf: ArrayBuffer) { + return Array.from(new Uint8Array(buf), x => x.toString(16).padStart(2, '0')).join(''); +} + +export function bufferToBase64(hash: ArrayBuffer) { + return btoa(String.fromCharCode(...new Uint8Array(hash))); +} + export const hasher = { + lookup: {} as Record, isSupported: window.crypto && !!window.crypto.subtle, - async sha(data: ArrayBuffer): Promise { - const dig = await crypto.subtle.digest('SHA-1', data); - return String.fromCharCode(...new Uint8Array(dig)); + async sha(data: ArrayBuffer): Promise { + return crypto.subtle.digest('SHA-1', data); + }, + digestHex(body: Blob, canceler?: Canceler): Promise { + return readBlob(body, canceler).then(buffer => this.sha(buffer).then(bufferToHex)); }, - getDigest(body: Blob, canceler?: Canceler): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - canceler && (canceler.onCancel = () => reject('aborted' && reader.abort())); - reader.onload = async () => resolve(await this.sha(reader.result as ArrayBuffer)); - reader.onerror = reject; - reader.readAsArrayBuffer(body); - }); + digestBase64(body: Blob, canceler?: Canceler): Promise { + return readBlob(body, canceler).then(buffer => this.sha(buffer).then(bufferToBase64)); } }; @@ -22,8 +37,19 @@ export async function injectTusChecksumHeader( req: RequestConfig ): Promise { if (hasher.isSupported && req.body instanceof Blob) { - const sha = await hasher.getDigest(req.body, req.canceler); - Object.assign(req.headers, { 'Upload-Checksum': `sha1 ${btoa(sha)}` }); + if (this.chunkSize) { + const { body, start } = this.getChunk((this.offset || 0) + this.chunkSize); + hasher.digestBase64(body, req.canceler).then(digest => { + const key = `${body.size}-${start}`; + hasher.lookup[req.url] = { key, sha: digest }; + }); + } + const key = `${req.body.size}-${this.offset}`; + const sha = + hasher.lookup[req.url]?.key === key + ? hasher.lookup[req.url].sha + : await hasher.digestBase64(req.body, req.canceler); + Object.assign(req.headers, { 'Upload-Checksum': `sha1 ${sha}` }); } return req; } @@ -33,8 +59,19 @@ export async function injectDigestHeader( req: RequestConfig ): Promise { if (hasher.isSupported && req.body instanceof Blob) { - const sha = await hasher.getDigest(req.body, req.canceler); - Object.assign(req.headers, { Digest: `sha=${btoa(sha)}` }); + if (this.chunkSize) { + const { body, start } = this.getChunk((this.offset || 0) + this.chunkSize); + hasher.digestBase64(body, req.canceler).then(digest => { + const key = `${body.size}-${start}`; + hasher.lookup[req.url] = { key, sha: digest }; + }); + } + const key = `${req.body.size}-${this.offset}`; + const sha = + hasher.lookup[req.url]?.key === key + ? hasher.lookup[req.url].sha + : await hasher.digestBase64(req.body, req.canceler); + Object.assign(req.headers, { Digest: `sha=${sha}` }); } return req; } diff --git a/src/app/on-push/on-push.component.ts b/src/app/on-push/on-push.component.ts index 1be9c0ed..6942d0a7 100644 --- a/src/app/on-push/on-push.component.ts +++ b/src/app/on-push/on-push.component.ts @@ -15,7 +15,7 @@ export class OnPushComponent implements OnDestroy { uploads$: Observable; options: UploadxOptions = { endpoint: `${environment.api}/files?uploadType=uploadx`, - chunkSize: 1024 * 1024 * 8, + chunkSize: 1024 * 1024 * 64, prerequest: injectDigestHeader, authorize: (req, token) => { token && (req.headers['Authorization'] = `Token ${token}`); diff --git a/src/app/service-way/service-way.component.ts b/src/app/service-way/service-way.component.ts index 305f784d..798ee26d 100644 --- a/src/app/service-way/service-way.component.ts +++ b/src/app/service-way/service-way.component.ts @@ -13,11 +13,10 @@ export class CustomId implements IdService { return new Date().getTime().toString(36); } const blob = uploader.file.slice(0, 256); - return await hasher.getDigest(blob); + return hasher.digestHex(blob); } } -// eslint-disable-next-line max-classes-per-file @Component({ selector: 'app-service-way', templateUrl: './service-way.component.html', @@ -27,34 +26,29 @@ export class ServiceWayComponent implements OnDestroy, OnInit { state$!: Observable; uploads: UploadState[] = []; private unsubscribe$ = new Subject(); - options!: UploadxOptions; + options: UploadxOptions = { + endpoint: `${environment.api}/files?uploadType=tus`, + uploaderClass: Tus, + token: this.authService.getAccessToken + // prerequest: injectTusChecksumHeader + }; constructor(private uploadxService: UploadxService, private authService: AuthService) {} ngOnInit(): void { - const endpoint = `${environment.api}/files?uploadType=tus`; - this.uploadxService.request({ method: 'OPTIONS', url: endpoint }).then( - ({ headers }) => { - console.table(headers); - const checkSumSupported = - hasher.isSupported && ('Tus-Checksum-Algorithm' in headers || true); // debug - this.options = { - endpoint, - uploaderClass: Tus, - token: this.authService.getAccessToken, - prerequest: checkSumSupported ? injectTusChecksumHeader : () => {} - }; - this.state$ = this.uploadxService.init(this.options); - this.state$.pipe(takeUntil(this.unsubscribe$)).subscribe(state => { - const target = this.uploads.find(item => item.uploadId === state.uploadId); - target ? Object.assign(target, state) : this.uploads.push(state); - }); - }, - e => { - console.error(e); - this.ngOnInit(); - } - ); + this.state$ = this.uploadxService.init(this.options); + this.state$.pipe(takeUntil(this.unsubscribe$)).subscribe(state => { + const target = this.uploads.find(item => item.uploadId === state.uploadId); + target ? Object.assign(target, state) : this.uploads.push(state); + }); + + this.uploadxService.ajax + .request({ method: 'OPTIONS', url: this.options.endpoint! }) + .then(({ headers }) => { + if (hasher.isSupported && headers['tus-checksum-algorithm'].includes('sha1')) { + this.uploadxService.options.prerequest = injectTusChecksumHeader; + } + }, console.error); } ngOnDestroy(): void { diff --git a/src/uploadx/lib/uploader.ts b/src/uploadx/lib/uploader.ts index d128a14c..8c01a48b 100644 --- a/src/uploadx/lib/uploader.ts +++ b/src/uploadx/lib/uploader.ts @@ -254,15 +254,20 @@ export abstract class Uploader implements UploadState { return this.responseHeaders[key.toLowerCase()] || null; } - protected getChunk(): { start: number; end: number; body: Blob } { + /** + * Get file chunk + * @param offset - number of bytes of the file to skip + * @param size - chunk size + */ + getChunk(offset?: number, size?: number): { start: number; end: number; body: Blob } { if (this.responseStatus === 413) { DynamicChunk.maxSize = DynamicChunk.size = Math.floor(DynamicChunk.size / 2); } this.chunkSize = this.options.chunkSize === 0 ? this.size : this.options.chunkSize || DynamicChunk.size; - const start = this.offset || 0; - const end = Math.min(start + this.chunkSize, this.size); - const body = this.file.slice(this.offset, end); + const start = offset ?? this.offset ?? 0; + const end = Math.min(start + (size || this.chunkSize), this.size); + const body = this.file.slice(start, end); return { start, end, body }; }