Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

906 lines
30 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import type {ReferenceBytes} from './referenceDatabase';
import Modes from '../../config/modes';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import {randomLong} from '../../helpers/random';
import {Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, WebDocument} from '../../layer';
import {DcId} from '../../types';
import CacheStorageController from '../files/cacheStorage';
import {logger, LogTypes} from '../logger';
import assumeType from '../../helpers/assumeType';
import noop from '../../helpers/noop';
import readBlobAsArrayBuffer from '../../helpers/blob/readBlobAsArrayBuffer';
import bytesToHex from '../../helpers/bytes/bytesToHex';
import findAndSplice from '../../helpers/array/findAndSplice';
import fixFirefoxSvg from '../../helpers/fixFirefoxSvg';
import {AppManager} from '../appManagers/manager';
import {getEnvironment} from '../../environment/utils';
import MTProtoMessagePort from './mtprotoMessagePort';
import getFileNameForUpload from '../../helpers/getFileNameForUpload';
import type {Progress} from '../appManagers/appDownloadManager';
import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails';
import networkStats from './networkStats';
import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions';
import StreamWriter from '../files/streamWriter';
import FileStorage from '../files/fileStorage';
import {MAX_FILE_SAVE_SIZE} from './mtproto_config';
import throttle from '../../helpers/schedulers/throttle';
import makeError from '../../helpers/makeError';
import readBlobAsUint8Array from '../../helpers/blob/readBlobAsUint8Array';
import DownloadStorage from '../files/downloadStorage';
import copy from '../../helpers/object/copy';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
type Delayed = {
offset: number,
writePromise: CancellablePromise<void>,
writeDeferred: CancellablePromise<void>
};
export type DownloadOptions = {
dcId: DcId,
location: InputFileLocation | InputWebFileLocation,
size?: number,
fileName?: string,
mimeType?: string,
limitPart?: number,
queueId?: number,
onlyCache?: boolean,
downloadId?: string
// getFileMethod: Parameters<CacheStorageController['getFile']>[1]
};
export type DownloadMediaOptions = {
media: Photo.photo | Document.document | WebDocument,
thumb?: PhotoSize,
queueId?: number,
onlyCache?: boolean,
downloadId?: string
};
type DownloadPromise = CancellablePromise<Blob>;
export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile;
// export interface RefreshReferenceTask extends WorkerTaskVoidTemplate {
// type: 'refreshReference',
// payload: ReferenceBytes,
// };
// export interface RefreshReferenceTaskResponse extends WorkerTaskVoidTemplate {
// type: 'refreshReference',
// payload: ReferenceBytes,
// originalPayload: ReferenceBytes
// };
const MAX_DOWNLOAD_FILE_PART_SIZE = 1 * 1024 * 1024;
const MAX_UPLOAD_FILE_PART_SIZE = 512 * 1024;
const MIN_PART_SIZE = 128 * 1024;
const AVG_PART_SIZE = 512 * 1024;
const REGULAR_DOWNLOAD_DELTA = (9 * 512 * 1024) / MIN_PART_SIZE;
const PREMIUM_DOWNLOAD_DELTA = REGULAR_DOWNLOAD_DELTA * 2;
const IGNORE_ERRORS: Set<ErrorType> = new Set([
'DOWNLOAD_CANCELED',
'UPLOAD_CANCELED',
'UNKNOWN',
'NO_NEW_CONTEXT'
]);
export class ApiFileManager extends AppManager {
private cacheStorage = new CacheStorageController('cachedFiles');
private downloadStorage = new DownloadStorage();
private downloadPromises: {
[fileName: string]: DownloadPromise
} = {};
// private downloadToDiscPromises: {
// [fileName: string]: DownloadPromise
// } = {};
private uploadPromises: {
[fileName: string]: CancellablePromise<InputFile>
} = {};
private downloadPulls: {
[dcId: string]: Array<{
id: number,
queueId: number,
cb: () => Promise<MyUploadFile | void>,
deferred: {
resolve: (...args: any[]) => void,
reject: (...args: any[]) => void
},
activeDelta: number
}>
} = {};
private downloadActives: {[dcId: string]: number} = {};
public refreshReferencePromises: {
[referenceHex: string]: {
deferred: CancellablePromise<ReferenceBytes>,
timeout?: number
}
} = {};
private log: ReturnType<typeof logger> = logger('AFM', LogTypes.Error | LogTypes.Log);
private tempId = 0;
private queueId = 0;
private debug = Modes.debug;
private maxUploadParts = 4000;
private maxDownloadParts = 8000;
private webFileDcId: DcId;
protected after() {
setInterval(() => { // clear old promises
for(const hex in this.refreshReferencePromises) {
const {deferred} = this.refreshReferencePromises[hex];
if(deferred.isFulfilled || deferred.isRejected) {
delete this.refreshReferencePromises[hex];
}
}
}, 1800e3);
this.rootScope.addEventListener('config', (config) => {
this.webFileDcId = config.webfile_dc_id;
});
this.rootScope.addEventListener('app_config', (appConfig) => {
this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default;
this.maxDownloadParts = appConfig.upload_max_fileparts_premium;
});
}
private downloadRequest(dcId: 'upload', id: number, cb: () => Promise<void>, activeDelta: number, queueId?: number): Promise<void>;
private downloadRequest(dcId: number, id: number, cb: () => Promise<MyUploadFile>, activeDelta: number, queueId?: number): Promise<MyUploadFile>;
private downloadRequest(dcId: number | string, id: number, cb: () => Promise<MyUploadFile | void>, activeDelta: number, queueId: number = 0) {
if(this.downloadPulls[dcId] === undefined) {
this.downloadPulls[dcId] = [];
this.downloadActives[dcId] = 0;
}
const downloadPull = this.downloadPulls[dcId];
const promise = new Promise<MyUploadFile | void>((resolve, reject) => {
downloadPull.push({id, queueId, cb, deferred: {resolve, reject}, activeDelta});
});
setTimeout(() => {
this.downloadCheck(dcId);
}, 0);
return promise;
}
private downloadCheck(dcId: string | number) {
const downloadPull = this.downloadPulls[dcId];
const downloadLimit = /* dcId === 'upload' ? 24 : */(this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA);
// const downloadLimit = Infinity;
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) {
return false;
}
// const data = downloadPull.shift();
const data = findAndSplice(downloadPull, (d) => d.queueId === 0) || findAndSplice(downloadPull, (d) => d.queueId === this.queueId) || downloadPull.shift();
const activeDelta = data.activeDelta || 1;
this.downloadActives[dcId] += activeDelta;
const promise = data.cb();
const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * MIN_PART_SIZE);
Promise.race([
promise,
networkPromise
]).then(() => {
this.downloadActives[dcId] -= activeDelta;
this.downloadCheck(dcId);
networkPromise.resolve();
}, (error: ApiError) => {
if(!error?.type || !IGNORE_ERRORS.has(error.type)) {
this.log.error('downloadCheck error:', error);
}
this.downloadActives[dcId] -= activeDelta;
this.downloadCheck(dcId);
networkPromise.reject(error);
}).finally(() => {
promise.then(data.deferred.resolve, data.deferred.reject);
});
}
public setQueueId(queueId: number) {
// this.log.error('setQueueId', queueId);
this.queueId = queueId;
}
private getFileStorage() {
return this.cacheStorage;
}
public cancelDownload(fileName: string) {
const promises = [this.downloadPromises[fileName], this.uploadPromises[fileName]].filter(Boolean);
let canceled = false;
for(let i = 0, length = promises.length; i < length; ++i) {
const promise = promises[i];
if(promise && !promise.isRejected && !promise.isFulfilled) {
promise.cancel();
canceled = true;
}
}
return canceled;
}
public requestWebFilePart(dcId: DcId, location: InputWebFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) {
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
checkCancel?.();
return this.apiManager.invokeApi('upload.getWebFile', {
location,
offset,
limit
}, {
dcId,
fileDownload: true
});
}, this.getDelta(limit), queueId);
}
public requestFilePart(
dcId: DcId,
location: InputFileLocation,
offset: number,
limit: number,
id = 0,
queueId = 0,
checkCancel?: () => void
) {
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
checkCancel?.();
const invoke = async(): Promise<MyUploadFile> => {
checkCancel?.(); // do not remove async, because checkCancel will throw an error
// * IMPORTANT: reference can be changed in previous request
const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice();
const promise = // pause(offset > (100 * 1024 * 1024) ? 10000000 : 0).then(() =>
this.apiManager.invokeApi('upload.getFile', {
location,
offset,
limit
}, {
dcId,
fileDownload: true
}) as Promise<MyUploadFile>/* ) */;
return promise.catch((err: ApiError) => {
checkCancel?.();
if(err.type === 'FILE_REFERENCE_EXPIRED') {
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke);
}
throw err;
});
};
assumeType<InputFileLocation.inputDocumentFileLocation>(location);
const reference = location.file_reference;
if(reference && !location.checkedReference) { // check stream's location because it's new every call
location.checkedReference = true;
const hex = bytesToHex(reference);
if(this.refreshReferencePromises[hex]) {
return this.refreshReference(location, reference).then(invoke);
}
}
return invoke();
}, this.getDelta(limit), queueId);
}
/* private convertBlobToBytes(blob: Blob) {
return blob.arrayBuffer().then((buffer) => new Uint8Array(buffer));
} */
private getDelta(bytes: number) {
return bytes / MIN_PART_SIZE;
}
private getLimitPart(size: number, isUpload: boolean): number {
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
return AVG_PART_SIZE;
}
// return 1 * 1024 * 1024;
let bytes = MIN_PART_SIZE;
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts;
const maxPartSize = isUpload ? MAX_UPLOAD_FILE_PART_SIZE : MAX_DOWNLOAD_FILE_PART_SIZE;
// usually it will stick to 512Kb size if the file is too big
while((size / bytes) > maxParts && bytes < maxPartSize) {
bytes *= 2;
}
/* if(size < 1e6 || !size) bytes = 512;
else if(size < 3e6) bytes = 256;
else bytes = 128; */
return bytes;
}
private uncompressTGS = (bytes: Uint8Array, fileName: string) => {
// this.log('uncompressTGS', bytes, bytes.slice().buffer);
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
return this.cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, false) as Promise<Uint8Array>;
};
private uncompressTGV = (bytes: Uint8Array, fileName: string) => {
// this.log('uncompressTGS', bytes, bytes.slice().buffer);
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
const buffer = bytes.slice().buffer;
if(getEnvironment().IS_FIREFOX) {
return this.cryptoWorker.invokeCrypto('gzipUncompress', buffer, true).then((text) => {
return fixFirefoxSvg(text as string);
}).then((text) => {
const textEncoder = new TextEncoder();
return textEncoder.encode(text);
});
}
return this.cryptoWorker.invokeCrypto('gzipUncompress', buffer, false) as Promise<Uint8Array>;
};
private convertWebp = (bytes: Uint8Array, fileName: string) => {
const instance = MTProtoMessagePort.getInstance<false>();
return instance.invoke('convertWebp', {fileName, bytes});
};
private convertOpus = (bytes: Uint8Array, fileName: string) => {
const instance = MTProtoMessagePort.getInstance<false>();
return instance.invoke('convertOpus', {fileName, bytes});
};
private refreshReference(
inputFileLocation: InputFileLocation.inputDocumentFileLocation,
reference: typeof inputFileLocation['file_reference'],
hex = bytesToHex(reference)
) {
let r = this.refreshReferencePromises[hex];
if(!r) {
const deferred = deferredPromise<ReferenceBytes>();
r = this.refreshReferencePromises[hex] = {
deferred
// ! I don't remember what it was for...
// timeout: ctx.setTimeout(() => {
// this.log.error('Didn\'t refresh the reference:', inputFileLocation);
// deferred.reject(makeError('REFERENCE_IS_NOT_REFRESHED'));
// }, 60000)
};
// deferred.catch(noop).finally(() => {
// clearTimeout(r.timeout);
// });
this.referenceDatabase.refreshReference(reference).then((reference) => {
if(hex === bytesToHex(reference)) {
deferred.reject(makeError('REFERENCE_IS_NOT_REFRESHED'));
}
deferred.resolve(reference);
}, deferred.reject);
}
// have to replace file_reference in any way, because location can be different everytime if it's stream
return r.deferred.then((reference) => {
inputFileLocation.file_reference = reference;
});
}
public isDownloading(fileName: string) {
return !!this.downloadPromises[fileName];
}
public getDownload(fileName: string) {
return this.downloadPromises[fileName];
}
public getUpload(fileName: string) {
return this.uploadPromises[fileName];
}
private getConvertMethod(mimeType: string) {
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
if(mimeType === 'application/x-tgwallpattern') {
process = this.uncompressTGV;
mimeType = 'image/svg+xml';
} else if(mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) {
process = this.convertWebp;
mimeType = 'image/png';
} else if(mimeType === 'application/x-tgsticker') {
process = this.uncompressTGS;
mimeType = 'application/json';
} else if(mimeType === 'audio/ogg' && !getEnvironment().IS_OPUS_SUPPORTED) {
process = this.convertOpus;
mimeType = 'audio/wav';
}
return {mimeType, process};
}
private allocateDeferredPromises(startOffset: number, size: number, limitPart: number) {
const delayed: Delayed[] = [];
let offset = startOffset;
let writePromise: CancellablePromise<void> = Promise.resolve(),
writeDeferred: CancellablePromise<void>;
do {
writeDeferred = deferredPromise<void>();
delayed.push({offset, writePromise, writeDeferred});
writePromise = writeDeferred;
offset += limitPart;
} while(offset < size);
return delayed;
}
public download(options: DownloadOptions): DownloadPromise {
const size = options.size ?? 0;
const {dcId, location, downloadId} = options;
const originalMimeType = options.mimeType;
const convertMethod = this.getConvertMethod(originalMimeType);
const {process} = convertMethod;
options.mimeType = convertMethod.mimeType || 'image/jpeg';
const fileName = getDownloadFileNameFromOptions(options);
const cacheFileName = downloadId ? getDownloadFileNameFromOptions({...copy(options), downloadId: undefined}) : fileName;
const cacheStorage: FileStorage = this.getFileStorage();
const downloadStorage: FileStorage = downloadId ? this.downloadStorage : undefined;
let deferred: DownloadPromise = downloadId ? undefined : this.downloadPromises[fileName];
this.debug && this.log('downloadFile', fileName, options);
if(deferred) {
return deferred;
}
// if(deferred) {
// if(size) {
// return deferred.then(async(blob) => {
// if(blob instanceof Blob && blob.size < size) {
// this.debug && this.log('downloadFile need to deleteFile, wrong size:', blob.size, size);
// try {
// await this.delete(fileName);
// } finally {
// return this.download(options);
// }
// } else {
// return blob;
// }
// });
// } else {
// return deferred;
// }
// }
const errorHandler = (item: typeof cachePrepared, error: ApiError) => {
if(item?.error) {
return;
}
for(const p of prepared) {
if(item && item !== p) {
continue;
}
p.error = error;
p.deferred.reject(error);
}
};
const id = this.tempId++;
const limitPart = options.limitPart || this.getLimitPart(size, false);
let getFile: FileStorage['getFile'] = cacheStorage.getFile.bind(cacheStorage);
let cachePrepared: ReturnType<FileStorage['prepareWriting']> & {writer?: StreamWriter, error?: ApiError},
downloadPrepared: typeof cachePrepared;
const prepared: (typeof cachePrepared)[] = [];
const possibleSize = size || limitPart;
const getErrorsCount = () => prepared.reduce((acc, item) => acc + +!!item.error, 0);
const attach = (item: typeof cachePrepared, fileName: string) => {
const {deferred} = item;
const _errorHandler = errorHandler.bind(null, item);
deferred.cancel = () => deferred.reject(makeError('DOWNLOAD_CANCELED'));
deferred.catch((error) => {
_errorHandler(error);
item.writer?.truncate?.();
}).finally(() => {
if(this.downloadPromises[fileName] === deferred) {
delete this.downloadPromises[fileName];
}
delete item.writer;
indexOfAndSplice(prepared, item);
});
this.downloadPromises[fileName] = deferred;
prepared.push(item);
};
if(cacheStorage && (!downloadStorage || possibleSize <= MAX_FILE_SAVE_SIZE)) {
cachePrepared = cacheStorage.prepareWriting(cacheFileName, possibleSize, options.mimeType)
attach(cachePrepared, cacheFileName);
}
if(downloadStorage) {
downloadPrepared = downloadStorage.prepareWriting({
fileName: options.fileName, // it's doc file_name
downloadId,
size: possibleSize
});
attach(downloadPrepared, fileName);
if(cachePrepared) { // cancel cache too
downloadPrepared.deferred.catch((err) => cachePrepared.deferred.reject(err));
}
// this.downloadToDiscPromises[cacheFileName] = deferred;
// deferred.catch(noop).finally(() => {
// if(this.downloadToDiscPromises[cacheFileName] === deferred) {
// delete this.downloadToDiscPromises[cacheFileName];
// }
// });
}
deferred = downloadPrepared?.deferred ?? cachePrepared.deferred;
if(downloadStorage && process) { // then have to load file again
getFile = downloadStorage.getFile.bind(downloadStorage);
}
getFile(cacheFileName).then(async(blob: Blob) => {
checkCancel();
// if(blob.size < size) {
// if(!options.onlyCache) {
// await this.delete(cacheFileName);
// checkCancel();
// }
// throw makeError('NO_ENTRY_FOUND');
// }
if(downloadPrepared) {
const writer = downloadPrepared.writer = downloadPrepared.getWriter();
checkCancel();
const arr = await readBlobAsUint8Array(blob);
checkCancel();
await writer.write(arr);
checkCancel();
downloadPrepared.deferred.resolve(await writer.finalize());
}
if(cachePrepared) {
cachePrepared.deferred.resolve(blob);
}
}).catch(async(err: ApiError) => {
if(options.onlyCache) {
errorHandler(null, err);
return;
}
prepared.forEach((p) => {
p.writer = p.getWriter();
});
const maxRequests = Infinity;
const isWebFile = location._ === 'inputWebFileLocation';
const requestPart = (isWebFile ? this.requestWebFilePart : this.requestFilePart).bind(this);
if(isWebFile && this.webFileDcId === undefined) {
await this.apiManager.getConfig();
checkCancel();
}
const delayed = this.allocateDeferredPromises(0, size, limitPart);
const progress: Progress = {done: 0, offset: 0, total: size, fileName};
const dispatchProgress = () => {
try {
checkCancel();
progress.done = done;
this.rootScope.dispatchEvent('download_progress', progress);
} catch(err) {}
};
const throttledDispatchProgress = throttle(dispatchProgress, 50, true);
let done = 0;
const superpuper = async() => {
const {offset, writePromise, writeDeferred} = delayed.shift();
try {
checkCancel();
const requestPerf = performance.now();
const result = await requestPart(dcId, location as any, offset, limitPart, id, options.queueId, checkCancel);
const requestTime = performance.now() - requestPerf;
const bytes = result.bytes;
if(delayed.length) {
superpuper();
}
const byteLength = bytes.byteLength;
this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
const isFinal = (offset + limitPart) >= size || !byteLength;
if(byteLength) {
done += byteLength;
if(isFinal) {
dispatchProgress();
} else {
throttledDispatchProgress();
}
const writeQueuePerf = performance.now();
await writePromise;
checkCancel();
const writeQueueTime = performance.now() - writeQueuePerf;
const perf = performance.now();
await Promise.all(prepared.map(({writer}) => writer?.write(bytes, offset)));
checkCancel();
// downloadId && this.log('write time', performance.now() - perf, 'request time', requestTime, 'queue time', writeQueueTime);
}
if(isFinal && process) {
const promises = prepared
.filter(({writer}) => writer?.getParts && writer.replaceParts)
.map(async({writer}) => {
const bytes = writer.getParts();
const processedResult = await process(bytes, cacheFileName);
writer.replaceParts(processedResult);
});
await Promise.all(promises);
checkCancel();
}
writeDeferred.resolve();
if(isFinal) {
const realSize = size || byteLength;
if(!size || byteLength < size) {
prepared.forEach(({writer}) => writer?.trim?.(realSize));
}
const saveToStorage = realSize <= MAX_FILE_SAVE_SIZE;
prepared.forEach((item) => {
const {deferred, writer} = item;
if(deferred.isFulfilled || deferred.isRejected || !writer) {
return;
}
const result = writer.finalize(saveToStorage);
deferred.resolve(result);
});
}
} catch(err) {
errorHandler(null, err as ApiError);
}
};
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
superpuper();
}
}).catch(noop);
const checkCancel = () => {
if(getErrorsCount() === prepared.length) {
throw prepared[0].error;
}
};
return deferred;
}
public downloadMedia(options: DownloadMediaOptions): DownloadPromise {
let {media, thumb} = options;
const isPhoto = media._ === 'photo';
if(isPhoto && !thumb) {
return Promise.reject('preloadPhoto photoEmpty!');
}
// get original instance with correct file_reference instead of using copies
const isDocument = media._ === 'document';
// const isWebDocument = media._ === 'webDocument';
if(isDocument) media = this.appDocsManager.getDoc((media as Document.document).id);
else if(isPhoto) media = this.appPhotosManager.getPhoto((media as Photo.photo).id);
const {fileName, downloadOptions} = getDownloadMediaDetails(options);
let promise = this.getDownload(fileName);
if(!promise) {
promise = this.download(downloadOptions);
if(isDocument && !thumb) {
this.rootScope.dispatchEvent('document_downloading', (media as Document.document).id);
promise.catch(noop).finally(() => {
this.rootScope.dispatchEvent('document_downloaded', (media as Document.document).id);
});
}
}
return promise;
}
public downloadMediaURL(options: DownloadMediaOptions): Promise<string> {
const {media, thumb} = options;
let cacheContext = this.thumbsStorage.getCacheContext(media as any, thumb?.type);
if((thumb ? (cacheContext.downloaded >= ('size' in thumb ? thumb.size : 0)) : true) && cacheContext.url) {
return Promise.resolve(cacheContext.url);
}
return this.downloadMedia(options).then((blob) => {
if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) {
const url = URL.createObjectURL(blob);
cacheContext = this.thumbsStorage.setCacheContextURL(media as any, cacheContext.type, url, blob.size);
}
return cacheContext.url;
});
}
public downloadMediaVoid(options: DownloadMediaOptions) {
return this.downloadMedia(options).then(noop);
}
private delete(fileName: string) {
delete this.downloadPromises[fileName];
return this.getFileStorage().delete(fileName);
}
public upload({file, fileName}: {file: Blob | File, fileName?: string}) {
fileName ||= getFileNameForUpload(file);
const fileSize = file.size;
const isBigFile = fileSize >= 10485760;
const partSize = this.getLimitPart(fileSize, true);
const activeDelta = this.getDelta(partSize);
const totalParts = Math.ceil(fileSize / partSize);
const fileId = randomLong();
const resultInputFile: InputFile = {
_: isBigFile ? 'inputFileBig' : 'inputFile',
id: fileId as any,
parts: totalParts,
name: fileName,
md5_checksum: ''
};
const deferred = deferredPromise<typeof resultInputFile>();
if(totalParts > this.maxUploadParts) {
deferred.reject(makeError('FILE_TOO_BIG'));
return deferred;
}
let canceled = false, resolved = false;
let errorHandler = (error: ApiError) => {
if(error?.type !== 'UPLOAD_CANCELED') {
this.log.error('Up Error', error);
}
deferred.reject(error);
canceled = true;
errorHandler = noop;
};
const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart';
const id = this.tempId++;
const self = this;
function* generator() {
let _part = 0, doneParts = 0;
for(let offset = 0; offset < fileSize; offset += partSize) {
const part = _part++; // 0, 1
yield self.downloadRequest('upload', id, async() => {
checkCancel();
const blob = file.slice(offset, offset + partSize);
const buffer = await readBlobAsArrayBuffer(blob);
checkCancel();
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length);
return self.apiManager.invokeApi(method, {
file_id: fileId,
file_part: part,
file_total_parts: totalParts,
bytes: buffer
} as any, {
fileUpload: true
}).then(() => {
if(canceled) {
return;
}
++doneParts;
const progress: Progress = {done: doneParts * partSize, offset, total: fileSize, fileName};
deferred.notify(progress);
if(doneParts >= totalParts) {
deferred.resolve(resultInputFile);
resolved = true;
}
}, errorHandler);
}, activeDelta).catch(errorHandler);
}
}
const it = generator();
const process = () => {
if(canceled) return;
const r = it.next();
if(r.done || canceled) return;
(r.value as Promise<void>).then(process);
};
const maxRequests = Infinity;
for(let i = 0, length = Math.min(maxRequests, totalParts); i < length; ++i) {
process();
}
const checkCancel = () => {
if(canceled) {
throw makeError('UPLOAD_CANCELED');
}
};
deferred.cancel = () => {
if(!canceled && !resolved) {
canceled = true;
errorHandler(makeError('UPLOAD_CANCELED'));
}
};
deferred.notify = (progress: Progress) => {
this.rootScope.dispatchEvent('download_progress', progress);
};
deferred.finally(() => {
if(this.uploadPromises[fileName] === deferred) {
delete this.uploadPromises[fileName];
}
});
return this.uploadPromises[fileName] = deferred;
}
}