Better error handling
Fix rare FILE_REFERENCE_EXPIRED case Native file downloading
This commit is contained in:
parent
b148a60c8c
commit
a02387efa1
@ -503,7 +503,7 @@ export default class ChatBubbles {
|
||||
div.replaceWith(newDiv);
|
||||
|
||||
if(timeSpan) {
|
||||
newDiv.querySelector('.document-size').append(timeSpan);
|
||||
newDiv.querySelector('.document').append(timeSpan);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -4127,7 +4127,7 @@ export default class ChatBubbles {
|
||||
nameContainer = newNameContainer;
|
||||
}
|
||||
|
||||
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document-size, .audio');
|
||||
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document, .audio');
|
||||
// lastContainer && lastContainer.append(timeSpan.cloneNode(true));
|
||||
lastContainer && lastContainer.append(timeSpan);
|
||||
|
||||
|
@ -108,7 +108,7 @@ export default class DialogsContextMenu {
|
||||
};
|
||||
|
||||
private onPinClick = () => {
|
||||
this.managers.appMessagesManager.toggleDialogPin(this.selectedId, this.filterId).catch(async(err) => {
|
||||
this.managers.appMessagesManager.toggleDialogPin(this.selectedId, this.filterId).catch(async(err: ApiError) => {
|
||||
if(err.type === 'PINNED_DIALOGS_TOO_MUCH') {
|
||||
if(this.filterId >= 1) {
|
||||
toastNew({langPackKey: 'PinFolderLimitReached'});
|
||||
|
@ -80,7 +80,8 @@ export default class LazyLoadQueueBase {
|
||||
//await item.load(item.div);
|
||||
await this.loadItem(item);
|
||||
} catch(err) {
|
||||
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(err as string)) {
|
||||
const ignoreErrors: Set<ErrorType> = new Set(['NO_ENTRY_FOUND', 'STORAGE_OFFLINE']);
|
||||
if(!ignoreErrors.has((err as ApiError)?.type)) {
|
||||
this.log.error('loadMediaQueue error:', err/* , item */);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
|
||||
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import findUpAsChild from "../../helpers/dom/findUpAsChild";
|
||||
import findUpClassName from "../../helpers/dom/findUpClassName";
|
||||
import loadScript from "../../helpers/dom/loadScript";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||
import replaceContent from "../../helpers/dom/replaceContent";
|
||||
@ -25,7 +24,6 @@ import ScrollSaver from "../../helpers/scrollSaver";
|
||||
import tsNow from "../../helpers/tsNow";
|
||||
import { AccountTmpPassword, InputInvoice, InputPaymentCredentials, LabeledPrice, Message, MessageMedia, PaymentRequestedInfo, PaymentSavedCredentials, PaymentsPaymentForm, PaymentsPaymentReceipt, PaymentsValidatedRequestedInfo, PostAddress, ShippingOption } from "../../layer";
|
||||
import I18n, { i18n, LangPackKey, _i18n } from "../../lib/langPack";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
|
||||
import wrapRichText from "../../lib/richTextProcessor/wrapRichText";
|
||||
import rootScope from "../../lib/rootScope";
|
||||
|
@ -7,7 +7,6 @@
|
||||
import PopupElement from ".";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import { AccountPassword, AccountTmpPassword } from "../../layer";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import { InputState } from "../inputField";
|
||||
import PasswordInputField from "../passwordInputField";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
|
@ -5,14 +5,9 @@
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import { attachClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import toggleDisability from "../../helpers/dom/toggleDisability";
|
||||
import { InputInvoice, Message, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
|
||||
import getServerMessageId from "../../lib/appManagers/utils/messageId/getServerMessageId";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import { InputInvoice, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
|
||||
import matchEmail from "../../lib/richTextProcessor/matchEmail";
|
||||
import Button from "../button";
|
||||
import CheckboxField from "../checkboxField";
|
||||
import CountryInputField from "../countryInputField";
|
||||
import InputField from "../inputField";
|
||||
|
@ -21,6 +21,7 @@ import { AppManagers } from "../../lib/appManagers/managers";
|
||||
import getDownloadMediaDetails from "../../lib/appManagers/utils/download/getDownloadMediaDetails";
|
||||
import choosePhotoSize from "../../lib/appManagers/utils/photos/choosePhotoSize";
|
||||
import { joinElementsWith } from "../../lib/langPack";
|
||||
import { MAX_FILE_SAVE_SIZE } from "../../lib/mtproto/mtproto_config";
|
||||
import wrapPlainText from "../../lib/richTextProcessor/wrapPlainText";
|
||||
import rootScope from "../../lib/rootScope";
|
||||
import type { ThumbCache } from "../../lib/storages/thumbs";
|
||||
@ -94,6 +95,7 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
|
||||
const icoDiv = document.createElement('div');
|
||||
icoDiv.classList.add('document-ico');
|
||||
let icoTextEl: HTMLElement;
|
||||
|
||||
const hadContext = !!cacheContext;
|
||||
const getCacheContext = () => {
|
||||
@ -101,8 +103,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
};
|
||||
|
||||
cacheContext = await getCacheContext();
|
||||
let hasThumb = false;
|
||||
if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo'))/* && doc.mime_type !== 'image/gif' */) {
|
||||
docDiv.classList.add('document-with-thumb');
|
||||
hasThumb = true;
|
||||
|
||||
let imgs: (HTMLImageElement | HTMLCanvasElement)[] = [];
|
||||
// ! WARNING, use thumbs for check when thumb will be generated for media
|
||||
@ -131,14 +135,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
|
||||
imgs.forEach((img) => img.classList.add('document-thumb'));
|
||||
} else {
|
||||
icoDiv.innerText = ext;
|
||||
icoTextEl = document.createElement('span');
|
||||
icoTextEl.classList.add('document-ico-text');
|
||||
icoTextEl.innerText = ext;
|
||||
icoDiv.append(icoTextEl);
|
||||
}
|
||||
|
||||
//let fileName = stringMiddleOverflow(doc.file_name || 'Unknown.file', 26);
|
||||
let fileName = doc.file_name ? wrapPlainText(doc.file_name) : 'Unknown.file';
|
||||
const descriptionEl = document.createElement('div');
|
||||
descriptionEl.classList.add('document-description');
|
||||
const bytesContainer = document.createElement('span');
|
||||
const bytesEl = formatBytes(doc.size);
|
||||
const bytesJoiner = ' / ';
|
||||
|
||||
const descriptionParts: (HTMLElement | string | DocumentFragment)[] = [bytesEl];
|
||||
|
||||
if(withTime) {
|
||||
@ -149,8 +159,16 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
descriptionParts.push(await wrapSenderToPeer(message));
|
||||
}
|
||||
|
||||
if(!withTime && !showSender) {
|
||||
const b = document.createElement('span');
|
||||
const bytesMaxEl = formatBytes(doc.size);
|
||||
b.append(bytesJoiner, bytesMaxEl);
|
||||
b.style.visibility = 'hidden';
|
||||
descriptionParts.push(b);
|
||||
}
|
||||
|
||||
docDiv.innerHTML = `
|
||||
${(cacheContext.downloaded && !uploadFileName) || !message.mid ? '' : `<div class="document-download"></div>`}
|
||||
${(cacheContext.downloaded && !uploadFileName) || !message.mid || !hasThumb ? '' : `<div class="document-download"></div>`}
|
||||
<div class="document-name"></div>
|
||||
<div class="document-size"></div>
|
||||
`;
|
||||
@ -169,7 +187,8 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
}
|
||||
|
||||
const sizeDiv = docDiv.querySelector('.document-size') as HTMLElement;
|
||||
sizeDiv.append(...joinElementsWith(descriptionParts, ' · '));
|
||||
bytesContainer.append(...joinElementsWith(descriptionParts, ' · '));
|
||||
sizeDiv.append(bytesContainer);
|
||||
|
||||
docDiv.prepend(icoDiv);
|
||||
|
||||
@ -179,12 +198,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
|
||||
let downloadDiv: HTMLElement, preloader: ProgressivePreloader = null;
|
||||
const onLoad = () => {
|
||||
if(doc.size <= MAX_FILE_SAVE_SIZE) {
|
||||
docDiv.classList.add('downloaded');
|
||||
}
|
||||
|
||||
docDiv.classList.remove('downloading');
|
||||
|
||||
if(downloadDiv) {
|
||||
downloadDiv.classList.add('downloaded');
|
||||
const _downloadDiv = downloadDiv;
|
||||
setTimeout(() => {
|
||||
_downloadDiv.remove();
|
||||
}, 200);
|
||||
if(downloadDiv !== icoDiv) {
|
||||
const _downloadDiv = downloadDiv;
|
||||
setTimeout(() => {
|
||||
_downloadDiv.remove();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
downloadDiv = null;
|
||||
}
|
||||
|
||||
@ -194,17 +221,26 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
};
|
||||
|
||||
const addByteProgress = (promise: CancellablePromise<any>) => {
|
||||
docDiv.classList.add('downloading');
|
||||
|
||||
const sizeContainer = document.createElement('span');
|
||||
promise.then(() => {
|
||||
onLoad();
|
||||
sizeContainer.replaceWith(bytesEl);
|
||||
}, () => {
|
||||
replaceContent(sizeContainer, bytesEl);
|
||||
const _bytesContainer = formatBytes(doc.size);
|
||||
sizeContainer.style.position = 'absolute';
|
||||
sizeContainer.style.left = '0';
|
||||
promise.then(onLoad, noop).finally(() => {
|
||||
// sizeContainer.replaceWith(bytesContainer);
|
||||
bytesContainer.style.visibility = '';
|
||||
sizeContainer.remove();
|
||||
// b && b.classList.remove('hide');
|
||||
});
|
||||
|
||||
// b && b.classList.add('hide');
|
||||
|
||||
let d = formatBytes(0);
|
||||
bytesEl.replaceWith(sizeContainer);
|
||||
sizeContainer.append(d, ' / ', bytesEl);
|
||||
bytesContainer.style.visibility = 'hidden';
|
||||
// bytesContainer.replaceWith(sizeContainer);
|
||||
sizeContainer.append(d, bytesJoiner, _bytesContainer);
|
||||
bytesContainer.parentElement.append(sizeContainer);
|
||||
promise.addNotifyListener((progress: Progress) => {
|
||||
const _d = formatBytes(progress.done);
|
||||
d.replaceWith(_d);
|
||||
@ -236,6 +272,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
download = appDownloadManager.downloadToDisc({media: doc, queueId});
|
||||
}
|
||||
|
||||
download.catch(() => {
|
||||
docDiv.classList.remove('downloading');
|
||||
});
|
||||
|
||||
if(downloadDiv) {
|
||||
preloader.attach(downloadDiv, true, download);
|
||||
addByteProgress(download);
|
||||
@ -244,14 +284,15 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
|
||||
|
||||
const {fileName: downloadFileName} = getDownloadMediaDetails({media: doc});
|
||||
if(await managers.apiFileManager.isDownloading(downloadFileName)) {
|
||||
downloadDiv = docDiv.querySelector('.document-download');
|
||||
downloadDiv = docDiv.querySelector('.document-download') || icoDiv;
|
||||
const promise = appDownloadManager.downloadMediaVoid({media: doc});
|
||||
|
||||
preloader = new ProgressivePreloader();
|
||||
preloader.attach(downloadDiv, false, promise);
|
||||
preloader.setDownloadFunction(load);
|
||||
addByteProgress(promise);
|
||||
} else if(!cacheContext.downloaded || uploadFileName) {
|
||||
downloadDiv = docDiv.querySelector('.document-download');
|
||||
downloadDiv = docDiv.querySelector('.document-download') || icoDiv;
|
||||
preloader = new ProgressivePreloader({
|
||||
isUpload: !!uploadFileName
|
||||
});
|
||||
|
@ -4,7 +4,7 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { IDBStore } from "../../lib/idb";
|
||||
import { IDBStore } from "../../lib/files/idb";
|
||||
|
||||
export type DatabaseStore<StoreName extends string> = Omit<IDBStore, 'name'> & {name: StoreName};
|
||||
export type Database<StoreName extends string> = {
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Database } from '.';
|
||||
import type { IDBIndex } from '../../lib/idb';
|
||||
import type { IDBIndex } from '../../lib/files/idb';
|
||||
|
||||
const DATABASE_STATE: Database<'session' | 'stickerSets' | 'users' | 'chats' | 'messages' | 'dialogs'> = {
|
||||
name: 'tweb',
|
||||
|
29
src/global.d.ts
vendored
29
src/global.d.ts
vendored
@ -23,13 +23,36 @@ declare global {
|
||||
type Long = string | number;
|
||||
type MTLong = string;
|
||||
|
||||
type LocalErrorType = 'DOWNLOAD_CANCELED';
|
||||
type ServerErrorType = 'FILE_REFERENCE_EXPIRED';
|
||||
type ApiFileManagerError = 'DOWNLOAD_CANCELED' | 'UPLOAD_CANCELED' | 'FILE_TOO_BIG' | 'REFERENCE_IS_NOT_REFRESHED';
|
||||
type StorageError = 'STORAGE_OFFLINE' | 'NO_ENTRY_FOUND' | 'IDB_CREATE_TIMEOUT';
|
||||
type ReferenceError = 'NO_NEW_CONTEXT';
|
||||
type NetworkerError = 'NETWORK_BAD_RESPONSE';
|
||||
type FiltersError = 'PINNED_DIALOGS_TOO_MUCH';
|
||||
|
||||
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError;
|
||||
type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN';
|
||||
|
||||
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
|
||||
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |
|
||||
'BOT_PRECHECKOUT_TIMEOUT' | 'TMP_PASSWORD_INVALID' | 'PASSWORD_HASH_INVALID' | 'CHANNEL_PRIVATE';
|
||||
|
||||
type ErrorType = LocalErrorType | ServerErrorType;
|
||||
|
||||
interface Error {
|
||||
type?: LocalErrorType | ServerErrorType;
|
||||
type?: ErrorType;
|
||||
}
|
||||
|
||||
type ApiError = Partial<{
|
||||
code: number,
|
||||
type: ErrorType,
|
||||
description: string,
|
||||
originalError: any,
|
||||
stack: string,
|
||||
handled: boolean,
|
||||
input: string,
|
||||
message: ApiError
|
||||
}>;
|
||||
|
||||
declare const electronHelpers: {
|
||||
openExternal(url): void;
|
||||
} | undefined;
|
||||
|
@ -16,17 +16,7 @@ export default function blobConstruct<T extends Uint8Array | string>(blobParts:
|
||||
blobParts = [blobParts];
|
||||
}
|
||||
|
||||
let blob;
|
||||
const safeMimeType = blobSafeMimeType(mimeType);
|
||||
try {
|
||||
blob = new Blob(blobParts, {type: safeMimeType});
|
||||
} catch(e) {
|
||||
// @ts-ignore
|
||||
let bb = new BlobBuilder;
|
||||
blobParts.forEach((blobPart: any) => {
|
||||
bb.append(blobPart);
|
||||
});
|
||||
blob = bb.getBlob(safeMimeType);
|
||||
}
|
||||
const blob = new Blob(blobParts, {type: safeMimeType});
|
||||
return blob;
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ import type { DownloadOptions } from "../lib/mtproto/apiFileManager";
|
||||
const FILENAME_JOINER = '_';
|
||||
|
||||
export function getFileNameByLocation(location: InputFileLocation | InputWebFileLocation, options?: Partial<{
|
||||
fileName: string
|
||||
fileName: string,
|
||||
downloadId: string
|
||||
}>) {
|
||||
const fileName = '';//(options?.fileName || '').split('.');
|
||||
const ext = fileName[fileName.length - 1] || '';
|
||||
@ -57,7 +58,7 @@ export function getFileNameByLocation(location: InputFileLocation | InputWebFile
|
||||
}
|
||||
}
|
||||
|
||||
return str + (ext ? '.' + ext : ext);
|
||||
return str + (options.downloadId || '') + (ext ? '.' + ext : ext);
|
||||
}
|
||||
|
||||
export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download';
|
||||
|
7
src/helpers/makeError.ts
Normal file
7
src/helpers/makeError.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default function makeError(type: Error['type']) {
|
||||
const error: ApiError = {
|
||||
type
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
4
src/helpers/string/fileNameRFC.ts
Normal file
4
src/helpers/string/fileNameRFC.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default function fileNameRFC(fileName: string) {
|
||||
// Make filename RFC5987 compatible
|
||||
return encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A');
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import CacheStorageController from "../lib/cacheStorage";
|
||||
import CacheStorageController from "../lib/files/cacheStorage";
|
||||
import AppStorage from "../lib/storage";
|
||||
import sessionStorage from "../lib/sessionStorage";
|
||||
import noop from "./noop";
|
||||
|
@ -4,21 +4,20 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import type { ApiFileManager, DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
|
||||
import type { DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
|
||||
import type { AppMessagesManager } from "./appMessagesManager";
|
||||
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
|
||||
import { Document, InputFile, Photo, PhotoSize, WebDocument } from "../../layer";
|
||||
import { getFileNameByLocation } from "../../helpers/fileName";
|
||||
import { InputFile, Photo, PhotoSize } from "../../layer";
|
||||
import getFileNameForUpload from "../../helpers/getFileNameForUpload";
|
||||
import { AppManagers } from "./managers";
|
||||
import rootScope from "../rootScope";
|
||||
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||
import getDocumentDownloadOptions from "./utils/docs/getDocumentDownloadOptions";
|
||||
import getPhotoDownloadOptions from "./utils/photos/getPhotoDownloadOptions";
|
||||
import createDownloadAnchor from "../../helpers/dom/createDownloadAnchor";
|
||||
import noop from "../../helpers/noop";
|
||||
import getDownloadMediaDetails from "./utils/download/getDownloadMediaDetails";
|
||||
import getDownloadFileNameFromOptions from "./utils/download/getDownloadFileNameFromOptions";
|
||||
import { AppMessagesManager } from "./appMessagesManager";
|
||||
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
|
||||
import { MAX_FILE_SAVE_SIZE } from "../mtproto/mtproto_config";
|
||||
import createDownloadAnchor from "../../helpers/dom/createDownloadAnchor";
|
||||
|
||||
export type ResponseMethodBlob = 'blob';
|
||||
export type ResponseMethodJson = 'json';
|
||||
@ -35,10 +34,10 @@ export type Download = DownloadBlob | DownloadUrl/* | DownloadJson */;
|
||||
export type Progress = {done: number, fileName: string, total: number, offset: number};
|
||||
export type ProgressCallback = (details: Progress) => void;
|
||||
|
||||
type DownloadType = 'url' | 'blob' | 'void';
|
||||
type DownloadType = 'url' | 'blob' | 'void' | 'disc';
|
||||
|
||||
export class AppDownloadManager {
|
||||
private downloads: {[fileName: string]: {main: Download, url?: Download, blob?: Download, void?: Download}} = {};
|
||||
private downloads: {[fileName: string]: {main: Download} & {[type in DownloadType]?: Download}} = {};
|
||||
private progress: {[fileName: string]: Progress} = {};
|
||||
// private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
|
||||
private managers: AppManagers;
|
||||
@ -84,7 +83,7 @@ export class AppDownloadManager {
|
||||
};
|
||||
|
||||
deferred.catch(() => {
|
||||
this.clearDownload(fileName);
|
||||
this.clearDownload(fileName, type);
|
||||
}).finally(() => {
|
||||
delete this.progress[fileName];
|
||||
// delete this.progressCallbacks[fileName];
|
||||
@ -101,6 +100,13 @@ export class AppDownloadManager {
|
||||
});
|
||||
}
|
||||
|
||||
const haveToClear = type === 'disc';
|
||||
if(haveToClear) {
|
||||
deferred.catch(noop).finally(() => {
|
||||
this.clearDownload(fileName, type);
|
||||
});
|
||||
}
|
||||
|
||||
return download[type] = deferred as any;
|
||||
}
|
||||
|
||||
@ -115,8 +121,18 @@ export class AppDownloadManager {
|
||||
return deferred as CancellablePromise<Awaited<T>>;
|
||||
}
|
||||
|
||||
private clearDownload(fileName: string) {
|
||||
delete this.downloads[fileName];
|
||||
private clearDownload(fileName: string, type?: DownloadType) {
|
||||
const downloads = this.downloads[fileName];
|
||||
if(!downloads) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete downloads[type];
|
||||
|
||||
const length = Object.keys(downloads).length;
|
||||
if(!length || (downloads.main && length === 1)) {
|
||||
delete this.downloads[fileName];
|
||||
}
|
||||
}
|
||||
|
||||
public getUpload(fileName: string): ReturnType<AppMessagesManager['sendFile']>['promise'] {
|
||||
@ -161,7 +177,15 @@ export class AppDownloadManager {
|
||||
const {downloadOptions, fileName} = getDownloadMediaDetails(options);
|
||||
|
||||
return this.d(fileName, () => {
|
||||
const cb = type === 'url' ? this.managers.apiFileManager.downloadMediaURL : (type === 'void' ? this.managers.apiFileManager.downloadMediaVoid : this.managers.apiFileManager.downloadMedia);
|
||||
let cb: any;
|
||||
if(type === 'url') {
|
||||
cb = this.managers.apiFileManager.downloadMediaURL;
|
||||
} else if(type === 'void' || type === 'disc') {
|
||||
cb = this.managers.apiFileManager.downloadMediaVoid;
|
||||
} else if(type === 'blob') {
|
||||
cb = this.managers.apiFileManager.downloadMedia;
|
||||
}
|
||||
|
||||
return cb(options);
|
||||
}, type) as any;
|
||||
}
|
||||
@ -207,20 +231,58 @@ export class AppDownloadManager {
|
||||
if(!isDocument && !options.thumb) {
|
||||
options.thumb = (media as Photo.photo).sizes.slice().pop() as PhotoSize.photoSize;
|
||||
}
|
||||
|
||||
const promise = this.downloadMedia(options);
|
||||
promise.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const downloadOptions = isDocument ?
|
||||
getDocumentDownloadOptions(media) :
|
||||
getPhotoDownloadOptions(media as any, options.thumb);
|
||||
const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location);
|
||||
createDownloadAnchor(url, fileName, () => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}, noop);
|
||||
|
||||
return promise;
|
||||
const {downloadOptions, fileName} = getDownloadMediaDetails(options);
|
||||
if(downloadOptions.size && downloadOptions.size > MAX_FILE_SAVE_SIZE) {
|
||||
const id = '' + (Math.random() * 0x7FFFFFFF | 0);
|
||||
const url = `/download/${id}`;
|
||||
options.downloadId = id;
|
||||
|
||||
const promise = this.downloadMedia(options, 'disc');
|
||||
|
||||
let iframe: HTMLIFrameElement;
|
||||
const onProgress = () => {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.hidden = true;
|
||||
// iframe.src = sw.scope + fileName;
|
||||
iframe.src = url;
|
||||
document.body.append(iframe);
|
||||
|
||||
indexOfAndSplice(promise.listeners, onProgress);
|
||||
};
|
||||
|
||||
promise.addNotifyListener(onProgress);
|
||||
promise.catch(noop).finally(() => {
|
||||
setTimeout(() => {
|
||||
iframe?.remove();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return promise;
|
||||
} else {
|
||||
const promise = this.downloadMedia(options, 'blob');
|
||||
promise.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
createDownloadAnchor(url, downloadOptions.fileName || fileName, () => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
// const promise = this.downloadMedia(options);
|
||||
// promise.then((blob) => {
|
||||
// const url = URL.createObjectURL(blob);
|
||||
// const downloadOptions = isDocument ?
|
||||
// getDocumentDownloadOptions(media) :
|
||||
// getPhotoDownloadOptions(media as any, options.thumb);
|
||||
// const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location);
|
||||
// createDownloadAnchor(url, fileName, () => {
|
||||
// URL.revokeObjectURL(url);
|
||||
// });
|
||||
// }, noop);
|
||||
|
||||
// return promise;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ import generateMessageId from './utils/messageId/generateMessageId';
|
||||
import getUserStatusString from '../../components/wrappers/getUserStatusString';
|
||||
import getChatMembersString from '../../components/wrappers/getChatMembersString';
|
||||
import { STATE_INIT } from '../../config/state';
|
||||
import CacheStorageController from '../cacheStorage';
|
||||
import CacheStorageController from '../files/cacheStorage';
|
||||
import themeController from '../../helpers/themeController';
|
||||
import overlayCounter from '../../helpers/overlayCounter';
|
||||
import appDialogsManager from './appDialogsManager';
|
||||
|
@ -59,6 +59,7 @@ import appTabsManager from "./appTabsManager";
|
||||
import MTProtoMessagePort from "../mtproto/mtprotoMessagePort";
|
||||
import getAlbumText from "./utils/messages/getAlbumText";
|
||||
import pause from "../../helpers/schedulers/pause";
|
||||
import makeError from "../../helpers/makeError";
|
||||
|
||||
//console.trace('include');
|
||||
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
|
||||
@ -3077,7 +3078,7 @@ export class AppMessagesManager extends AppManager {
|
||||
(this.rootScope.premium ? appConfig.dialogs_folder_pinned_limit_premium : appConfig.dialogs_folder_pinned_limit_default) :
|
||||
(this.rootScope.premium ? appConfig.dialogs_pinned_limit_premium : appConfig.dialogs_pinned_limit_default);
|
||||
if(this.dialogsStorage.getPinnedOrders(filterId).length >= max) {
|
||||
return Promise.reject({type: 'PINNED_DIALOGS_TOO_MUCH'});
|
||||
return Promise.reject(makeError('PINNED_DIALOGS_TOO_MUCH'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ export default function getDownloadMediaDetails(options: DownloadMediaOptions) {
|
||||
else if(media._ === 'photo') downloadOptions = getPhotoDownloadOptions(media, thumb, queueId, onlyCache);
|
||||
else if(isWebDocument(media)) downloadOptions = getWebDocumentDownloadOptions(media);
|
||||
|
||||
downloadOptions.downloadId = options.downloadId;
|
||||
|
||||
const fileName = getDownloadFileNameFromOptions(downloadOptions);
|
||||
return {fileName, downloadOptions};
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* 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 blobConstruct from "../helpers/blob/blobConstruct";
|
||||
|
||||
export class FileManager {
|
||||
private blobSupported = true;
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
blobConstruct([], '');
|
||||
} catch(e) {
|
||||
this.blobSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isAvailable() {
|
||||
return this.blobSupported;
|
||||
}
|
||||
|
||||
public getFakeFileWriter(mimeType: string, size: number, saveFileCallback?: (blob: Blob) => Promise<Blob>) {
|
||||
let bytes: Uint8Array = new Uint8Array(size);
|
||||
const fakeFileWriter = {
|
||||
write: async(part: Uint8Array, offset: number) => {
|
||||
if(!this.blobSupported) {
|
||||
throw false;
|
||||
}
|
||||
|
||||
// sometimes file size can be bigger than the prov
|
||||
const endOffset = offset + part.byteLength;
|
||||
if(endOffset > bytes.byteLength) {
|
||||
const newBytes = new Uint8Array(endOffset);
|
||||
newBytes.set(bytes, 0);
|
||||
bytes = newBytes;
|
||||
}
|
||||
|
||||
bytes.set(part, offset);
|
||||
},
|
||||
truncate: () => {
|
||||
bytes = new Uint8Array();
|
||||
},
|
||||
trim: (size: number) => {
|
||||
bytes = bytes.slice(0, size);
|
||||
},
|
||||
finalize: (saveToStorage = true) => {
|
||||
const blob = blobConstruct(bytes, mimeType);
|
||||
|
||||
if(saveToStorage && saveFileCallback) {
|
||||
saveFileCallback(blob);
|
||||
}
|
||||
|
||||
return blob;
|
||||
},
|
||||
getParts: () => bytes,
|
||||
replaceParts: (parts: typeof bytes) => {
|
||||
bytes = parts;
|
||||
}
|
||||
};
|
||||
|
||||
return fakeFileWriter;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileManager();
|
@ -4,15 +4,16 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import Modes from '../config/modes';
|
||||
import blobConstruct from '../helpers/blob/blobConstruct';
|
||||
import FileManager from './fileManager';
|
||||
//import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
|
||||
//import { logger } from './polyfill';
|
||||
import Modes from '../../config/modes';
|
||||
import blobConstruct from '../../helpers/blob/blobConstruct';
|
||||
import MemoryWriter from './memoryWriter';
|
||||
import FileManager from './memoryWriter';
|
||||
import FileStorage from './fileStorage';
|
||||
import makeError from '../../helpers/makeError';
|
||||
|
||||
export type CacheStorageDbName = 'cachedFiles' | 'cachedStreamChunks' | 'cachedAssets';
|
||||
|
||||
export default class CacheStorageController {
|
||||
export default class CacheStorageController implements FileStorage {
|
||||
private static STORAGES: CacheStorageController[] = [];
|
||||
private openDbPromise: Promise<Cache>;
|
||||
|
||||
@ -64,7 +65,7 @@ export default class CacheStorageController {
|
||||
return this.get(fileName).then((response) => {
|
||||
if(!response) {
|
||||
//console.warn('getFile:', response, fileName);
|
||||
throw 'NO_ENTRY_FOUND';
|
||||
throw makeError('NO_ENTRY_FOUND');
|
||||
}
|
||||
|
||||
const promise = response[method]();
|
||||
@ -92,7 +93,7 @@ export default class CacheStorageController {
|
||||
|
||||
public timeoutOperation<T>(callback: (cache: Cache) => Promise<T>) {
|
||||
if(!this.useStorage) {
|
||||
return Promise.reject('STORAGE_OFFLINE');
|
||||
return Promise.reject(makeError('STORAGE_OFFLINE'));
|
||||
}
|
||||
|
||||
return new Promise<T>(async(resolve, reject) => {
|
||||
@ -123,12 +124,12 @@ export default class CacheStorageController {
|
||||
});
|
||||
}
|
||||
|
||||
public getFileWriter(fileName: string, fileSize: number, mimeType: string) {
|
||||
const fakeWriter = FileManager.getFakeFileWriter(mimeType, fileSize, (blob) => {
|
||||
public getWriter(fileName: string, fileSize: number, mimeType: string) {
|
||||
const writer = new MemoryWriter(mimeType, fileSize, (blob) => {
|
||||
return this.saveFile(fileName, blob).catch(() => blob);
|
||||
});
|
||||
|
||||
return Promise.resolve(fakeWriter);
|
||||
return Promise.resolve(writer);
|
||||
}
|
||||
|
||||
public static toggleStorage(enabled: boolean, clearWrite: boolean) {
|
13
src/lib/files/fileStorage.ts
Normal file
13
src/lib/files/fileStorage.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import StreamWriter from "./streamWriter";
|
||||
|
||||
export default abstract class FileStorage {
|
||||
public abstract getFile(fileName: string): Promise<any>;
|
||||
|
||||
public abstract getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter>;
|
||||
}
|
@ -9,10 +9,11 @@
|
||||
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { Database } from '../config/databases';
|
||||
import Modes from '../config/modes';
|
||||
import safeAssign from '../helpers/object/safeAssign';
|
||||
import { logger } from './logger';
|
||||
import { Database } from '../../config/databases';
|
||||
import Modes from '../../config/modes';
|
||||
import makeError from '../../helpers/makeError';
|
||||
import safeAssign from '../../helpers/object/safeAssign';
|
||||
import { logger } from '../logger';
|
||||
|
||||
/**
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex
|
||||
@ -111,7 +112,7 @@ export class IDB {
|
||||
let finished = false;
|
||||
setTimeout(() => {
|
||||
if(!finished) {
|
||||
request.onerror({type: 'IDB_CREATE_TIMEOUT'} as Event);
|
||||
request.onerror(makeError('IDB_CREATE_TIMEOUT') as Event);
|
||||
}
|
||||
}, 3000);
|
||||
|
58
src/lib/files/memoryWriter.ts
Normal file
58
src/lib/files/memoryWriter.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import blobConstruct from "../../helpers/blob/blobConstruct";
|
||||
import StreamWriter from './streamWriter';
|
||||
|
||||
export default class MemoryWriter implements StreamWriter {
|
||||
private bytes: Uint8Array;
|
||||
|
||||
constructor(
|
||||
private mimeType: string,
|
||||
private size: number,
|
||||
private saveFileCallback?: (blob: Blob) => Promise<Blob>
|
||||
) {
|
||||
this.bytes = new Uint8Array(size);
|
||||
}
|
||||
|
||||
public async write(part: Uint8Array, offset: number) {
|
||||
// sometimes file size can be bigger than the prov
|
||||
const endOffset = offset + part.byteLength;
|
||||
if(endOffset > this.bytes.byteLength) {
|
||||
const newBytes = new Uint8Array(endOffset);
|
||||
newBytes.set(this.bytes, 0);
|
||||
this.bytes = newBytes;
|
||||
}
|
||||
|
||||
this.bytes.set(part, offset);
|
||||
};
|
||||
|
||||
public truncate() {
|
||||
this.bytes = new Uint8Array();
|
||||
}
|
||||
|
||||
public trim(size: number) {
|
||||
this.bytes = this.bytes.slice(0, size);
|
||||
}
|
||||
|
||||
public finalize(saveToStorage = true) {
|
||||
const blob = blobConstruct(this.bytes, this.mimeType);
|
||||
|
||||
if(saveToStorage && this.saveFileCallback) {
|
||||
this.saveFileCallback(blob);
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
public getParts() {
|
||||
return this.bytes;
|
||||
}
|
||||
|
||||
public replaceParts(parts: Uint8Array) {
|
||||
this.bytes = parts;
|
||||
}
|
||||
}
|
14
src/lib/files/streamWriter.ts
Normal file
14
src/lib/files/streamWriter.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
export default abstract class StreamWriter {
|
||||
public abstract write(part: Uint8Array, offset?: number): Promise<any>;
|
||||
public abstract truncate?(): void;
|
||||
public abstract trim?(size: number): void;
|
||||
public abstract finalize(saveToStorage?: boolean): Promise<Blob> | Blob;
|
||||
public abstract getParts?(): Uint8Array;
|
||||
public abstract replaceParts?(parts: Uint8Array): void;
|
||||
}
|
@ -12,12 +12,10 @@
|
||||
import type { ReferenceBytes } from "./referenceDatabase";
|
||||
import Modes from "../../config/modes";
|
||||
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
|
||||
import { getFileNameByLocation } from "../../helpers/fileName";
|
||||
import { randomLong } from "../../helpers/random";
|
||||
import { Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, WebDocument } from "../../layer";
|
||||
import { DcId } from "../../types";
|
||||
import CacheStorageController from "../cacheStorage";
|
||||
import fileManager from "../fileManager";
|
||||
import CacheStorageController from "../files/cacheStorage";
|
||||
import { logger, LogTypes } from "../logger";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import ctx from "../../environment/ctx";
|
||||
@ -33,12 +31,19 @@ import getFileNameForUpload from "../../helpers/getFileNameForUpload";
|
||||
import type { Progress } from "../appManagers/appDownloadManager";
|
||||
import getDownloadMediaDetails from "../appManagers/utils/download/getDownloadMediaDetails";
|
||||
import networkStats from "./networkStats";
|
||||
import pause from "../../helpers/schedulers/pause";
|
||||
import getDownloadFileNameFromOptions from "../appManagers/utils/download/getDownloadFileNameFromOptions";
|
||||
import { getServiceMessagePort } from "./mtproto.worker";
|
||||
import StreamWriter from "../files/streamWriter";
|
||||
import FileStorage from "../files/fileStorage";
|
||||
import fileNameRFC from "../../helpers/string/fileNameRFC";
|
||||
import { MAX_FILE_SAVE_SIZE } from "./mtproto_config";
|
||||
import throttle from "../../helpers/schedulers/throttle";
|
||||
import makeError from "../../helpers/makeError";
|
||||
|
||||
type Delayed = {
|
||||
offset: number,
|
||||
writeFilePromise: CancellablePromise<void>,
|
||||
writeFileDeferred: CancellablePromise<void>
|
||||
writePromise: CancellablePromise<void>,
|
||||
writeDeferred: CancellablePromise<void>
|
||||
};
|
||||
|
||||
export type DownloadOptions = {
|
||||
@ -50,6 +55,7 @@ export type DownloadOptions = {
|
||||
limitPart?: number,
|
||||
queueId?: number,
|
||||
onlyCache?: boolean,
|
||||
downloadId?: string
|
||||
// getFileMethod: Parameters<CacheStorageController['getFile']>[1]
|
||||
};
|
||||
|
||||
@ -57,7 +63,8 @@ export type DownloadMediaOptions = {
|
||||
media: Photo.photo | Document.document | WebDocument,
|
||||
thumb?: PhotoSize,
|
||||
queueId?: number,
|
||||
onlyCache?: boolean
|
||||
onlyCache?: boolean,
|
||||
downloadId?: string
|
||||
};
|
||||
|
||||
type DownloadPromise = CancellablePromise<Blob>;
|
||||
@ -75,11 +82,18 @@ export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile;
|
||||
// originalPayload: ReferenceBytes
|
||||
// };
|
||||
|
||||
const MAX_FILE_SAVE_SIZE = 20 * 1024 * 1024;
|
||||
const MAX_FILE_PART_SIZE = 1 * 1024 * 1024;
|
||||
|
||||
const REGULAR_DOWNLOAD_DELTA = 36;
|
||||
const PREMIUM_DOWNLOAD_DELTA = 72;
|
||||
|
||||
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');
|
||||
|
||||
@ -108,7 +122,7 @@ export class ApiFileManager extends AppManager {
|
||||
public refreshReferencePromises: {
|
||||
[referenceHex: string]: {
|
||||
deferred: CancellablePromise<ReferenceBytes>,
|
||||
timeout: number
|
||||
timeout?: number
|
||||
}
|
||||
} = {};
|
||||
|
||||
@ -117,6 +131,9 @@ export class ApiFileManager extends AppManager {
|
||||
private queueId = 0;
|
||||
private debug = Modes.debug;
|
||||
|
||||
private maxUploadParts = 4000;
|
||||
private maxDownloadParts = 8000;
|
||||
|
||||
protected after() {
|
||||
setInterval(() => { // clear old promises
|
||||
for(const hex in this.refreshReferencePromises) {
|
||||
@ -126,6 +143,11 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
}
|
||||
}, 1800e3);
|
||||
|
||||
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>;
|
||||
@ -154,12 +176,12 @@ export class ApiFileManager extends AppManager {
|
||||
const downloadLimit = dcId === 'upload' ? 24 : (this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA);
|
||||
//const downloadLimit = Infinity;
|
||||
|
||||
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull || !downloadPull.length) {
|
||||
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 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;
|
||||
@ -174,9 +196,8 @@ export class ApiFileManager extends AppManager {
|
||||
this.downloadCheck(dcId);
|
||||
|
||||
networkPromise.resolve();
|
||||
}, (error: Error) => {
|
||||
// @ts-ignore
|
||||
if(!error || !error.type || (error.type !== 'DOWNLOAD_CANCELED' && error.type !== 'UPLOAD_CANCELED')) {
|
||||
}, (error: ApiError) => {
|
||||
if(!error?.type || !IGNORE_ERRORS.has(error.type)) {
|
||||
this.log.error('downloadCheck error:', error);
|
||||
}
|
||||
|
||||
@ -234,6 +255,9 @@ export class ApiFileManager extends AppManager {
|
||||
const invoke = async(): Promise<MyUploadFile> => {
|
||||
checkCancel && 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(1000).then(() => */this.apiManager.invokeApi('upload.getFile', {
|
||||
location,
|
||||
offset,
|
||||
@ -243,9 +267,9 @@ export class ApiFileManager extends AppManager {
|
||||
fileDownload: true
|
||||
}) as Promise<MyUploadFile>/* ) */;
|
||||
|
||||
return promise.catch((err) => {
|
||||
return promise.catch((err: ApiError) => {
|
||||
if(err.type === 'FILE_REFERENCE_EXPIRED') {
|
||||
return this.refreshReference(location).then(invoke);
|
||||
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke);
|
||||
}
|
||||
|
||||
throw err;
|
||||
@ -258,7 +282,7 @@ export class ApiFileManager extends AppManager {
|
||||
location.checkedReference = true;
|
||||
const hex = bytesToHex(reference);
|
||||
if(this.refreshReferencePromises[hex]) {
|
||||
return this.refreshReference(location).then(invoke);
|
||||
return this.refreshReference(location, reference).then(invoke);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,14 +298,16 @@ export class ApiFileManager extends AppManager {
|
||||
return bytes / 1024 / 128;
|
||||
}
|
||||
|
||||
private getLimitPart(size: number): number {
|
||||
private getLimitPart(size: number, isUpload: boolean): number {
|
||||
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
|
||||
return 512 * 1024;
|
||||
}
|
||||
|
||||
let bytes = 128 * 1024;
|
||||
|
||||
while((size / bytes) > 2000) {
|
||||
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts;
|
||||
// usually it will stick to 512Kb size if the file is too big
|
||||
while((size / bytes) > maxParts && bytes < MAX_FILE_PART_SIZE) {
|
||||
bytes *= 2;
|
||||
}
|
||||
/* if(size < 1e6 || !size) bytes = 512;
|
||||
@ -323,38 +349,41 @@ export class ApiFileManager extends AppManager {
|
||||
return instance.invoke('convertOpus', {fileName, bytes});
|
||||
};
|
||||
|
||||
private refreshReference(inputFileLocation: InputFileLocation) {
|
||||
const reference = (inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference;
|
||||
const hex = bytesToHex(reference);
|
||||
|
||||
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,
|
||||
timeout: ctx.setTimeout(() => {
|
||||
this.log.error('Didn\'t refresh the reference:', inputFileLocation);
|
||||
deferred.reject('REFERENCE_IS_NOT_REFRESHED');
|
||||
}, 60000)
|
||||
|
||||
// ! 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);
|
||||
});
|
||||
// deferred.catch(noop).finally(() => {
|
||||
// clearTimeout(r.timeout);
|
||||
// });
|
||||
|
||||
this.referenceDatabase.refreshReference(reference).then(deferred.resolve, deferred.reject);
|
||||
// const task = {type: 'refreshReference', payload: reference};
|
||||
// notifySomeone(task);
|
||||
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) => {
|
||||
if(hex === bytesToHex(reference)) {
|
||||
throw 'REFERENCE_IS_NOT_REFRESHED';
|
||||
}
|
||||
|
||||
(inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference = reference;
|
||||
inputFileLocation.file_reference = reference;
|
||||
});
|
||||
}
|
||||
|
||||
@ -371,16 +400,14 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
|
||||
public download(options: DownloadOptions): DownloadPromise {
|
||||
if(!fileManager.isAvailable()) {
|
||||
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
|
||||
}
|
||||
|
||||
const size = options.size ?? 0;
|
||||
const {dcId, location} = options;
|
||||
const {dcId, location, downloadId} = options;
|
||||
|
||||
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
|
||||
|
||||
if(options.mimeType === 'application/x-tgwallpattern') {
|
||||
if(downloadId) {
|
||||
|
||||
} else if(options.mimeType === 'application/x-tgwallpattern') {
|
||||
process = this.uncompressTGV;
|
||||
options.mimeType = 'image/svg+xml';
|
||||
} else if(options.mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) {
|
||||
@ -394,9 +421,9 @@ export class ApiFileManager extends AppManager {
|
||||
options.mimeType = 'audio/wav';
|
||||
}
|
||||
|
||||
const fileName = getFileNameByLocation(location, {fileName: options.fileName});
|
||||
const cachedPromise = this.downloadPromises[fileName];
|
||||
const fileStorage = this.getFileStorage();
|
||||
const fileName = getDownloadFileNameFromOptions(options);
|
||||
const cachedPromise = options.downloadId ? undefined : this.downloadPromises[fileName];
|
||||
let fileStorage: FileStorage = this.getFileStorage();
|
||||
|
||||
this.debug && this.log('downloadFile', fileName, size, location, options.mimeType);
|
||||
|
||||
@ -429,28 +456,77 @@ export class ApiFileManager extends AppManager {
|
||||
const deferred: DownloadPromise = deferredPromise();
|
||||
const mimeType = options.mimeType || 'image/jpeg';
|
||||
|
||||
let error: Error;
|
||||
let error: ApiError;
|
||||
let resolved = false;
|
||||
let cacheFileWriter: ReturnType<typeof fileManager['getFakeFileWriter']>;
|
||||
let errorHandler = (_error: Error) => {
|
||||
let cacheWriter: StreamWriter;
|
||||
let errorHandler = (_error: typeof error) => {
|
||||
error = _error;
|
||||
delete this.downloadPromises[fileName];
|
||||
deferred.reject(error);
|
||||
errorHandler = () => {};
|
||||
|
||||
if(cacheFileWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) {
|
||||
cacheFileWriter.truncate();
|
||||
if(cacheWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) {
|
||||
cacheWriter.truncate?.();
|
||||
}
|
||||
};
|
||||
|
||||
const id = this.tempId++;
|
||||
|
||||
if(downloadId) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream; charset=utf-8',
|
||||
'Content-Disposition': "attachment; filename*=UTF-8''" + fileNameRFC(options.fileName),
|
||||
// 'Content-Disposition': `attachment; filename="${options.fileName}"`,
|
||||
// 'Content-Type': 'application/octet-stream; charset=utf-8',
|
||||
...(size ? {'Content-Length': size} : {})
|
||||
};
|
||||
|
||||
const serviceMessagePort = getServiceMessagePort();
|
||||
const promise = serviceMessagePort.invoke('download', {
|
||||
fileName,
|
||||
headers,
|
||||
id: downloadId
|
||||
});
|
||||
|
||||
promise.catch(errorHandler);
|
||||
deferred.catch(() => {
|
||||
getServiceMessagePort().invoke('downloadCancel', downloadId);
|
||||
});
|
||||
|
||||
class f implements StreamWriter {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
public async write(part: Uint8Array, offset?: number) {
|
||||
return serviceMessagePort.invoke('downloadChunk', {
|
||||
id: downloadId,
|
||||
chunk: part
|
||||
});
|
||||
}
|
||||
|
||||
public finalize(saveToStorage?: boolean): Promise<Blob> {
|
||||
return serviceMessagePort.invoke('downloadFinalize', downloadId).then(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
class d implements FileStorage {
|
||||
public getFile(fileName: string): Promise<any> {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
public getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter> {
|
||||
return Promise.resolve(new f());
|
||||
}
|
||||
}
|
||||
|
||||
fileStorage = new d();
|
||||
}
|
||||
|
||||
fileStorage.getFile(fileName).then(async(blob: Blob) => {
|
||||
//this.log('maybe cached', fileName);
|
||||
//throw '';
|
||||
|
||||
if(blob.size < size) {
|
||||
//this.log('downloadFile need to deleteFile 2, wrong size:', blob.size, size);
|
||||
if(!options.onlyCache) {
|
||||
await this.delete(fileName);
|
||||
}
|
||||
@ -459,118 +535,121 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
|
||||
deferred.resolve(blob);
|
||||
}).catch((err) => {
|
||||
}).catch(async(err: ApiError) => {
|
||||
if(options.onlyCache) {
|
||||
errorHandler(err);
|
||||
return;
|
||||
}
|
||||
|
||||
//this.log('not cached', fileName);
|
||||
const limit = options.limitPart || this.getLimitPart(size);
|
||||
const fileWriterPromise = fileStorage.getFileWriter(fileName, size || limit, mimeType);
|
||||
const limit = options.limitPart || this.getLimitPart(size, false);
|
||||
const writerPromise = fileStorage.getWriter(fileName, size || limit, mimeType);
|
||||
|
||||
fileWriterPromise.then((fileWriter) => {
|
||||
cacheFileWriter = fileWriter;
|
||||
let offset: number;
|
||||
let startOffset = 0;
|
||||
let writeFilePromise: CancellablePromise<void> = Promise.resolve(),
|
||||
writeFileDeferred: CancellablePromise<void>;
|
||||
//const maxRequests = 13107200 / limit; // * 100 Mb speed
|
||||
const maxRequests = Infinity;
|
||||
const writer = cacheWriter = await writerPromise;
|
||||
|
||||
let offset: number;
|
||||
let startOffset = 0;
|
||||
let writePromise: CancellablePromise<void> = Promise.resolve(),
|
||||
writeDeferred: CancellablePromise<void>;
|
||||
//const maxRequests = 13107200 / limit; // * 100 Mb speed
|
||||
const maxRequests = Infinity;
|
||||
|
||||
//console.error('maxRequests', maxRequests);
|
||||
const processDownloaded = async(bytes: Uint8Array) => {
|
||||
if(process) {
|
||||
//const perf = performance.now();
|
||||
const processed = await process(bytes, fileName);
|
||||
//this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process);
|
||||
return processed;
|
||||
}
|
||||
|
||||
const processDownloaded = async(bytes: Uint8Array) => {
|
||||
if(process) {
|
||||
//const perf = performance.now();
|
||||
const processed = await process(bytes, fileName);
|
||||
//this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process);
|
||||
return processed;
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this);
|
||||
|
||||
const delayed: Delayed[] = [];
|
||||
offset = startOffset;
|
||||
do {
|
||||
writeDeferred = deferredPromise<void>();
|
||||
delayed.push({offset, writePromise, writeDeferred});
|
||||
writePromise = writeDeferred;
|
||||
offset += limit;
|
||||
} while(offset < size);
|
||||
|
||||
const progress: Progress = {done: 0, offset, total: size, fileName};
|
||||
const dispatchProgress = () => {
|
||||
progress.done = done;
|
||||
deferred.notify?.(progress);
|
||||
};
|
||||
|
||||
const throttledDispatchProgress = throttle(dispatchProgress, 50, true);
|
||||
|
||||
let done = 0;
|
||||
const superpuper = async() => {
|
||||
//if(!delayed.length) return;
|
||||
|
||||
const {offset, writePromise, writeDeferred} = delayed.shift();
|
||||
try {
|
||||
checkCancel();
|
||||
|
||||
// @ts-ignore
|
||||
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel);
|
||||
|
||||
const bytes = result.bytes;
|
||||
|
||||
if(delayed.length) {
|
||||
superpuper();
|
||||
}
|
||||
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this);
|
||||
|
||||
const delayed: Delayed[] = [];
|
||||
offset = startOffset;
|
||||
do {
|
||||
////this.log('offset:', startOffset);
|
||||
writeFileDeferred = deferredPromise<void>();
|
||||
delayed.push({offset, writeFilePromise, writeFileDeferred});
|
||||
writeFilePromise = writeFileDeferred;
|
||||
offset += limit;
|
||||
} while(offset < size);
|
||||
|
||||
let done = 0;
|
||||
const superpuper = async() => {
|
||||
//if(!delayed.length) return;
|
||||
|
||||
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
|
||||
try {
|
||||
checkCancel();
|
||||
|
||||
// @ts-ignore
|
||||
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel);
|
||||
|
||||
const bytes = result.bytes;
|
||||
|
||||
if(delayed.length) {
|
||||
superpuper();
|
||||
}
|
||||
|
||||
this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
|
||||
const isFinal = (offset + limit) >= size || !bytes.byteLength;
|
||||
if(bytes.byteLength) {
|
||||
//done += limit;
|
||||
done += bytes.byteLength;
|
||||
|
||||
//if(!isFinal) {
|
||||
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
|
||||
const progress: Progress = {done, offset, total: size, fileName};
|
||||
deferred.notify(progress);
|
||||
//}
|
||||
|
||||
await writeFilePromise;
|
||||
checkCancel();
|
||||
|
||||
await fileWriter.write(bytes, offset);
|
||||
}
|
||||
|
||||
if(isFinal && process) {
|
||||
const bytes = fileWriter.getParts();
|
||||
const processedResult = await processDownloaded(bytes);
|
||||
checkCancel();
|
||||
|
||||
fileWriter.replaceParts(processedResult);
|
||||
}
|
||||
|
||||
writeFileDeferred.resolve();
|
||||
const byteLength = bytes.byteLength;
|
||||
this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
|
||||
const isFinal = (offset + limit) >= size || !byteLength;
|
||||
if(byteLength) {
|
||||
done += byteLength;
|
||||
|
||||
if(isFinal) {
|
||||
resolved = true;
|
||||
|
||||
const realSize = size || bytes.byteLength;
|
||||
if(!size) {
|
||||
fileWriter.trim(realSize);
|
||||
}
|
||||
|
||||
deferred.resolve(fileWriter.finalize(realSize < MAX_FILE_SAVE_SIZE));
|
||||
dispatchProgress();
|
||||
} else {
|
||||
throttledDispatchProgress();
|
||||
}
|
||||
} catch(err) {
|
||||
errorHandler(err as Error);
|
||||
}
|
||||
};
|
||||
|
||||
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
|
||||
superpuper();
|
||||
await writePromise;
|
||||
checkCancel();
|
||||
|
||||
// const perf = performance.now();
|
||||
await writer.write(bytes, offset);
|
||||
checkCancel();
|
||||
// downloadId && this.log('write time', performance.now() - perf);
|
||||
}
|
||||
|
||||
if(isFinal && process) {
|
||||
const bytes = writer.getParts();
|
||||
const processedResult = await processDownloaded(bytes);
|
||||
checkCancel();
|
||||
|
||||
writer.replaceParts(processedResult);
|
||||
}
|
||||
|
||||
writeDeferred.resolve();
|
||||
|
||||
if(isFinal) {
|
||||
resolved = true;
|
||||
|
||||
const realSize = size || byteLength;
|
||||
if(!size) {
|
||||
writer.trim(realSize);
|
||||
}
|
||||
|
||||
deferred.resolve(await writer.finalize(realSize <= MAX_FILE_SAVE_SIZE));
|
||||
}
|
||||
} catch(err) {
|
||||
errorHandler(err as ApiError);
|
||||
}
|
||||
}).catch((err) => {
|
||||
if(!['STORAGE_OFFLINE'].includes(err)) {
|
||||
this.log.error('saveFile error:', err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
|
||||
superpuper();
|
||||
}
|
||||
});
|
||||
|
||||
const checkCancel = () => {
|
||||
@ -581,8 +660,7 @@ export class ApiFileManager extends AppManager {
|
||||
|
||||
deferred.cancel = () => {
|
||||
if(!error && !resolved) {
|
||||
const error = new Error('Canceled');
|
||||
error.type = 'DOWNLOAD_CANCELED';
|
||||
const error = makeError('DOWNLOAD_CANCELED');
|
||||
errorHandler(error);
|
||||
}
|
||||
};
|
||||
@ -610,8 +688,8 @@ export class ApiFileManager extends AppManager {
|
||||
// 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 Photo.photo).id);
|
||||
else if(isPhoto) media = this.appPhotosManager.getPhoto((media as Document.document).id);
|
||||
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);
|
||||
|
||||
@ -653,7 +731,6 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
|
||||
private delete(fileName: string) {
|
||||
//this.log('will delete file:', fileName);
|
||||
delete this.downloadPromises[fileName];
|
||||
return this.getFileStorage().delete(fileName);
|
||||
}
|
||||
@ -665,16 +742,7 @@ export class ApiFileManager extends AppManager {
|
||||
let canceled = false,
|
||||
resolved = false,
|
||||
doneParts = 0,
|
||||
partSize = 262144; // 256 Kb
|
||||
|
||||
/* if(fileSize > (524288 * 3000)) {
|
||||
partSize = 1024 * 1024;
|
||||
activeDelta = 8;
|
||||
} else */if(fileSize > 67108864) {
|
||||
partSize = 524288;
|
||||
} else if(fileSize < 102400) {
|
||||
partSize = 32768;
|
||||
}
|
||||
partSize = this.getLimitPart(fileSize, true);
|
||||
|
||||
fileName ||= getFileNameForUpload(file);
|
||||
|
||||
@ -694,12 +762,12 @@ export class ApiFileManager extends AppManager {
|
||||
};
|
||||
|
||||
const deferred = deferredPromise<typeof resultInputFile>();
|
||||
if(totalParts > 4000) {
|
||||
deferred.reject({type: 'FILE_TOO_BIG'});
|
||||
if(totalParts > this.maxUploadParts) {
|
||||
deferred.reject(makeError('FILE_TOO_BIG'));
|
||||
return deferred;
|
||||
}
|
||||
|
||||
let errorHandler = (error: any) => {
|
||||
let errorHandler = (error: ApiError) => {
|
||||
if(error?.type !== 'UPLOAD_CANCELED') {
|
||||
this.log.error('Up Error', error);
|
||||
}
|
||||
@ -713,10 +781,6 @@ export class ApiFileManager extends AppManager {
|
||||
|
||||
const id = this.tempId++;
|
||||
|
||||
/* setInterval(() => {
|
||||
console.log(file);
|
||||
}, 1e3); */
|
||||
|
||||
const self = this;
|
||||
function* generator() {
|
||||
for(let offset = 0; offset < fileSize; offset += partSize) {
|
||||
@ -726,7 +790,7 @@ export class ApiFileManager extends AppManager {
|
||||
|
||||
return readBlobAsArrayBuffer(blob).then((buffer) => {
|
||||
if(canceled) {
|
||||
throw {type: 'UPLOAD_CANCELED'};
|
||||
throw makeError('UPLOAD_CANCELED');
|
||||
}
|
||||
|
||||
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length);
|
||||
@ -800,10 +864,9 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
|
||||
deferred.cancel = () => {
|
||||
//this.log('cancel upload', canceled, resolved);
|
||||
if(!canceled && !resolved) {
|
||||
canceled = true;
|
||||
errorHandler({type: 'UPLOAD_CANCELED'});
|
||||
errorHandler(makeError('UPLOAD_CANCELED'));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,7 @@ import type { MethodDeclMap } from '../../layer';
|
||||
import deferredPromise, { CancellablePromise } from '../../helpers/cancellablePromise';
|
||||
import App from '../../config/app';
|
||||
import { MOUNT_CLASS_TO } from '../../config/debug';
|
||||
import { IDB } from '../idb';
|
||||
import { IDB } from '../files/idb';
|
||||
import CryptoWorker from "../crypto/cryptoMessagePort";
|
||||
import ctx from '../../environment/ctx';
|
||||
import noop from '../../helpers/noop';
|
||||
@ -38,17 +38,6 @@ import { getEnvironment } from '../../environment/utils';
|
||||
import toggleStorages from '../../helpers/toggleStorages';
|
||||
import type TcpObfuscated from './transports/tcpObfuscated';
|
||||
|
||||
export type ApiError = Partial<{
|
||||
code: number,
|
||||
type: string,
|
||||
description: string,
|
||||
originalError: any,
|
||||
stack: string,
|
||||
handled: boolean,
|
||||
input: string,
|
||||
message: ApiError
|
||||
}>;
|
||||
|
||||
/* class RotatableArray<T> {
|
||||
public array: Array<T> = [];
|
||||
private lastIndex = -1;
|
||||
|
@ -9,7 +9,6 @@ import { ignoreRestrictionReasons } from "../../helpers/restrictions";
|
||||
import { MethodDeclMap, User } from "../../layer";
|
||||
import { InvokeApiOptions } from "../../types";
|
||||
import { AppManager } from "../appManagers/manager";
|
||||
import { ApiError } from "./apiManager";
|
||||
import { MTAppConfig } from "./appConfig";
|
||||
import { UserAuth } from "./mtproto_config";
|
||||
import { MTMessage } from "./networker";
|
||||
|
@ -22,7 +22,6 @@ import CryptoWorker from "../crypto/cryptoMessagePort";
|
||||
import { logger, LogTypes } from "../logger";
|
||||
import DEBUG from "../../config/debug";
|
||||
import { Awaited, DcId } from "../../types";
|
||||
import { ApiError } from "./apiManager";
|
||||
import addPadding from "../../helpers/bytes/addPadding";
|
||||
import bytesCmp from "../../helpers/bytes/bytesCmp";
|
||||
import bytesFromHex from "../../helpers/bytes/bytesFromHex";
|
||||
|
@ -18,6 +18,7 @@ export const REPLIES_HIDDEN_CHANNEL_ID: ChatId = 777;
|
||||
export const SERVICE_PEER_ID: PeerId = 777000;
|
||||
export const MUTE_UNTIL = 0x7FFFFFFF;
|
||||
export const BOT_START_PARAM = '';
|
||||
export const MAX_FILE_SAVE_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
export const FOLDER_ID_ALL: REAL_FOLDER_ID = 0;
|
||||
export const FOLDER_ID_ARCHIVE: REAL_FOLDER_ID = 1;
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Awaited } from '../../types';
|
||||
import type { CacheStorageDbName } from '../cacheStorage';
|
||||
import type { CacheStorageDbName } from '../files/cacheStorage';
|
||||
import type { State } from '../../config/state';
|
||||
import type { Message, MessagePeerReaction, PeerNotifySettings } from '../../layer';
|
||||
import { CryptoMethods } from '../crypto/crypto_methods';
|
||||
@ -247,6 +247,8 @@ class ApiManagerProxy extends MTProtoMessagePort {
|
||||
const worker = navigator.serviceWorker;
|
||||
this._registerServiceWorker();
|
||||
|
||||
// worker.startMessages();
|
||||
|
||||
worker.addEventListener('controllerchange', () => {
|
||||
this.log.warn('controllerchange');
|
||||
|
||||
|
@ -207,7 +207,7 @@ export default class MTPNetworker {
|
||||
const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : '';
|
||||
this.name = 'NET-' + dcId + suffix;
|
||||
//this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error);
|
||||
this.log = logger(this.name, LogTypes.Log | LogTypes.Debug | LogTypes.Error | LogTypes.Warn, undefined);
|
||||
this.log = logger(this.name, LogTypes.Log /* | LogTypes.Debug */ | LogTypes.Error | LogTypes.Warn);
|
||||
this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */);
|
||||
|
||||
// Test resend after bad_server_salt
|
||||
@ -1310,9 +1310,10 @@ export default class MTPNetworker {
|
||||
if(!(this.transport instanceof HTTP)) return promise;
|
||||
/// #endif
|
||||
|
||||
const baseError = {
|
||||
const baseError: ApiError = {
|
||||
code: 406,
|
||||
type: 'NETWORK_BAD_RESPONSE',
|
||||
// @ts-ignore
|
||||
transport: this.transport
|
||||
};
|
||||
|
||||
@ -1596,13 +1597,13 @@ export default class MTPNetworker {
|
||||
}
|
||||
}
|
||||
|
||||
private processError(rawError: {error_message: string, error_code: number}) {
|
||||
private processError(rawError: {error_message: string, error_code: number}): ApiError {
|
||||
const matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
|
||||
rawError.error_code = rawError.error_code;
|
||||
|
||||
return {
|
||||
code: !rawError.error_code || rawError.error_code <= 0 ? 500 : rawError.error_code,
|
||||
type: matches[1] || 'UNKNOWN',
|
||||
type: matches[1] as any || 'UNKNOWN',
|
||||
description: matches[3] || ('CODE#' + rawError.error_code + ' ' + rawError.error_message),
|
||||
originalError: rawError
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import { logger } from "../logger";
|
||||
import bytesToHex from "../../helpers/bytes/bytesToHex";
|
||||
import deepEqual from "../../helpers/object/deepEqual";
|
||||
import { AppManager } from "../appManagers/manager";
|
||||
import makeError from "../../helpers/makeError";
|
||||
|
||||
export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds | ReferenceContext.referenceContextReactions | ReferenceContext.referenceContextUserFull;
|
||||
export namespace ReferenceContext {
|
||||
@ -173,7 +174,7 @@ export class ReferenceDatabase extends AppManager {
|
||||
|
||||
this.log.error('refreshReference: no new context, reference before:', hex, 'after:', newHex, context);
|
||||
|
||||
throw 'NO_NEW_CONTEXT';
|
||||
throw makeError('NO_NEW_CONTEXT');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -12,12 +12,14 @@ import { logger, LogTypes } from '../logger';
|
||||
import { CACHE_ASSETS_NAME, requestCache } from './cache';
|
||||
import onStreamFetch from './stream';
|
||||
import { closeAllNotifications, onPing } from './push';
|
||||
import CacheStorageController from '../cacheStorage';
|
||||
import CacheStorageController from '../files/cacheStorage';
|
||||
import { IS_SAFARI } from '../../environment/userAgent';
|
||||
import ServiceMessagePort from './serviceMessagePort';
|
||||
import ServiceMessagePort, { ServiceDownloadTaskPayload } from './serviceMessagePort';
|
||||
import listenMessagePort from '../../helpers/listenMessagePort';
|
||||
import { getWindowClients } from '../../helpers/context';
|
||||
import { MessageSendPort } from '../mtproto/superMessagePort';
|
||||
import noop from '../../helpers/noop';
|
||||
import makeError from '../../helpers/makeError';
|
||||
|
||||
export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
|
||||
const ctx = self as any as ServiceWorkerGlobalScope;
|
||||
@ -33,18 +35,36 @@ const sendMessagePort = (source: MessageSendPort) => {
|
||||
};
|
||||
|
||||
const sendMessagePortIfNeeded = (source: MessageSendPort) => {
|
||||
if(!connectedWindows && !_mtprotoMessagePort) {
|
||||
if(!connectedWindows.size && !_mtprotoMessagePort) {
|
||||
sendMessagePort(source);
|
||||
}
|
||||
};
|
||||
|
||||
const onWindowConnected = (source: MessageSendPort) => {
|
||||
sendMessagePortIfNeeded(source);
|
||||
const onWindowConnected = (source: WindowClient) => {
|
||||
log('window connected', source.id);
|
||||
|
||||
if(source.frameType === 'none') {
|
||||
log.warn('maybe a bugged Safari starting window', source.id);
|
||||
return;
|
||||
}
|
||||
|
||||
++connectedWindows;
|
||||
log('window connected');
|
||||
sendMessagePortIfNeeded(source);
|
||||
connectedWindows.add(source.id);
|
||||
};
|
||||
|
||||
type DownloadType = Uint8Array;
|
||||
type DownloadItem = ServiceDownloadTaskPayload & {
|
||||
transformStream: TransformStream<DownloadType, DownloadType>,
|
||||
readableStream: ReadableStream<DownloadType>,
|
||||
writableStream: WritableStream<DownloadType>,
|
||||
writer: WritableStreamDefaultWriter<DownloadType>,
|
||||
// controller: TransformStreamDefaultController<DownloadType>,
|
||||
// promise: CancellablePromise<void>,
|
||||
used?: boolean
|
||||
};
|
||||
const downloadMap: Map<string, DownloadItem> = new Map();
|
||||
const DOWNLOAD_ERROR = makeError('UNKNOWN');
|
||||
|
||||
export const serviceMessagePort = new ServiceMessagePort<false>();
|
||||
serviceMessagePort.addMultipleEventsListeners({
|
||||
notificationsClear: closeAllNotifications,
|
||||
@ -58,32 +78,117 @@ serviceMessagePort.addMultipleEventsListeners({
|
||||
},
|
||||
|
||||
hello: (payload, source) => {
|
||||
onWindowConnected(source);
|
||||
onWindowConnected(source as any as WindowClient);
|
||||
},
|
||||
|
||||
download: (payload) => {
|
||||
const {id} = payload;
|
||||
if(downloadMap.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const writableStrategy = new ByteLengthQueuingStrategy({highWaterMark: 1024 * 1024});
|
||||
// let controller: TransformStreamDefaultController<DownloadType>;
|
||||
const transformStream = new TransformStream<DownloadType, DownloadType>(/* {
|
||||
start: (_controller) => controller = _controller,
|
||||
}, {
|
||||
highWaterMark: 1,
|
||||
size: (chunk) => chunk.byteLength
|
||||
}, new CountQueuingStrategy({highWaterMark: 4}) */);
|
||||
|
||||
const {readable, writable} = transformStream;
|
||||
const writer = writable.getWriter();
|
||||
// const promise = deferredPromise<void>();
|
||||
// promise.catch(noop).finally(() => {
|
||||
// downloadMap.delete(id);
|
||||
// });
|
||||
|
||||
// writer.closed.then(promise.resolve, promise.reject);
|
||||
|
||||
writer.closed.catch(noop).finally(() => {
|
||||
log.error('closed writer');
|
||||
downloadMap.delete(id);
|
||||
});
|
||||
|
||||
const item: DownloadItem = {
|
||||
...payload,
|
||||
transformStream,
|
||||
readableStream: readable,
|
||||
writableStream: writable,
|
||||
writer,
|
||||
// promise,
|
||||
// controller
|
||||
};
|
||||
|
||||
downloadMap.set(id, item);
|
||||
|
||||
return writer.closed.catch(() => {throw DOWNLOAD_ERROR;});
|
||||
// return promise;
|
||||
},
|
||||
|
||||
downloadChunk: ({id, chunk}) => {
|
||||
const item = downloadMap.get(id);
|
||||
if(!item) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// return item.controller.enqueue(chunk);
|
||||
return item.writer.write(chunk);
|
||||
},
|
||||
|
||||
downloadFinalize: (id) => {
|
||||
const item = downloadMap.get(id);
|
||||
if(!item) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// item.promise.resolve();
|
||||
// return item.controller.terminate();
|
||||
return item.writer.close();
|
||||
},
|
||||
|
||||
downloadCancel: (id) => {
|
||||
const item = downloadMap.get(id);
|
||||
if(!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// item.promise.reject();
|
||||
// return item.controller.error();
|
||||
return item.writer.abort();
|
||||
}
|
||||
});
|
||||
|
||||
// * service worker can be killed, so won't get 'hello' event
|
||||
getWindowClients().then((windowClients) => {
|
||||
log(`got ${windowClients.length} windows from the start`);
|
||||
windowClients.forEach((windowClient) => {
|
||||
onWindowConnected(windowClient);
|
||||
});
|
||||
});
|
||||
|
||||
let connectedWindows = 0;
|
||||
let connectedWindows: Set<string> = new Set();
|
||||
listenMessagePort(serviceMessagePort, undefined, (source) => {
|
||||
if(source === _mtprotoMessagePort) {
|
||||
const isWindowClient = source instanceof WindowClient;
|
||||
if(!isWindowClient || !connectedWindows.has(source.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log('window disconnected');
|
||||
connectedWindows = Math.max(0, connectedWindows - 1);
|
||||
if(!connectedWindows) {
|
||||
connectedWindows.delete(source.id);
|
||||
if(!connectedWindows.size) {
|
||||
log.warn('no windows left');
|
||||
|
||||
if(_mtprotoMessagePort) {
|
||||
serviceMessagePort.detachPort(_mtprotoMessagePort);
|
||||
_mtprotoMessagePort = undefined;
|
||||
}
|
||||
|
||||
if(downloadMap.size) {
|
||||
for(const [id, item] of downloadMap) {
|
||||
item.writer.abort().catch(noop);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
/// #endif
|
||||
@ -102,15 +207,28 @@ const onFetch = (event: FetchEvent): void => {
|
||||
try {
|
||||
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
|
||||
|
||||
//log.debug('[fetch]:', event);
|
||||
// log.debug('[fetch]:', event);
|
||||
|
||||
switch(scope) {
|
||||
case 'stream': {
|
||||
onStreamFetch(event, params);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'download': {
|
||||
const item = downloadMap.get(params);
|
||||
if(!item || item.used) {
|
||||
break;
|
||||
}
|
||||
|
||||
item.used = true;
|
||||
const response = new Response(item.transformStream.readable, {headers: item.headers});
|
||||
event.respondWith(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
log.error('fetch error', err);
|
||||
event.respondWith(new Response('', {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
@ -133,13 +251,13 @@ ctx.addEventListener('activate', (event) => {
|
||||
event.waitUntil(ctx.clients.claim());
|
||||
});
|
||||
|
||||
ctx.onerror = (error) => {
|
||||
log.error('error:', error);
|
||||
};
|
||||
// ctx.onerror = (error) => {
|
||||
// log.error('error:', error);
|
||||
// };
|
||||
|
||||
ctx.onunhandledrejection = (error) => {
|
||||
log.error('onunhandledrejection:', error);
|
||||
};
|
||||
// ctx.onunhandledrejection = (error) => {
|
||||
// log.error('onunhandledrejection:', error);
|
||||
// };
|
||||
|
||||
ctx.onoffline = ctx.ononline = onChangeState;
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { Database } from "../../config/databases";
|
||||
import DATABASE_STATE from "../../config/databases/state";
|
||||
import { IS_FIREFOX } from "../../environment/userAgent";
|
||||
import deepEqual from "../../helpers/object/deepEqual";
|
||||
import IDBStorage from "../idb";
|
||||
import IDBStorage from "../files/idb";
|
||||
import { log, serviceMessagePort } from "./index.service";
|
||||
import { ServicePushPingTaskPayload } from "./serviceMessagePort";
|
||||
|
||||
|
@ -27,6 +27,12 @@ export type ServiceRequestFilePartTaskPayload = {
|
||||
limit: number
|
||||
};
|
||||
|
||||
export type ServiceDownloadTaskPayload = {
|
||||
fileName: string,
|
||||
headers: any,
|
||||
id: string,
|
||||
};
|
||||
|
||||
export type ServiceEvent = {
|
||||
port: (payload: void, source: MessageEventSource, event: MessageEvent) => void
|
||||
};
|
||||
@ -36,7 +42,13 @@ export default class ServiceMessagePort<Master extends boolean = false> extends
|
||||
notificationsClear: () => void,
|
||||
toggleStorages: (payload: {enabled: boolean, clearWrite: boolean}) => void,
|
||||
pushPing: (payload: ServicePushPingTaskPayload, source: MessageEventSource, event: MessageEvent) => void,
|
||||
hello: (payload: void, source: MessageEventSource, event: MessageEvent) => void
|
||||
hello: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
|
||||
|
||||
// from mtproto worker
|
||||
download: (payload: ServiceDownloadTaskPayload) => void,
|
||||
downloadChunk: (payload: {id: ServiceDownloadTaskPayload['id'], chunk: Uint8Array}) => void
|
||||
downloadFinalize: (payload: ServiceDownloadTaskPayload['id']) => void,
|
||||
downloadCancel: (payload: ServiceDownloadTaskPayload['id']) => void
|
||||
}, {
|
||||
// to main thread
|
||||
pushClick: (payload: PushNotificationObject) => void,
|
||||
|
@ -8,7 +8,7 @@ import readBlobAsUint8Array from "../../helpers/blob/readBlobAsUint8Array";
|
||||
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
|
||||
import debounce from "../../helpers/schedulers/debounce";
|
||||
import { InputFileLocation } from "../../layer";
|
||||
import CacheStorageController from "../cacheStorage";
|
||||
import CacheStorageController from "../files/cacheStorage";
|
||||
import { DownloadOptions, MyUploadFile } from "../mtproto/apiFileManager";
|
||||
import { getMtprotoMessagePort, log, serviceMessagePort } from "./index.service";
|
||||
import { ServiceRequestFilePartTaskPayload } from "./serviceMessagePort";
|
||||
@ -140,8 +140,8 @@ class Stream {
|
||||
const key = this.getChunkKey(alignedOffset, limit);
|
||||
return cacheStorage.getFile(key).then((blob: Blob) => {
|
||||
return fromPreload ? new Uint8Array() : readBlobAsUint8Array(blob);
|
||||
}, (error) => {
|
||||
if(error === 'NO_ENTRY_FOUND') {
|
||||
}, (error: ApiError) => {
|
||||
if(error.type === 'NO_ENTRY_FOUND') {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromi
|
||||
import { IS_WORKER } from "../helpers/context";
|
||||
import throttle from "../helpers/schedulers/throttle";
|
||||
//import { WorkerTaskTemplate } from "../types";
|
||||
import IDBStorage from "./idb";
|
||||
import IDBStorage from "./files/idb";
|
||||
|
||||
function noop() {}
|
||||
|
||||
@ -166,8 +166,9 @@ export default class AppStorage<
|
||||
}
|
||||
|
||||
// console.log('[AS]: get time', keys, performance.now() - perf);
|
||||
}, (error) => {
|
||||
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(error)) {
|
||||
}, (error: ApiError) => {
|
||||
const ignoreErrors: Set<ErrorType> = new Set(['NO_ENTRY_FOUND', 'STORAGE_OFFLINE']);
|
||||
if(!ignoreErrors.has(error.type)) {
|
||||
this.useStorage = false;
|
||||
console.error('[AS]: get error:', error, keys, storeName);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { AppManager } from "../appManagers/manager";
|
||||
import findAndSplice from "../../helpers/array/findAndSplice";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import { FOLDER_ID_ALL, FOLDER_ID_ARCHIVE, REAL_FOLDERS, REAL_FOLDER_ID, START_LOCAL_ID } from "../mtproto/mtproto_config";
|
||||
import makeError from "../../helpers/makeError";
|
||||
|
||||
export type MyDialogFilter = DialogFilter.dialogFilter;
|
||||
|
||||
@ -313,7 +314,7 @@ export default class FiltersStorage extends AppManager {
|
||||
|
||||
if(!wasPinned) {
|
||||
if(filter.pinned_peers.length >= (await this.apiManager.getConfig()).pinned_infolder_count_max) {
|
||||
return Promise.reject({type: 'PINNED_DIALOGS_TOO_MUCH'});
|
||||
return Promise.reject(makeError('PINNED_DIALOGS_TOO_MUCH'));
|
||||
}
|
||||
|
||||
filter.pinned_peers.unshift(this.appPeersManager.getInputPeerById(peerId));
|
||||
|
@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import type { DcId } from '../types';
|
||||
import type { ApiError } from '../lib/mtproto/apiManager';
|
||||
import Page from './page';
|
||||
import { AuthAuthorization, AuthLoginToken } from '../layer';
|
||||
import App from '../config/app';
|
||||
|
@ -1490,6 +1490,13 @@ $bubble-beside-button-width: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.document {
|
||||
.time {
|
||||
height: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* .document,
|
||||
.audio {
|
||||
.time.tgico {
|
||||
|
@ -23,6 +23,14 @@
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
&-text {
|
||||
opacity: 0;
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: opacity .2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.document:not(.document-with-thumb) & {
|
||||
padding: 1.5625rem .25rem 0 .25rem;
|
||||
|
||||
@ -43,8 +51,8 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
// width: var(--size);
|
||||
// height: var(--size);
|
||||
border-bottom-left-radius: .25rem;
|
||||
border-style: solid;
|
||||
border-width: calc(var(--size) / 2);
|
||||
@ -52,25 +60,54 @@
|
||||
border-bottom-color: rgba(0, 0, 0, .25);
|
||||
border-top-color: var(--message-background-color);
|
||||
border-right-color: var(--message-background-color);
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: border-width .2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.downloaded) {
|
||||
@include hover() {
|
||||
.document-ico:after {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.document-ico-text {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.preloader-container {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.downloading) {
|
||||
.document-ico-text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.preloader-container {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.downloading {
|
||||
.document-ico:after {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-ico,
|
||||
&-download {
|
||||
&-ico {
|
||||
font-size: 1.125rem;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
&-ico,
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-download {
|
||||
background-color: var(--background-color);
|
||||
border-radius: $border-radius;
|
||||
&-name,
|
||||
&-size {
|
||||
@include text-overflow();
|
||||
}
|
||||
|
||||
&.ext-zip {
|
||||
@ -95,7 +132,7 @@
|
||||
}
|
||||
|
||||
.document-download {
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.preloader-circular {
|
||||
@ -123,13 +160,12 @@
|
||||
}
|
||||
|
||||
&-size {
|
||||
white-space: nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--font-size-14);
|
||||
line-height: var(--line-height-14);
|
||||
//padding-right: 32px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preloader-container {
|
||||
|
Loading…
x
Reference in New Issue
Block a user