diff --git a/src/components/popupAvatar.ts b/src/components/popupAvatar.ts index 70b159f2..84b3322f 100644 --- a/src/components/popupAvatar.ts +++ b/src/components/popupAvatar.ts @@ -1,5 +1,4 @@ import resizeableImage from "../lib/cropper"; -import apiFileManager from "../lib/mtproto/apiFileManager"; export class PopupAvatar { private container = document.getElementById('popup-avatar'); @@ -77,7 +76,11 @@ export class PopupAvatar { private resolve() { this.onCrop(() => { - return apiFileManager.uploadFile(this.blob); + //return apiFileManager.uploadFile(this.blob); + return fetch('/upload', { + method: 'POST', + body: this.blob + }).then(res => res.json()); }); } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 76377d91..4b007465 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -18,7 +18,6 @@ import { mediaSizes } from '../lib/config'; import { MTDocument, MTPhotoSize } from '../types'; import animationIntersector from './animationIntersector'; import AudioElement from './audio'; -import MP4Source from '../lib/MP4Source'; export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: { doc: MTDocument, @@ -90,26 +89,18 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai container.append(video); } - let loadVideo = async() => { - let url: string; + const loadVideo = async() => { if(message.media.preloader) { // means upload (message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false); } else if(!doc.downloaded) { - const promise = appDocsManager.downloadVideo(doc.id); + const promise = appDocsManager.downloadDoc(doc.id); //if(!doc.supportsStreaming) { const preloader = new ProgressivePreloader(container, true); preloader.attach(container, true, promise, false); //} - - const mp4Source: MP4Source = await (promise as Promise); - if(mp4Source instanceof MP4Source) { - url = mp4Source.getURL(); - } - } - if(!url) { - url = doc.url; + await promise; } if(middleware && !middleware()) { @@ -131,7 +122,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai }, {once: true}); } - renderImageFromUrl(source, url); + renderImageFromUrl(source, doc.url); source.type = doc.mime_type; video.append(source); video.setAttribute('playsinline', ''); diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 1f38c5bc..4761c3c9 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -1,24 +1,15 @@ -import apiFileManager from '../mtproto/apiFileManager'; import FileManager from '../filemanager'; import {RichTextProcessor} from '../richtextprocessor'; import { CancellablePromise, deferredPromise } from '../polyfill'; -import { isObject } from '../utils'; +import { isObject, getFileURL } from '../utils'; import opusDecodeController from '../opusDecodeController'; -import { MTDocument } from '../../types'; -import MP4Source from '../MP4Source'; -import { bufferConcat } from '../bin_utils'; +import { MTDocument, inputDocumentFileLocation } from '../../types'; class AppDocsManager { private docs: {[docID: string]: MTDocument} = {}; private thumbs: {[docIDAndSize: string]: Promise} = {}; private downloadPromises: {[docID: string]: CancellablePromise} = {}; - private videoChunks: {[docID: string]: CancellablePromise[]} = {}; - private videoChunksQueue: {[docID: string]: {offset: number}[]} = {}; - - private loadedMP4Box: Promise; - private mp4Source: MP4Source; - public saveDoc(apiDoc: MTDocument, context?: any) { //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); if(this.docs[apiDoc.id]) { @@ -41,16 +32,6 @@ class AppDocsManager { this.docs[apiDoc.id] = apiDoc; - if(apiDoc.thumb && apiDoc.thumb._ == 'photoCachedSize') { - console.warn('this will happen!!!'); - apiFileManager.saveSmallFile(apiDoc.thumb.location, apiDoc.thumb.bytes); - - // Memory - apiDoc.thumb.size = apiDoc.thumb.bytes.length; - delete apiDoc.thumb.bytes; - apiDoc.thumb._ = 'photoSize'; - } - if(apiDoc.thumb && apiDoc.thumb._ == 'photoSizeEmpty') { delete apiDoc.thumb; } @@ -156,8 +137,8 @@ class AppDocsManager { return apiDoc; } - public getDoc(docID: any): MTDocument { - return isObject(docID) ? docID : this.docs[docID]; + public getDoc(docID: string | MTDocument): MTDocument { + return isObject(docID) && typeof(docID) !== 'string' ? docID : this.docs[docID as string]; } public getMediaInputByID(docID: any) { @@ -175,7 +156,7 @@ class AppDocsManager { }; } - public getInputByID(docID: any, thumbSize?: string) { + public getInputByID(docID: any, thumbSize?: string): inputDocumentFileLocation { let doc = this.getDoc(docID); return { @@ -200,189 +181,40 @@ class AppDocsManager { return 't_' + (doc.type || 'file') + doc.id + fileExt; } - private loadMP4Box() { - if(this.loadedMP4Box) return this.loadedMP4Box; - - return this.loadedMP4Box = new Promise((resolve, reject) => { - (window as any).mp4BoxLoaded = () => { - //console.log('webpHero loaded'); - this.mp4Source = (window as any).MP4Source; - resolve(); - }; - - let sc = document.createElement('script'); - sc.src = 'mp4box.all.min.js'; - sc.async = true; - sc.onload = (window as any).mp4BoxLoaded; - - document.body.appendChild(sc); - }); - } - - private createMP4Stream(doc: MTDocument) { - const limitPart = 524288; - const chunks = this.videoChunks[doc.id]; - const queue = this.videoChunksQueue[doc.id]; - - //let mp4Source = new MP4Source({duration: doc.duration, video: {expected_size: doc.size}}, (offset: number, end: number) => { - let mp4Source = new (this.mp4Source as any)({duration: doc.duration, video: {expected_size: doc.size}}, (offset: number, end: number) => { - const chunkStart = offset - (offset % limitPart); - - const sorted: typeof queue = []; - const lower: typeof queue = []; - for(let i = 0; i < queue.length; ++i) { - if(queue[i].offset >= chunkStart) { - sorted.push(queue[i]); - } else { - lower.push(queue[i]); - } - } - sorted.sort((a, b) => a.offset - b.offset).concat(lower).forEach((q, i) => { - queue[i] = q; - }); - - const index1 = offset / limitPart | 0; - const index2 = end / limitPart | 0; - - const p = chunks.slice(index1, index2 + 1); - - //console.log('MP4Source getBuffer:', offset, end, index1, index2, doc.size, JSON.stringify(queue)); - - if(offset % limitPart == 0) { - return p[0]; - } else { - return Promise.all(p).then(buffers => { - const buffer = buffers.length > 1 ? bufferConcat(buffers[0], buffers[1]) : buffers[0]; - const start = (offset % limitPart); - const _end = start + (end - offset); - - const sliced = buffer.slice(start, _end); - //console.log('slice buffer:', sliced); - return sliced; - }); - } - }); - - return mp4Source; - } - - private mp4Stream(doc: MTDocument, deferred: CancellablePromise) { - const limitPart = 524288; - const promises = this.videoChunks[doc.id] ?? (this.videoChunks[doc.id] = []); - if(!promises.length) { - for(let offset = 0; offset < doc.size; offset += limitPart) { - const deferred = deferredPromise(); - promises.push(deferred); - } - } - - let good = false; - return async(bytes: Uint8Array, offset: number, queue: {offset: number}[]) => { - if(!deferred.isFulfilled && !deferred.isRejected/* && offset == 0 */) { - this.videoChunksQueue[doc.id] = queue; - console.log('stream:', doc, doc.url, deferred); - //doc.url = mp4Source.getURL(); - //deferred.resolve(mp4Source); - deferred.resolve(); - good = true; - } else if(!good) { - //mp4Source.stop(); - //mp4Source = null; - promises.length = 0; - return; - } - - const index = offset % limitPart == 0 ? offset / limitPart : promises.length - 1; - promises[index].resolve(bytes.slice().buffer); - //console.log('i wont believe in you', doc, bytes, offset, promises, bytes.length, bytes.buffer.byteLength, bytes.slice().buffer); - //console.log('i wont believe in you', bytes, doc, bytes.length, offset); - }; - } - - public downloadVideo(docID: string): CancellablePromise { - const doc = this.getDoc(docID); - if(!doc.supportsStreaming || doc.url) { - return this.downloadDoc(docID); - } - - const deferred = deferredPromise(); - let canceled = false; - deferred.cancel = () => { - canceled = true; - }; - - this.loadMP4Box().then(() => { - if(canceled) { - throw 'canceled'; - } - - const promise = this.downloadDoc(docID); - - deferred.cancel = () => { - promise.cancel(); - }; - - promise.notify = (...args) => { - deferred.notify && deferred.notify(...args); - }; - - promise.then(() => { - if(doc.url) { // может быть уже загружен из кэша - deferred.resolve(); - } else { - deferred.resolve(this.createMP4Stream(doc)); - } - }); - }, deferred.reject); - - return deferred; + public getFileURLByDoc(doc: MTDocument) { + const inputFileLocation = this.getInputByID(doc); + return getFileURL('document', {dcID: doc.dc_id, location: inputFileLocation, size: doc.size, mimeType: doc.mime_type || 'application/octet-stream'}); } - public downloadDoc(docID: any, toFileEntry?: any): CancellablePromise { + public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise { const doc = this.getDoc(docID); if(doc._ == 'documentEmpty') { return Promise.reject(); } - const inputFileLocation = this.getInputByID(doc); if(doc.downloaded && !toFileEntry) { if(doc.url) return Promise.resolve(null); - const cachedBlob = apiFileManager.getCachedFile(inputFileLocation); + /* const cachedBlob = apiFileManager.getCachedFile(inputFileLocation); if(cachedBlob) { return Promise.resolve(cachedBlob); - } + } */ } if(this.downloadPromises[doc.id]) { return this.downloadPromises[doc.id]; } - //historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size}; - const deferred = deferredPromise(); - deferred.cancel = () => { - downloadPromise.cancel(); - }; - - const processPart = doc.supportsStreaming ? this.mp4Stream(doc, deferred) : undefined; - - // нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся - const downloadPromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { - mimeType: doc.mime_type || 'application/octet-stream', - toFileEntry: toFileEntry, - stickerType: doc.sticker, - processPart - }); - - downloadPromise.notify = (...args) => { - deferred.notify && deferred.notify(...args); - }; - //deferred.notify = downloadPromise.notify; + /* if(doc.supportsStreaming) { + doc.url = '/stream/' + ''; + } */ - downloadPromise.then((blob) => { + const url = this.getFileURLByDoc(doc); + fetch(url).then(res => res.blob()) + /* downloadPromise */.then((blob) => { if(blob) { doc.downloaded = true; @@ -405,11 +237,7 @@ class AppDocsManager { return; } else if(doc.type && doc.sticker != 2) { - /* if(processPart) { - console.log('stream after:', doc, doc.url, deferred); - } */ - - doc.url = URL.createObjectURL(blob); + doc.url = url; } } @@ -417,23 +245,10 @@ class AppDocsManager { }, (e) => { console.log('document download failed', e); deferred.reject(e); - //historyDoc.progress.enabled = false; }).finally(() => { - deferred.notify = downloadPromise.notify = deferred.cancel = downloadPromise.cancel = null; + //deferred.notify = downloadPromise.notify = deferred.cancel = downloadPromise.cancel = null; }); - /* downloadPromise.notify = (progress) => { - console.log('dl progress', progress); - historyDoc.progress.enabled = true; - historyDoc.progress.done = progress.done; - historyDoc.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - $rootScope.$broadcast('history_update'); - }; */ - - //historyDoc.progress.cancel = downloadPromise.cancel; - - //console.log('return downloadPromise:', downloadPromise); - return this.downloadPromises[doc.id] = deferred; } @@ -451,16 +266,8 @@ class AppDocsManager { return Promise.reject(); } - let mimeType = doc.sticker ? 'image/webp' : doc.mime_type; - let promise = apiFileManager.downloadSmallFile(input, { - dcID: doc.dc_id, - stickerType: doc.sticker ? 1 : undefined, - mimeType: mimeType - }); - - return this.thumbs[key] = promise.then((blob) => { - return URL.createObjectURL(blob); - }); + const url = getFileURL('thumb', {dcID: doc.dc_id, location: input, mimeType: doc.sticker ? 'image/webp' : doc.mime_type}); + return this.thumbs[key] = Promise.resolve(url); } public hasDownloadedThumb(docID: string, thumbSize: string) { diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 32754d8e..195f60c5 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -549,7 +549,7 @@ export class AppImManager { private closeBtn = this.topbar.querySelector('.sidebar-close-button') as HTMLButtonElement; constructor() { - this.log = logger('IM', LogLevels.log | LogLevels.error | LogLevels.warn | LogLevels.debug); + this.log = logger('IM', /* LogLevels.log | LogLevels.warn | LogLevels.debug | */ LogLevels.error); this.chatInputC = new ChatInput(); this.preloader = new ProgressivePreloader(null, false); this.selectTab(0); @@ -2641,7 +2641,7 @@ export class AppImManager { avatarElem.setAttribute('peer', '' + ((message.fwd_from && this.peerID == this.myID ? message.fwdFromID : message.fromID) || 0)); - this.log('exec loadDialogPhoto', message); + //this.log('exec loadDialogPhoto', message); bubbleContainer.append(avatarElem); } diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index a45f29ef..7d15a056 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -826,7 +826,7 @@ export class AppMediaViewer { if(!source.src || (media.url && media.url != source.src)) { const load = () => { - const promise = appDocsManager.downloadVideo(media.id); + const promise = appDocsManager.downloadDoc(media.id); const streamable = media.supportsStreaming && !media.url; //if(!streamable) { diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 4098adf4..0b058b06 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -11,7 +11,6 @@ import appPhotosManager from "./appPhotosManager"; import AppStorage from '../storage'; import appPeersManager from "./appPeersManager"; import ServerTimeManager from "../mtproto/serverTimeManager"; -import apiFileManager from "../mtproto/apiFileManager"; import appDocsManager from "./appDocsManager"; import ProgressivePreloader from "../../components/preloader"; import serverTimeManager from "../mtproto/serverTimeManager"; @@ -22,7 +21,8 @@ import { CancellablePromise, deferredPromise } from "../polyfill"; import appPollsManager from "./appPollsManager"; import searchIndexManager from '../searchIndexManager'; import { MTDocument, MTPhotoSize } from "../../types"; -import { logger } from "../logger"; +import { logger, LogLevels } from "../logger"; +import type {ApiFileManager} from '../mtproto/apiFileManager'; //console.trace('include'); @@ -595,7 +595,7 @@ export class AppMessagesManager { dialogs: [] }; - private log = logger('MESSAGES'/* , LogLevels.error */); + private log = logger('MESSAGES', LogLevels.error); public dialogsStorage = new DialogsStorage(); public filtersStorage = new FiltersStorage(); @@ -1179,7 +1179,7 @@ export class AppMessagesManager { }; var uploaded = false, - uploadPromise: ReturnType = null; + uploadPromise: ReturnType = null; let invoke = (flags: number, inputMedia: any) => { this.setTyping('sendMessageCancelAction'); @@ -1241,7 +1241,11 @@ export class AppMessagesManager { this.sendFilePromise.then(() => { if(!uploaded || message.error) { uploaded = false; - uploadPromise = apiFileManager.uploadFile(file); + //uploadPromise = apiFileManager.uploadFile(file); + uploadPromise = fetch('/upload', { + method: 'POST', + body: file + }).then(res => res.json()); } uploadPromise && uploadPromise.then((inputFile) => { @@ -1485,7 +1489,7 @@ export class AppMessagesManager { }; let uploaded = false, - uploadPromise: ReturnType = null; + uploadPromise: ReturnType = null; let inputPeer = appPeersManager.getInputPeerByID(peerID); let invoke = (multiMedia: any[]) => { @@ -1517,7 +1521,11 @@ export class AppMessagesManager { if(!uploaded || message.error) { uploaded = false; - uploadPromise = apiFileManager.uploadFile(file); + //uploadPromise = apiFileManager.uploadFile(file); + uploadPromise = fetch('/upload', { + method: 'POST', + body: file + }).then(res => res.json()); } uploadPromise.notify = (progress: {done: number, total: number}) => { diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 29582fcc..67f7f6bb 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -1,11 +1,10 @@ import appUsersManager from "./appUsersManager"; -import { calcImageInBox, isObject } from "../utils"; +import { calcImageInBox, isObject, getFileURL } from "../utils"; import fileManager from '../filemanager'; import { bytesFromHex } from "../bin_utils"; -import apiFileManager from "../mtproto/apiFileManager"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; -import { MTPhotoSize } from "../../types"; +import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, InputFileLocation, FileLocation } from "../../types"; export type MTPhoto = { _: 'photo' | 'photoEmpty' | string, @@ -250,17 +249,17 @@ export class AppPhotosManager { } public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): Promise { - let photo = this.getPhoto(photoID); + const photo = this.getPhoto(photoID); if(!photoSize) { - let fullWidth = this.windowW; - let fullHeight = this.windowH; + const fullWidth = this.windowW; + const fullHeight = this.windowH; photoSize = this.choosePhotoSize(photo, fullWidth, fullHeight); } - let isDocument = photo._ == 'document'; - let cacheContext = isDocument ? (this.documentThumbsCache[photo.id] ?? (this.documentThumbsCache[photo.id] = {downloaded: -1, url: ''})) : photo; + const isDocument = photo._ == 'document'; + const cacheContext = isDocument ? (this.documentThumbsCache[photo.id] ?? (this.documentThumbsCache[photo.id] = {downloaded: -1, url: ''})) : photo; if(cacheContext.downloaded >= photoSize.size && cacheContext.url) { return Promise.resolve(); @@ -273,7 +272,7 @@ export class AppPhotosManager { // maybe it's a thumb let isPhoto = photoSize.size && photo.access_hash && photo.file_reference; - let location = isPhoto ? { + let location: inputPhotoFileLocation | inputDocumentFileLocation | FileLocation = isPhoto ? { _: isDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation', id: photo.id, access_hash: photo.access_hash, @@ -281,19 +280,20 @@ export class AppPhotosManager { thumb_size: photoSize.type } : photoSize.location; + const url = getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}); let promise: Promise; if(isPhoto/* && photoSize.size >= 1e6 */) { - //console.log('Photos downloadFile exec', photo); - promise = apiFileManager.downloadFile(photo.dc_id, location, photoSize.size); + promise = fetch(url).then(res => res.blob()); } else { //console.log('Photos downloadSmallFile exec', photo, location); - promise = apiFileManager.downloadSmallFile(location); + promise = fetch(url).then(res => res.blob()); } promise.then(blob => { if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) { cacheContext.downloaded = blob.size; - cacheContext.url = URL.createObjectURL(blob); + //cacheContext.url = URL.createObjectURL(blob); + cacheContext.url = url; //console.log('wrote photo:', photo, photoSize, cacheContext, blob); } @@ -333,7 +333,7 @@ export class AppPhotosManager { var fullWidth = this.windowW; var fullHeight = this.windowH; var fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight); - var inputFileLocation = { + var inputFileLocation: inputDocumentFileLocation | inputPhotoFileLocation = { // @ts-ignore _: photo._ == 'document' ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation', id: photo.id, @@ -346,7 +346,7 @@ export class AppPhotosManager { let writer = fileManager.chooseSaveFile(fileName, ext, mimeType, fullPhotoSize.size); writer.ready.then(() => { console.log('ready'); - apiFileManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, { + apiManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, { mimeType: mimeType, toFileEntry: writer }).then(() => { @@ -360,12 +360,12 @@ export class AppPhotosManager { } catch(err) { console.error('err', err); - var cachedBlob = apiFileManager.getCachedFile(inputFileLocation) + /* var cachedBlob = apiFileManager.getCachedFile(inputFileLocation) if (cachedBlob) { return fileManager.download(cachedBlob, mimeType, fileName); - } + } */ - apiFileManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, {mimeType: mimeType}) + apiManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, {mimeType: mimeType}) .then((blob: Blob) => { fileManager.download(blob, mimeType, fileName); }, (e: any) => { diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 92c36bab..9664caa4 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -1,10 +1,9 @@ import AppStorage from '../storage'; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; -import apiFileManager from '../mtproto/apiFileManager'; import appDocsManager from './appDocsManager'; -import { MTDocument } from '../../types'; -import { $rootScope } from '../utils'; +import { MTDocument, inputStickerSetThumb } from '../../types'; +import { $rootScope, getFileURL } from '../utils'; export type MTStickerSet = { _: 'stickerSet', @@ -224,17 +223,17 @@ class AppStickersManager { const isAnimated = stickerSet.pFlags?.animated; - const promise = apiFileManager.downloadFile(dcID, { + const input: inputStickerSetThumb = { _: 'inputStickerSetThumb', stickerset: this.getStickerSetInput(stickerSet), volume_id: thumb.location.volume_id, local_id: thumb.location.local_id - }, thumb.size, { - stickerType: isAnimated ? 2 : 1, - mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp' - }); + }; + + const url = getFileURL('document', {dcID, location: input, size: thumb.size, mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp'}); + return fetch(url).then(res => res.blob()); - return promise; + //return promise; } public getStickerSetInput(set: {id: string, access_hash: string}) { diff --git a/src/lib/appManagers/appWebpManager.ts b/src/lib/appManagers/appWebpManager.ts index f3a922c6..35a517d4 100644 --- a/src/lib/appManagers/appWebpManager.ts +++ b/src/lib/appManagers/appWebpManager.ts @@ -71,6 +71,7 @@ class AppWebpManager { if(this.testPromise) return this.testPromise; return this.testPromise = new Promise((resolve, reject) => { + return resolve(this.webpSupport = true); let webP = new Image(); webP.src = 'data:image/webp;base64,UklGRi4AAABXRUJQVlA4TCEAAAAvAUAAEB8wAiMw' + 'AgSSNtse/cXjxyCCmrYNWPwmHRH9jwMA'; @@ -95,8 +96,8 @@ class AppWebpManager { } const appWebpManager = new AppWebpManager(); -// @ts-ignore +/* // @ts-ignore if(process.env.NODE_ENV != 'production') { (window as any).appWebpManager = appWebpManager; -} +} */ export default appWebpManager; diff --git a/src/lib/bin_utils.ts b/src/lib/bin_utils.ts index d7552c89..48871510 100644 --- a/src/lib/bin_utils.ts +++ b/src/lib/bin_utils.ts @@ -7,6 +7,7 @@ // @ts-ignore import {BigInteger, SecureRandom} from 'jsbn'; +import { InputFileLocation, FileLocation } from '../types'; /// #if !MTPROTO_WORKER // @ts-ignore @@ -380,3 +381,30 @@ export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, export function nextRandomInt(maxValue: number) { return Math.floor(Math.random() * maxValue); } + +export function getFileNameByLocation(location: InputFileLocation | FileLocation, options?: Partial<{ + fileName: string +}>) { + const fileName = (options?.fileName || '').split('.'); + const ext = fileName[fileName.length - 1] || ''; + + switch(location._) { + case 'inputPhotoFileLocation': + case 'inputDocumentFileLocation': { + const thumbPart = location.thumb_size ? '_' + location.thumb_size : ''; + return (fileName[0] ? fileName[0] + '_' : '') + location.id + thumbPart + (ext ? '.' + ext : ext); + } + + case 'fileLocationToBeDeprecated': + case 'inputPeerPhotoFileLocation': + case 'inputStickerSetThumb': + case 'inputFileLocation': { + return location.volume_id + '_' + location.local_id + (ext ? '.' + ext : ext); + } + + default: { + console.error('Unrecognized location:', location); + return ''; + } + } +} diff --git a/src/lib/cacheStorage.ts b/src/lib/cacheStorage.ts index a37ec0ff..13847b05 100644 --- a/src/lib/cacheStorage.ts +++ b/src/lib/cacheStorage.ts @@ -95,8 +95,8 @@ class CacheStorageController { } const cacheStorage = new CacheStorageController(); -// @ts-ignore +/* // @ts-ignore if(process.env.NODE_ENV != 'production') { (window as any).cacheStorage = cacheStorage; -} +} */ export default cacheStorage; diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index c512fdc5..d8b3e6d5 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -1,13 +1,12 @@ -import { nextRandomInt } from "../bin_utils"; +import { nextRandomInt, getFileNameByLocation } from "../bin_utils"; -//import IdbFileStorage from "../idb"; import cacheStorage from "../cacheStorage"; import FileManager from "../filemanager"; -//import apiManager from "./apiManager"; -import apiManager from "./mtprotoworker"; +import apiManager from "./apiManager"; import { deferredPromise, CancellablePromise } from "../polyfill"; import appWebpManager from "../appManagers/appWebpManager"; import { logger } from "../logger"; +import { InputFileLocation, FileLocation } from "../../types"; type Delayed = { offset: number, @@ -15,20 +14,15 @@ type Delayed = { writeFileDeferred: CancellablePromise }; +type DownloadOptions = Partial<{ + +}>; + export class ApiFileManager { - public cachedSavePromises: { - [fileName: string]: Promise - } = {}; public cachedDownloadPromises: { - [fileName: string]: any - } = {}; - public cachedDownloads: { - [fileName: string]: Blob + [fileName: string]: Promise } = {}; - /* public indexedKeys: Set = new Set(); - private keysLoaded = false; */ - public downloadPulls: { [x: string]: Array<{ cb: () => Promise, @@ -95,194 +89,26 @@ export class ApiFileManager { }); } - public getFileName(location: any, options?: Partial<{ - stickerType: number - }>) { - switch(location._) { - case 'inputDocumentFileLocation': { - let fileName = (location.file_name as string || '').split('.'); - let ext = fileName[fileName.length - 1] || ''; - - if(options?.stickerType == 1 && !appWebpManager.isSupported()) { - ext += '.png' - } - - let thumbPart = location.thumb_size ? '_' + location.thumb_size : ''; - return (fileName[0] ? fileName[0] + '_' : '') + location.id + thumbPart + (ext ? '.' + ext : ext); - } - - default: { - if(!location.volume_id && !location.file_reference) { - this.log.trace('Empty location', location); - } - - let ext = 'jpg'; - if(options?.stickerType == 1 && !appWebpManager.isSupported()) { - ext += '.png' - } - - if(location.volume_id) { - return location.volume_id + '_' + location.local_id + '.' + ext; - } else { - return location.id + '_' + location.access_hash + '.' + ext; - } - } - } - } - - public getTempFileName(file: any) { - const size = file.size || -1; - const random = nextRandomInt(0xFFFFFFFF); - return '_temp' + random + '_' + size; - } - - public getCachedFile(location: any) { - if(!location) { - return false; - } - const fileName = this.getFileName(location); - - return this.cachedDownloads[fileName] || false; - } - public getFileStorage() { return cacheStorage; } - /* public isFileExists(location: any) { - var fileName = this.getFileName(location); - - return this.cachedDownloads[fileName] || this.indexedKeys.has(fileName); - //return this.cachedDownloads[fileName] || this.indexedKeys.has(fileName) ? Promise.resolve(true) : this.getFileStorage().isFileExists(fileName); - } */ - - public saveSmallFile(location: any, bytes: Uint8Array) { - var fileName = this.getFileName(location); - - if(!this.cachedSavePromises[fileName]) { - this.cachedSavePromises[fileName] = this.getFileStorage().saveFile(fileName, bytes).then((blob: any) => { - return this.cachedDownloads[fileName] = blob; - }, (error: any) => { - delete this.cachedSavePromises[fileName]; - }); - } - return this.cachedSavePromises[fileName]; - } - - public downloadSmallFile(location: any, options: Partial<{ - mimeType: string, - dcID: number, - stickerType: number - }> = {}): Promise { + public downloadFile(options: { + dcID: number, + location: InputFileLocation | FileLocation, + size: number, + mimeType?: string, + toFileEntry?: any, + limitPart?: number, + stickerType?: number, + processPart?: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise + }): CancellablePromise { if(!FileManager.isAvailable()) { return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); } - /* if(!this.keysLoaded) { - this.getIndexedKeys(); - } */ - - //this.log('downloadSmallFile', location, options); - - let processSticker = false; - if(options.stickerType == 1 && !appWebpManager.isSupported()) { - processSticker = true; - options.mimeType = 'image/png'; - } - - let dcID = options.dcID || location.dc_id; - let mimeType = options.mimeType || 'image/jpeg'; - let fileName = this.getFileName(location, options); - let cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; - - //this.log('downloadSmallFile!', location, options, fileName, cachedPromise); - - if(cachedPromise) { - return cachedPromise; - } - - let fileStorage = this.getFileStorage(); - - return this.cachedDownloadPromises[fileName] = fileStorage.getFile(fileName).then((blob) => { - //throw ''; - //this.log('downloadSmallFile found photo by fileName:', fileName); - return this.cachedDownloads[fileName] = blob; - }).catch(() => { - //this.log.warn('downloadSmallFile found no photo by fileName:', fileName); - let downloadPromise = this.downloadRequest(dcID, () => { - let inputLocation = location; - if(!inputLocation._ || inputLocation._ == 'fileLocation') { - inputLocation = Object.assign({}, location, {_: 'inputFileLocation'}); - } - - let params = { - flags: 0, - location: inputLocation, - offset: 0, - limit: 1024 * 1024 - }; - - //this.log('next small promise', params); - return apiManager.invokeApi('upload.getFile', params, { - dcID: dcID, - fileDownload: true, - noErrorBox: true - }); - }, dcID); - - let processDownloaded = (bytes: Uint8Array) => { - //this.log('processDownloaded', location, bytes); - - if(processSticker) { - return appWebpManager.convertToPng(bytes); - } - - return Promise.resolve(bytes); - }; - - return fileStorage.getFileWriter(fileName, mimeType).then(fileWriter => { - return downloadPromise.then((result: any) => { - return processDownloaded(result.bytes).then((proccessedResult) => { - return FileManager.write(fileWriter, proccessedResult).then(() => { - return this.cachedDownloads[fileName] = fileWriter.finalize(); - }); - }); - }); - }); - }); - } - - public getDownloadedFile(location: any) { - var fileStorage = this.getFileStorage(); - var fileName = typeof(location) !== 'string' ? this.getFileName(location) : location; - - //console.log('getDownloadedFile', location, fileName); - - return fileStorage.getFile(fileName); - } - - /* public getIndexedKeys() { - this.keysLoaded = true; - this.getFileStorage().getAllKeys().then(keys => { - this.indexedKeys.clear(); - this.indexedKeys = new Set(keys); - }); - } */ - - public downloadFile(dcID: number, location: any, size: number, options: Partial<{ - mimeType: string, - toFileEntry: any, - limitPart: number, - stickerType: number, - processPart: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise - }> = {}): CancellablePromise { - if(!FileManager.isAvailable()) { - return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); - } - - /* if(!this.keysLoaded) { - this.getIndexedKeys(); - } */ + let size = options.size ?? 0; + let {dcID, location} = options; let processSticker = false; if(options.stickerType == 1 && !appWebpManager.isSupported()) { @@ -295,12 +121,12 @@ export class ApiFileManager { } // this.log('Dload file', dcID, location, size) - const fileName = this.getFileName(location, options); + const fileName = getFileNameByLocation(location); const toFileEntry = options.toFileEntry || null; - const cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; + const cachedPromise = this.cachedDownloadPromises[fileName]; const fileStorage = this.getFileStorage(); - //this.log('downloadFile', fileStorage.name, fileName, fileName.length, location, arguments); + //this.log('downloadFile', fileName, fileName.length, location, arguments); if(cachedPromise) { if(toFileEntry) { @@ -317,9 +143,9 @@ export class ApiFileManager { this.log('downloadFile need to deleteFile, wrong size:', blob.size, size); return this.deleteFile(fileName).then(() => { - return this.downloadFile(dcID, location, size, options); + return this.downloadFile(options); }).catch(() => { - return this.downloadFile(dcID, location, size, options); + return this.downloadFile(options); }); } else { return blob; @@ -346,7 +172,7 @@ export class ApiFileManager { }; fileStorage.getFile(fileName).then(async(blob: Blob) => { - //this.log('is that i wanted'); + //this.log('maybe cached', fileName); //throw ''; if(blob.size < size) { @@ -358,10 +184,10 @@ export class ApiFileManager { if(toFileEntry) { FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler); } else { - deferred.resolve(this.cachedDownloads[fileName] = blob); + deferred.resolve(blob); } }).catch(() => { - //this.log('not i wanted'); + //this.log('not cached', fileName); //var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType); const fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType); @@ -374,6 +200,10 @@ export class ApiFileManager { writeFileDeferred: CancellablePromise; const maxRequests = options.processPart ? 5 : 5; + if(!size) { + size = limit; + } + if(fileWriter.length) { startOffset = fileWriter.length; @@ -381,7 +211,7 @@ export class ApiFileManager { if(toFileEntry) { deferred.resolve(); } else { - deferred.resolve(this.cachedDownloads[fileName] = fileWriter.finalize()); + deferred.resolve(fileWriter.finalize()); } return; @@ -475,7 +305,7 @@ export class ApiFileManager { if(toFileEntry) { deferred.resolve(); } else { - deferred.resolve(this.cachedDownloads[fileName] = fileWriter.finalize()); + deferred.resolve(fileWriter.finalize()); } } } catch(err) { @@ -511,11 +341,7 @@ export class ApiFileManager { public deleteFile(fileName: string) { //this.log('will delete file:', fileName); - delete this.cachedDownloadPromises[fileName]; - delete this.cachedDownloads[fileName]; - delete this.cachedSavePromises[fileName]; - return this.getFileStorage().deleteFile(fileName); } @@ -638,4 +464,9 @@ export class ApiFileManager { } } -export default new ApiFileManager(); +const apiFileManager = new ApiFileManager(); +// @ts-ignore +if(process.env.NODE_ENV != 'production') { + (self as any).apiFileManager = apiFileManager; +} +export default apiFileManager; diff --git a/src/lib/mtproto/mtproto.service.ts b/src/lib/mtproto/mtproto.service.ts index b4c68edd..f64176a9 100644 --- a/src/lib/mtproto/mtproto.service.ts +++ b/src/lib/mtproto/mtproto.service.ts @@ -6,6 +6,7 @@ import apiManager from "./apiManager"; import AppStorage from '../storage'; import cryptoWorker from "../crypto/cryptoworker"; import networkerFactory from "./networkerFactory"; +import apiFileManager from './apiFileManager'; const ctx = self as any as ServiceWorkerGlobalScope; @@ -85,14 +86,16 @@ networkerFactory.setUpdatesProcessor((obj, bool) => { return; } - listeners[0].postMessage({update: {obj, bool}}); + listeners.forEach(listener => { + listener.postMessage({update: {obj, bool}}); + }); }); }); ctx.addEventListener('message', async(e) => { const taskID = e.data.taskID; - console.log('[SW] Got message:', taskID, e, e.data); + //console.log('[SW] Got message:', taskID, e, e.data); if(e.data.useLs) { AppStorage.finishTask(e.data.taskID, e.data.args); @@ -107,6 +110,24 @@ ctx.addEventListener('message', async(e) => { respond(e.source, {taskID: taskID, result: result}); }); + case 'downloadFile': { + /* // @ts-ignore + return apiFileManager.downloadFile(...e.data.args); */ + + try { + // @ts-ignore + let result = apiFileManager[e.data.task].apply(apiFileManager, e.data.args); + + if(result instanceof Promise) { + result = await result; + } + + respond(e.source, {taskID: taskID, result: result}); + } catch(err) { + respond(e.source, {taskID: taskID, error: err}); + } + } + default: { try { // @ts-ignore @@ -151,3 +172,104 @@ ctx.addEventListener('activate', (event) => { event.waitUntil(ctx.clients.claim()); }); + +function timeout(delay: number): Promise { + return new Promise(((resolve) => { + setTimeout(() => { + resolve(new Response('', { + status: 408, + statusText: 'Request timed out.', + })); + }, delay); + })); +} + +/** + * Fetch requests + */ +ctx.addEventListener('fetch', (event: FetchEvent): void => { + const [, url, scope, fileName] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || []; + + //console.log('[SW] fetch:', event, event.request, url, scope, fileName); + + switch(scope) { + case 'thumb': + case 'document': + case 'photo': { + const info = JSON.parse(decodeURIComponent(fileName)); + + //console.log('[SW] fetch cachedDownloadPromises:', info/* apiFileManager.cachedDownloadPromises, apiFileManager.cachedDownloadPromises.hasOwnProperty(fileName) */); + + const promise = apiFileManager.downloadFile(info).then(b => new Response(b)); + event.respondWith(promise); + + break; + } + + case 'upload': { + if(event.request.method == 'POST') { + event.respondWith(event.request.blob().then(blob => { + return apiFileManager.uploadFile(blob).then(v => new Response(JSON.stringify(v), {headers: {'Content-Type': 'application/json'}})); + })); + } + + break; + } + + /* default: { + + break; + } + case 'documents': + case 'photos': + case 'profiles': + // direct download + if (event.request.method === 'POST') { + event.respondWith(// download(url, 'unknown file.txt', getFilePartRequest)); + event.request.text() + .then((text) => { + const [, filename] = text.split('='); + return download(url, filename ? filename.toString() : 'unknown file', getFilePartRequest); + }), + ); + + // inline + } else { + event.respondWith( + ctx.cache.match(url).then((cached) => { + if (cached) return cached; + + return Promise.race([ + timeout(45 * 1000), // safari fix + new Promise((resolve) => { + fetchRequest(url, resolve, getFilePartRequest, ctx.cache, fileProgress); + }), + ]); + }), + ); + } + break; + + case 'stream': { + const [offset, end] = parseRange(event.request.headers.get('Range') || ''); + + log('stream', url, offset, end); + + event.respondWith(new Promise((resolve) => { + fetchStreamRequest(url, offset, end, resolve, getFilePartRequest); + })); + break; + } + + case 'stripped': + case 'cached': { + const bytes = getThumb(url) || null; + event.respondWith(new Response(bytes, { headers: { 'Content-Type': 'image/jpg' } })); + break; + } + + default: + if (url && url.endsWith('.tgs')) event.respondWith(fetchTGS(url)); + else event.respondWith(fetch(event.request.url)); */ + } +}); diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index eae92d7a..4deec3d5 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -2,6 +2,7 @@ import {dT, isObject, $rootScope} from '../utils'; import AppStorage from '../storage'; import CryptoWorkerMethods from '../crypto/crypto_methods'; import runtime from 'serviceworker-webpack-plugin/lib/runtime'; +import { InputFileLocation, FileLocation } from '../../types'; type Task = { taskID: number, @@ -45,14 +46,14 @@ class ApiManagerProxy extends CryptoWorkerMethods { this.releasePending(); }); - navigator.serviceWorker.oncontrollerchange = () => { - console.error('oncontrollerchange'); + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.warn(dT(), 'ApiManagerProxy controllerchange'); this.releasePending(); navigator.serviceWorker.controller.addEventListener('error', (e) => { console.error('controller error:', e); }); - }; + }); /** * Message resolver @@ -75,41 +76,6 @@ class ApiManagerProxy extends CryptoWorkerMethods { this.finalizeTask(e.data.taskID, e.data.result, e.data.error); } }); - - /* if(window.Worker) { - import('./mtproto_service.js').then((worker: any) => { - var tmpWorker = new worker.default(); - tmpWorker.onmessage = (e: any) => { - if(!this.webWorker) { - this.webWorker = tmpWorker; - console.info(dT(), 'ApiManagerProxy set webWorker'); - this.releasePending(); - } - - if(!isObject(e.data)) { - return; - } - - if(e.data.useLs) { - // @ts-ignore - AppStorage[e.data.task](...e.data.args).then(res => { - (this.webWorker as Worker).postMessage({useLs: true, taskID: e.data.taskID, args: res}); - }); - } else if(e.data.update) { - if(this.updatesProcessor) { - this.updatesProcessor(e.data.update.obj, e.data.update.bool); - } - } else { - this.finalizeTask(e.data.taskID, e.data.result, e.data.error); - } - }; - - tmpWorker.onerror = (error: any) => { - console.error('ApiManagerProxy error', error); - this.webWorker = false; - }; - }); - } */ } private finalizeTask(taskID: number, result: any, error: any) { @@ -127,13 +93,12 @@ class ApiManagerProxy extends CryptoWorkerMethods { return new Promise((resolve, reject) => { this.awaiting[this.taskID] = {resolve, reject, taskName: task}; - let params = { + const params = { task, taskID: this.taskID, args }; - //(this.webWorker as Worker).postMessage(params); this.pending.push(params); this.releasePending(); @@ -194,6 +159,15 @@ class ApiManagerProxy extends CryptoWorkerMethods { public logOut(): Promise { return this.performTaskWorker('logOut'); } + + public downloadFile(dcID: number, location: InputFileLocation | FileLocation, size: number = 0, options: Partial<{ + mimeType: string, + toFileEntry: any, + limitPart: number, + stickerType: number + }> = {}): Promise { + return this.performTaskWorker('downloadFile', dcID, location, size, options); + } } const apiManagerProxy = new ApiManagerProxy(); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c23a5800..cd9e5d95 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,6 +4,9 @@ * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ + +import { InputFileLocation, FileLocation } from "../types"; + var _logTimer = Date.now(); export function dT () { return '[' + ((Date.now() - _logTimer) / 1000).toFixed(3) + ']'; @@ -519,3 +522,17 @@ export function getEmojiToneIndex(input: string) { let match = input.match(/[\uDFFB-\uDFFF]/); return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0; } + +export function getFileURL(type: 'photo' | 'thumb' | 'document', options: { + dcID: number, + location: InputFileLocation | FileLocation, + size?: number, + mimeType?: string +}) { + //console.log('getFileURL', location); + //const perf = performance.now(); + const encoded = encodeURIComponent(JSON.stringify(options)); + //console.log('getFileURL encode:', performance.now() - perf, encoded); + + return '/' + type + '/' + encoded; +} diff --git a/src/types.d.ts b/src/types.d.ts index 6c00c499..b71e49b7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -41,7 +41,7 @@ export type MTPhotoSize = { h?: number, size?: number, type?: string, // i, m, x, y, w by asc - location?: any, + location?: FileLocation, bytes?: Uint8Array // if type == 'i' }; @@ -87,4 +87,46 @@ export type AccountPassword = { srp_B?: Uint8Array, srp_id?: string, secure_random: Uint8Array, -}; \ No newline at end of file +}; + +export type FileLocation = { + _: 'fileLocationToBeDeprecated', + volume_id: string, + local_id: number +}; + +export type inputFileLocation = { + _: 'inputFileLocation', + volume_id: string, + local_id: number, + secret: string, + file_reference: Uint8Array | number[] +}; + +export type inputDocumentFileLocation = { + _: 'inputDocumentFileLocation', + id: string, + access_hash: string, + file_reference: Uint8Array | number[], + thumb_size: string +}; + +export type inputPhotoFileLocation = Omit & {_: 'inputPhotoFileLocation'}; + +export type inputPeerPhotoFileLocation = { + _: 'inputPeerPhotoFileLocation', + flags: number, + big?: true, + peer: any, + volume_id: string, + local_id: number +}; + +export type inputStickerSetThumb = { + _: 'inputStickerSetThumb', + stickerset: any, + volume_id: string, + local_id: number +}; + +export type InputFileLocation = inputFileLocation | inputDocumentFileLocation | inputPhotoFileLocation | inputPeerPhotoFileLocation | inputStickerSetThumb; \ No newline at end of file