From 8a5a91b3c47bac7944cc04d022313e06dd04d486 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Fri, 28 Aug 2020 18:11:25 +0300 Subject: [PATCH] Upload fix Fixed upload preloader --- src/components/audio.ts | 2 +- src/components/preloader.ts | 58 ++++----- src/components/wrappers.ts | 4 +- src/lib/appManagers/appDocsManager.ts | 5 +- src/lib/appManagers/appDownloadManager.ts | 56 ++++++--- src/lib/appManagers/appImManager.ts | 10 +- src/lib/appManagers/appMessagesManager.ts | 136 ++++++++++------------ src/lib/mtproto/apiFileManager.ts | 67 ++++++----- src/lib/mtproto/mtproto.worker.ts | 1 + src/lib/mtproto/mtprotoworker.ts | 4 + src/types.d.ts | 14 ++- webpack.common.js | 2 +- 12 files changed, 203 insertions(+), 156 deletions(-) diff --git a/src/components/audio.ts b/src/components/audio.ts index 9daefbda..4260210c 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -303,7 +303,7 @@ export default class AudioElement extends HTMLElement { downloadDiv.innerHTML = '
'; } - if(doc.type != 'audio' && !uploading) { + if(doc.type != 'audio' || uploading) { this.append(downloadDiv); } diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 5ef32988..b9a1d42f 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -53,37 +53,41 @@ export default class ProgressivePreloader { } } - public attach(elem: Element, reset = true, promise?: CancellablePromise, append = true) { - if(promise/* && false */) { - this.promise = promise; + public attachPromise(promise: CancellablePromise) { + this.promise = promise; - const tempID = --this.tempID; + const tempID = --this.tempID; - const onEnd = () => { - promise.notify = null; + const onEnd = () => { + promise.notify = null; - if(tempID == this.tempID) { - this.detach(); - this.promise = promise = null; - } - }; - - //promise.catch(onEnd); - promise.finally(onEnd); - - if(promise.addNotifyListener) { - promise.addNotifyListener((details: {done: number, total: number}) => { - /* if(details.done >= details.total) { - onEnd(); - } */ - - if(tempID != this.tempID) return; - - //console.log('preloader download', promise, details); - const percents = details.done / details.total * 100; - this.setProgress(percents); - }); + if(tempID == this.tempID) { + this.detach(); + this.promise = promise = null; } + }; + + //promise.catch(onEnd); + promise.finally(onEnd); + + if(promise.addNotifyListener) { + promise.addNotifyListener((details: {done: number, total: number}) => { + /* if(details.done >= details.total) { + onEnd(); + } */ + + if(tempID != this.tempID) return; + + //console.log('preloader download', promise, details); + const percents = details.done / details.total * 100; + this.setProgress(percents); + }); + } + } + + public attach(elem: Element, reset = true, promise?: CancellablePromise, append = true) { + if(promise/* && false */) { + this.attachPromise(promise); } this.detached = false; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 80e82aff..ea7d3a83 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -59,7 +59,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai let img: HTMLImageElement; if(message) { - if(doc.type == 'video') { + if(doc.type == 'video' && doc.thumbs?.length) { return wrapPhoto(doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware); } @@ -390,7 +390,7 @@ export function wrapPhoto(photo: MTPhoto | MTDocument, message: any, container: }); }; - return cacheContext.downloaded ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true}); + return cacheContext.downloaded || !lazyLoadQueue ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true}); } export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: { diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 9c320345..2f409f60 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -124,7 +124,10 @@ class AppDocsManager { if((doc.type == 'gif' && doc.size > 8e6) || doc.type == 'audio' || doc.type == 'video') { doc.supportsStreaming = true; - doc.url = this.getFileURL(doc); + + if(!doc.url) { + doc.url = this.getFileURL(doc); + } } if(!doc.file_name) { diff --git a/src/lib/appManagers/appDownloadManager.ts b/src/lib/appManagers/appDownloadManager.ts index c7e85873..4904ee31 100644 --- a/src/lib/appManagers/appDownloadManager.ts +++ b/src/lib/appManagers/appDownloadManager.ts @@ -3,6 +3,7 @@ import apiManager from "../mtproto/mtprotoworker"; import { deferredPromise, CancellablePromise } from "../polyfill"; import type { DownloadOptions } from "../mtproto/apiFileManager"; import { getFileNameByLocation } from "../bin_utils"; +import { InputFile } from "../../types"; export type ResponseMethodBlob = 'blob'; export type ResponseMethodJson = 'json'; @@ -23,6 +24,8 @@ export class AppDownloadManager { private progress: {[fileName: string]: Progress} = {}; private progressCallbacks: {[fileName: string]: Array} = {}; + private uploadID = 0; + constructor() { $rootScope.$on('download_progress', (e) => { const details = e.detail as {done: number, fileName: string, total: number, offset: number}; @@ -40,39 +43,56 @@ export class AppDownloadManager { }); } - public download(options: DownloadOptions, responseMethod?: ResponseMethodBlob): DownloadBlob; - public download(options: DownloadOptions, responseMethod?: ResponseMethodJson): DownloadJson; - public download(options: DownloadOptions, responseMethod: ResponseMethod = 'blob'): DownloadBlob { - const fileName = getFileNameByLocation(options.location, {fileName: options.fileName}); - - if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; - + private getNewDeferred(fileName: string) { const deferred = deferredPromise(); - apiManager.downloadFile(options) - .then(deferred.resolve, deferred.reject) - .finally(() => { - delete this.progressCallbacks[fileName]; - }); - - //console.log('Will download file:', fileName, url); - deferred.cancel = () => { const error = new Error('Download canceled'); error.name = 'AbortError'; apiManager.cancelDownload(fileName); - delete this.downloads[fileName]; - delete this.progress[fileName]; - delete this.progressCallbacks[fileName]; + this.clearDownload(fileName); deferred.reject(error); deferred.cancel = () => {}; }; + deferred.finally(() => { + delete this.progress[fileName]; + delete this.progressCallbacks[fileName]; + }); + return this.downloads[fileName] = deferred; } + private clearDownload(fileName: string) { + delete this.downloads[fileName]; + } + + public download(options: DownloadOptions): DownloadBlob { + const fileName = getFileNameByLocation(options.location, {fileName: options.fileName}); + if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; + + const deferred = this.getNewDeferred(fileName); + apiManager.downloadFile(options).then(deferred.resolve, deferred.reject); + + //console.log('Will download file:', fileName, url); + return deferred; + } + + public upload(file: File | Blob) { + const fileName = /* (file as File).name || */'upload-' + this.uploadID++; + + const deferred = this.getNewDeferred(fileName); + apiManager.uploadFile({file, fileName}).then(deferred.resolve, deferred.reject); + + deferred.finally(() => { + this.clearDownload(fileName); + }); + + return deferred as any as CancellablePromise; + } + public getDownload(fileName: string) { return this.downloads[fileName]; } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index f3cc1c3c..9ffc694b 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -39,6 +39,7 @@ import appAudio from '../../components/appAudio'; import appPollsManager from './appPollsManager'; import { ripple } from '../../components/ripple'; import { horizontalMenu } from '../../components/horizontalMenu'; +import AudioElement from '../../components/audio'; //console.log('appImManager included33!'); @@ -2251,15 +2252,14 @@ export class AppImManager { case 'audio': case 'voice': case 'document': { - let doc = appDocsManager.getDoc(message.id); + const doc = appDocsManager.getDoc(message.id); this.log('will wrap pending doc:', doc); - let docDiv = wrapDocument(doc, false, true, message.id); + const docDiv = wrapDocument(doc, false, true, message.id); if(doc.type == 'audio' || doc.type == 'voice') { - // @ts-ignore - docDiv.preloader = preloader; + (docDiv as AudioElement).preloader = preloader; } else { - let icoDiv = docDiv.querySelector('.audio-download, .document-ico'); + const icoDiv = docDiv.querySelector('.audio-download, .document-ico'); preloader.attach(icoDiv, false); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 200fec5c..821095e0 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -23,6 +23,7 @@ import searchIndexManager from '../searchIndexManager'; import { MTDocument, MTPhotoSize } from "../../types"; import { logger, LogLevels } from "../logger"; import type {ApiFileManager} from '../mtproto/apiFileManager'; +import appDownloadManager from "./appDownloadManager"; //console.trace('include'); @@ -1121,9 +1122,9 @@ export class AppMessagesManager { flags |= 256; } - let preloader = new ProgressivePreloader(null, true); + const preloader = new ProgressivePreloader(null, true); - var media = { + const media = { _: 'messageMediaPending', type: attachType, file_name: fileName || apiFileName, @@ -1132,22 +1133,10 @@ export class AppMessagesManager { preloader: preloader, w: options.width, h: options.height, - url: options.objectURL, - progress: { - percent: 1, - total: file.size, - done: 0, - cancel: () => {} - } - }; - - preloader.preloader.onclick = () => { - this.log('cancelling upload', media); - this.setTyping('sendMessageCancelAction'); - media.progress.cancel(); + url: options.objectURL }; - var message: any = { + const message: any = { _: 'message', id: messageID, from_id: fromID, @@ -1168,7 +1157,7 @@ export class AppMessagesManager { pending: true }; - var toggleError = (on: boolean) => { + const toggleError = (on: boolean) => { if(on) { message.error = true; } else { @@ -1178,10 +1167,10 @@ export class AppMessagesManager { $rootScope.$broadcast('messages_pending'); }; - var uploaded = false, + let uploaded = false, uploadPromise: ReturnType = null; - let invoke = (flags: number, inputMedia: any) => { + const invoke = (flags: number, inputMedia: any) => { this.setTyping('sendMessageCancelAction'); return apiManager.invokeApi('messages.sendMedia', { @@ -1221,9 +1210,9 @@ export class AppMessagesManager { flags |= 128; // clear_draft if(isDocument) { - let {id, access_hash, file_reference} = file as MTDocument; + const {id, access_hash, file_reference} = file as MTDocument; - let inputMedia = { + const inputMedia = { _: 'inputMediaDocument', flags: 0, id: { @@ -1236,16 +1225,13 @@ export class AppMessagesManager { invoke(flags, inputMedia); } else if(file instanceof File || file instanceof Blob) { - let deferred = deferredPromise(); + const deferred = deferredPromise(); this.sendFilePromise.then(() => { if(!uploaded || message.error) { uploaded = false; - //uploadPromise = apiFileManager.uploadFile(file); - uploadPromise = fetch('/upload', { - method: 'POST', - body: file - }).then(res => res.json()); + uploadPromise = appDownloadManager.upload(file); + preloader.attachPromise(uploadPromise); } uploadPromise && uploadPromise.then((inputFile) => { @@ -1277,28 +1263,23 @@ export class AppMessagesManager { toggleError(true); }); - uploadPromise.notify = (progress: {done: number, total: number}) => { + uploadPromise.addNotifyListener((progress: {done: number, total: number}) => { this.log('upload progress', progress); - media.progress.done = progress.done; - media.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - this.setTyping({_: actionName, progress: media.progress.percent | 0}); - preloader.setProgress(media.progress.percent); // lol, nice - $rootScope.$broadcast('history_update', {peerID: peerID}); - }; - - media.progress.cancel = () => { - if(!uploaded) { + const percents = Math.max(1, Math.floor(100 * progress.done / progress.total)); + this.setTyping({_: actionName, progress: percents | 0}); + }); + + uploadPromise.catch(err => { + if(err.name === 'AbortError' && !uploaded) { + this.log('cancelling upload', media); + deferred.resolve(); - uploadPromise.cancel(); this.cancelPendingMessage(randomIDS); + this.setTyping('sendMessageCancelAction'); } - }; - - // @ts-ignore - uploadPromise['finally'](() => { - deferred.resolve(); - preloader.detach(); }); + + uploadPromise.finally(deferred.resolve); }); this.sendFilePromise = deferred; @@ -1382,12 +1363,6 @@ export class AppMessagesManager { _: 'messageMediaPending', type: 'album', preloader: preloader, - progress: { - percent: 1, - total: file.size, - done: 0, - cancel: () => {} - }, document: undefined as any, photo: undefined as any }; @@ -1442,12 +1417,6 @@ export class AppMessagesManager { media.photo = photo; } - preloader.preloader.onclick = () => { - this.log('cancelling upload', media); - this.setTyping('sendMessageCancelAction'); - media.progress.cancel(); - }; - let message = { _: 'message', id: messageID, @@ -1509,36 +1478,52 @@ export class AppMessagesManager { let inputs: any[] = []; for(let i = 0, length = files.length; i < length; ++i) { - let file = files[i]; - let message = messages[i]; - let media = message.media; - let preloader = media.preloader; - let actionName = file.type.indexOf('video/') === 0 ? 'sendMessageUploadVideoAction' : 'sendMessageUploadPhotoAction'; - let deferred = deferredPromise(); + const file = files[i]; + const message = messages[i]; + const media = message.media; + const preloader = media.preloader; + const actionName = file.type.indexOf('video/') === 0 ? 'sendMessageUploadVideoAction' : 'sendMessageUploadPhotoAction'; + const deferred = deferredPromise(); + let canceled = false; + + let apiFileName: string; + if(file.type.indexOf('video/') === 0) { + apiFileName = 'video.mp4'; + } else { + apiFileName = 'photo.' + file.type.split('/')[1]; + } await this.sendFilePromise; this.sendFilePromise = deferred; if(!uploaded || message.error) { uploaded = false; - //uploadPromise = apiFileManager.uploadFile(file); - uploadPromise = fetch('/upload', { - method: 'POST', - body: file - }).then(res => res.json()); + uploadPromise = appDownloadManager.upload(file); + preloader.attachPromise(uploadPromise); } - uploadPromise.notify = (progress: {done: number, total: number}) => { + uploadPromise.addNotifyListener((progress: {done: number, total: number}) => { this.log('upload progress', progress); - media.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - this.setTyping({_: actionName, progress: media.progress.percent | 0}); - preloader.setProgress(media.progress.percent); // lol, nice - $rootScope.$broadcast('history_update', {peerID: peerID}); - }; + const percents = Math.max(1, Math.floor(100 * progress.done / progress.total)); + this.setTyping({_: actionName, progress: percents | 0}); + }); + + uploadPromise.catch(err => { + if(err.name === 'AbortError' && !uploaded) { + this.log('cancelling upload item', media); + canceled = true; + } + }); await uploadPromise.then((inputFile) => { this.log('appMessagesManager: sendAlbum file uploaded:', inputFile); + if(canceled) { + return; + } + + inputFile.name = apiFileName; + let inputMedia: any; let details = options.sendFileDetails[i]; if(details.duration) { @@ -1568,6 +1553,10 @@ export class AppMessagesManager { peer: inputPeer, media: inputMedia }).then(messageMedia => { + if(canceled) { + return; + } + let inputMedia: any; if(messageMedia.photo) { let photo = messageMedia.photo; @@ -1598,7 +1587,6 @@ export class AppMessagesManager { this.log('appMessagesManager: sendAlbum uploadPromise.finally!'); deferred.resolve(); - preloader.detach(); } uploaded = true; diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index c69a3235..4c572939 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -5,7 +5,7 @@ import FileManager from "../filemanager"; import apiManager from "./apiManager"; import { deferredPromise, CancellablePromise } from "../polyfill"; import { logger, LogLevels } from "../logger"; -import { InputFileLocation, FileLocation, UploadFile } from "../../types"; +import { InputFileLocation, FileLocation, UploadFile, InputFile } from "../../types"; import { isSafari } from "../../helpers/userAgent"; import cryptoWorker from "../crypto/cryptoworker"; import { notifySomeone, notifyAll } from "../../helpers/context"; @@ -33,6 +33,10 @@ export class ApiFileManager { [fileName: string]: CancellablePromise } = {}; + public uploadPromises: { + [fileName: string]: CancellablePromise + } = {}; + public downloadPulls: { [x: string]: Array<{ cb: () => Promise, @@ -108,8 +112,8 @@ export class ApiFileManager { } public cancelDownload(fileName: string) { - const promise = this.cachedDownloadPromises[fileName]; - if(promise) { + const promise = this.cachedDownloadPromises[fileName] || this.uploadPromises[fileName]; + if(promise && !promise.isRejected && !promise.isFulfilled) { promise.cancel(); return true; } @@ -401,10 +405,11 @@ export class ApiFileManager { return this.getFileStorage().deleteFile(fileName); } - public uploadFile(file: Blob | File) { - var fileSize = file.size, - isBigFile = fileSize >= 10485760, - canceled = false, + public uploadFile({file, fileName}: {file: Blob | File, fileName: string}) { + const fileSize = file.size, + isBigFile = fileSize >= 10485760; + + let canceled = false, resolved = false, doneParts = 0, partSize = 262144, // 256 Kb @@ -418,27 +423,27 @@ export class ApiFileManager { activeDelta = 1; } - var totalParts = Math.ceil(fileSize / partSize); + const totalParts = Math.ceil(fileSize / partSize); + const fileID: [number, number] = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]; - var fileID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]; + let _part = 0; - var _part = 0, - resultInputFile = { - _: isBigFile ? 'inputFileBig' : 'inputFile', - id: fileID, - parts: totalParts, - name: file instanceof File ? file.name : '', - md5_checksum: '' + const resultInputFile: InputFile = { + _: isBigFile ? 'inputFileBig' : 'inputFile', + id: fileID, + parts: totalParts, + name: fileName, + md5_checksum: '' }; - let deferredHelper: { + const deferredHelper: { resolve?: (input: typeof resultInputFile) => void, reject?: (error: any) => void, notify?: (details: {done: number, total: number}) => void } = { notify: (details: {done: number, total: number}) => {} }; - let deferred: CancellablePromise = new Promise((resolve, reject) => { + const deferred: CancellablePromise = new Promise((resolve, reject) => { if(totalParts > 3000) { return reject({type: 'FILE_TOO_BIG'}); } @@ -459,13 +464,13 @@ export class ApiFileManager { errorHandler = () => {}; }; - let method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart'; + const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart'; for(let offset = 0; offset < fileSize; offset += partSize) { - let part = _part++; // 0, 1 + const part = _part++; // 0, 1 this.downloadRequest('upload', () => { return new Promise((uploadResolve, uploadReject) => { - var reader = new FileReader(); - var blob = file.slice(offset, offset + partSize); + const reader = new FileReader(); + const blob = file.slice(offset, offset + partSize); reader.onloadend = (e) => { if(canceled) { @@ -475,6 +480,7 @@ export class ApiFileManager { if(e.target.readyState != FileReader.DONE) { this.log.error('wrong readyState!'); + uploadReject(); return; } @@ -494,18 +500,19 @@ export class ApiFileManager { uploadResolve(); //////this.log('Progress', doneParts * partSize / fileSize); + + deferred.notify({done: doneParts * partSize, total: fileSize}); + if(doneParts >= totalParts) { deferred.resolve(resultInputFile); resolved = true; - } else { - deferred.notify({done: doneParts * partSize, total: fileSize}); } }, errorHandler); }; reader.readAsArrayBuffer(blob); }); - }, activeDelta); + }, activeDelta).catch(errorHandler); } deferred.cancel = () => { @@ -516,7 +523,15 @@ export class ApiFileManager { } }; - return deferred; + deferred.notify = (progress: {done: number, total: number}) => { + notifyAll({progress: {fileName, ...progress}}); + }; + + deferred.finally(() => { + delete this.uploadPromises[fileName]; + }); + + return this.uploadPromises[fileName] = deferred; } } diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts index 2e3439a0..a5edaeeb 100644 --- a/src/lib/mtproto/mtproto.worker.ts +++ b/src/lib/mtproto/mtproto.worker.ts @@ -106,6 +106,7 @@ ctx.addEventListener('message', async(e) => { }); case 'cancelDownload': + case 'uploadFile': case 'downloadFile': { try { // @ts-ignore diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index a919a90c..67a777d7 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -219,6 +219,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { public downloadFile(options: DownloadOptions) { return this.performTaskWorker('downloadFile', options); } + + public uploadFile(options: {file: Blob | File, fileName: string}) { + return this.performTaskWorker('uploadFile', options); + } } const apiManagerProxy = new ApiManagerProxy(); diff --git a/src/types.d.ts b/src/types.d.ts index 372bb112..d73c1e1b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -150,4 +150,16 @@ export type WorkerTaskTemplate = { type: string, id: number, payload: any -}; \ No newline at end of file +}; + +export type inputFile = { + _: 'inputFile', + parts: number, + id: [number, number], + name: string, + md5_checksum: string +}; + +export type inputFileBig = Omit & {_: 'inputFileBig'}; + +export type InputFile = inputFile | inputFileBig; \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index 5848e7ef..021d572a 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -6,7 +6,7 @@ const postcssPresetEnv = require('postcss-preset-env'); const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); const fs = require('fs'); -const allowedIPs = ['195.66.140.39', '192.168.31.144', '127.0.0.1', '192.168.31.1', '192.168.31.192', '176.100.18.181', '46.219.250.22']; +const allowedIPs = ['195.66.140.39', '192.168.31.144', '127.0.0.1', '192.168.31.1', '192.168.31.192', '176.100.18.181', '46.219.250.22', '193.42.119.184']; const devMode = process.env.NODE_ENV !== 'production'; const useLocal = false;