From 2a5eeb3111935975dd5a0fcb7e30236975dc7767 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 Aug 2022 19:23:12 +0200 Subject: [PATCH] Downloads: Fallback to Service Worker when OPFS not supported (#1949) --- src/api/gramjs/methods/client.ts | 4 +- src/api/gramjs/methods/media.ts | 1 + src/api/types/media.ts | 1 + src/api/types/misc.ts | 1 + src/components/common/Audio.tsx | 1 + src/components/common/Document.tsx | 3 +- src/components/main/DownloadManager.tsx | 26 +++-- .../common/PremiumLimitReachedModal.tsx | 4 +- .../mediaViewer/MediaViewerActions.tsx | 3 +- src/components/middle/composer/Composer.tsx | 4 +- src/components/middle/message/Photo.tsx | 5 +- src/components/middle/message/Video.tsx | 3 +- src/config.ts | 2 +- src/global/actions/api/initial.ts | 5 +- src/global/helpers/messageMedia.ts | 20 +++- src/lib/gramjs/Utils.js | 30 +++++- src/lib/gramjs/client/TelegramClient.d.ts | 10 +- src/lib/gramjs/client/downloadFile.ts | 8 +- src/lib/gramjs/client/uploadFile.ts | 4 +- src/serviceWorker.ts | 7 +- src/serviceWorker/download.ts | 99 +++++++++++++++++++ src/serviceWorker/progressive.ts | 12 +-- src/util/environment.ts | 2 + src/util/mediaLoader.ts | 18 +++- 24 files changed, 227 insertions(+), 46 deletions(-) create mode 100644 src/serviceWorker/download.ts diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 35728f906..942cadca8 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -50,7 +50,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) onUpdate = _onUpdate; const { - userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, + userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, maxBufferSize, } = initialArgs; const session = new sessions.CallbackSession(sessionData, onSessionUpdate); @@ -60,6 +60,8 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) if (isMovSupported) SUPPORTED_VIDEO_CONTENT_TYPES.add(VIDEO_MOV_TYPE); // eslint-disable-next-line no-restricted-globals (self as any).isWebmSupported = isWebmSupported; + // eslint-disable-next-line no-restricted-globals + (self as any).maxBufferSize = maxBufferSize; client = new TelegramClient( session, diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 5db806a28..633a4dda4 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -253,6 +253,7 @@ async function parseMedia( case ApiMediaFormat.Text: return data.toString(); case ApiMediaFormat.Progressive: + case ApiMediaFormat.DownloadUrl: return data.buffer; } diff --git a/src/api/types/media.ts b/src/api/types/media.ts index ca0dd8da0..3ce41c50a 100644 --- a/src/api/types/media.ts +++ b/src/api/types/media.ts @@ -5,6 +5,7 @@ export enum ApiMediaFormat { BlobUrl, Progressive, Stream, + DownloadUrl, Text, } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 48f8fc537..25b94aba0 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -9,6 +9,7 @@ export interface ApiInitialArgs { isTest?: boolean; isMovSupported?: boolean; isWebmSupported?: boolean; + maxBufferSize?: number; } export interface ApiOnProgress { diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index cb79a5f90..2d85c14c0 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -125,6 +125,7 @@ const Audio: FC = ({ const { loadProgress: downloadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'download'), !isDownloading, + getMessageMediaFormat(message, 'download'), ); const handleForcePlay = useCallback(() => { diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 94dd3337f..ff3b98a5c 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -9,6 +9,7 @@ import type { ApiMessage } from '../../api/types'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; import { getMediaTransferState, + getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, isMessageDocumentVideo, @@ -86,7 +87,7 @@ const Document: FC = ({ const documentHash = getMessageMediaHash(message, 'download'); const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress( - documentHash, !shouldDownload, undefined, undefined, undefined, true, + documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, undefined, true, ); const isLoaded = Boolean(mediaData); diff --git a/src/components/main/DownloadManager.tsx b/src/components/main/DownloadManager.tsx index c16929dec..7ddd41e55 100644 --- a/src/components/main/DownloadManager.tsx +++ b/src/components/main/DownloadManager.tsx @@ -6,11 +6,11 @@ import type { Thread } from '../../global/types'; import type { ApiMessage } from '../../api/types'; import { ApiMediaFormat } from '../../api/types'; -import { IS_OPFS_SUPPORTED } from '../../util/environment'; +import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/environment'; import * as mediaLoader from '../../util/mediaLoader'; import download from '../../util/download'; import { - getMessageContentFilename, getMessageMediaHash, + getMessageContentFilename, getMessageMediaFormat, getMessageMediaHash, } from '../../global/helpers'; import useRunDebounced from '../../hooks/useRunDebounced'; @@ -25,8 +25,6 @@ type StateProps = { const GLOBAL_UPDATE_DEBOUNCE = 1000; -const MAX_BLOB_SAFE_SIZE = 2000 * 1024 * 1024; - const processedMessages = new Set(); const downloadedMessages = new Set(); @@ -81,18 +79,30 @@ const DownloadManager: FC = ({ document, video, audio, } = message.content; const mediaSize = (document || video || audio)?.size || 0; - if (mediaSize > MAX_BLOB_SAFE_SIZE && !IS_OPFS_SUPPORTED) { + if (mediaSize > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) { showNotification({ - message: 'Downloading files bigger than 2GB is currently not supported in your browser.', + message: 'Downloading files bigger than 2GB is not supported in your browser.', }); handleMessageDownloaded(message); return; } - mediaLoader.fetch(downloadHash, ApiMediaFormat.BlobUrl, true).then((result) => { - if (result) { + const mediaFormat = getMessageMediaFormat(message, 'download'); + mediaLoader.fetch(downloadHash, mediaFormat, true).then((result) => { + if (mediaFormat === ApiMediaFormat.DownloadUrl) { + const url = new URL(result, window.document.baseURI); + const filename = getMessageContentFilename(message); + url.searchParams.set('filename', encodeURIComponent(filename)); + const downloadWindow = window.open(url.toString()); + downloadWindow?.addEventListener('beforeunload', () => { + showNotification({ + message: 'Download started. Please, do not close the app before it is finished.', + }); + }); + } else if (result) { download(result, getMessageContentFilename(message)); } + handleMessageDownloaded(message); }); }); diff --git a/src/components/main/premium/common/PremiumLimitReachedModal.tsx b/src/components/main/premium/common/PremiumLimitReachedModal.tsx index c3e3ae393..abe80ef1a 100644 --- a/src/components/main/premium/common/PremiumLimitReachedModal.tsx +++ b/src/components/main/premium/common/PremiumLimitReachedModal.tsx @@ -10,7 +10,7 @@ import { formatFileSize } from '../../../../util/textFormat'; import { getActions, withGlobal } from '../../../../global'; import { selectIsCurrentUserPremium, selectIsPremiumPurchaseBlocked } from '../../../../global/selectors'; import useLang from '../../../../hooks/useLang'; -import { FILEPART_SIZE } from '../../../../config'; +import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config'; import useFlag from '../../../../hooks/useFlag'; import Modal from '../../../ui/Modal'; @@ -60,7 +60,7 @@ const LIMIT_VALUE_FORMATTER: Partial = ({ const { loadProgress: downloadProgress } = useMediaWithLoadProgress( message && getMessageMediaHash(message, 'download'), !isDownloading, + message && getMessageMediaFormat(message, 'download'), ); const handleDownloadClick = useCallback(() => { diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 76c43d138..78f3a6386 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -31,7 +31,7 @@ import { EDITABLE_INPUT_ID, REPLIES_USER_ID, SEND_MESSAGE_ACTION_INTERVAL, - EDITABLE_INPUT_CSS_SELECTOR, FILEPART_SIZE, + EDITABLE_INPUT_CSS_SELECTOR, MAX_UPLOAD_FILEPART_SIZE, } from '../../../config'; import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; @@ -1355,7 +1355,7 @@ export default memo(withGlobal( attachMenuBots: global.attachMenu.bots, attachMenuPeerType: selectAttachMenuPeerType(global, chatId), theme: selectTheme(global), - fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * FILEPART_SIZE, + fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * MAX_UPLOAD_FILEPART_SIZE, captionLimit: selectCurrentLimit(global, 'captionLength'), isCurrentUserPremium: selectIsCurrentUserPremium(global), }; diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index 05a3b5e59..f56b983ec 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -14,6 +14,7 @@ import { getMessageMediaHash, getMediaTransferState, isOwnMessage, + getMessageMediaFormat, } from '../../../global/helpers'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; @@ -87,7 +88,9 @@ const Photo: FC = ({ const { loadProgress: downloadProgress, - } = useMediaWithLoadProgress(getMessageMediaHash(message, 'download'), !isDownloading); + } = useMediaWithLoadProgress( + getMessageMediaHash(message, 'download'), !isDownloading, getMessageMediaFormat(message, 'download'), + ); const { isUploading, isTransferring, transferProgress, diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index 058dc5957..31b030d5f 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useRef, useState } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiMessage } from '../../../api/types'; -import { ApiMediaFormat } from '../../../api/types'; import type { IMediaDimensions } from './helpers/calculateAlbumLayout'; import { formatMediaDuration } from '../../../util/dateFormat'; @@ -101,7 +100,7 @@ const Video: FC = ({ const { loadProgress: downloadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'download'), !isDownloading, - ApiMediaFormat.BlobUrl, + getMessageMediaFormat(message, 'download'), lastSyncTime, ); diff --git a/src/config.ts b/src/config.ts index b4fe4d17a..01ccfe7aa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -214,7 +214,7 @@ export const LIGHT_THEME_BG_COLOR = '#99BA92'; export const DARK_THEME_BG_COLOR = '#0F0F0F'; export const DEFAULT_PATTERN_COLOR = '#4A8E3A8C'; export const DARK_THEME_PATTERN_COLOR = '#0A0A0A8C'; -export const FILEPART_SIZE = 524288; +export const MAX_UPLOAD_FILEPART_SIZE = 524288; // Group calls export const GROUP_CALL_VOLUME_MULTIPLIER = 100; diff --git a/src/global/actions/api/initial.ts b/src/global/actions/api/initial.ts index eac8a5266..9dda0ac5e 100644 --- a/src/global/actions/api/initial.ts +++ b/src/global/actions/api/initial.ts @@ -13,7 +13,9 @@ import { IS_TEST, LOCK_SCREEN_ANIMATION_DURATION_MS, } from '../../../config'; -import { IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, PLATFORM_ENV } from '../../../util/environment'; +import { + IS_MOV_SUPPORTED, IS_WEBM_SUPPORTED, MAX_BUFFER_SIZE, PLATFORM_ENV, +} from '../../../util/environment'; import { unsubscribe } from '../../../util/notifications'; import * as cacheApi from '../../../util/cacheApi'; import { updateAppBadge } from '../../../util/appBadge'; @@ -42,6 +44,7 @@ addActionHandler('initApi', async (global, actions) => { isTest: window.location.search.includes('test'), isMovSupported: IS_MOV_SUPPORTED, isWebmSupported: IS_WEBM_SUPPORTED, + maxBufferSize: MAX_BUFFER_SIZE, }); }); diff --git a/src/global/helpers/messageMedia.ts b/src/global/helpers/messageMedia.ts index e0f73da9e..015773fbb 100644 --- a/src/global/helpers/messageMedia.ts +++ b/src/global/helpers/messageMedia.ts @@ -12,7 +12,13 @@ import type { } from '../../api/types'; import { ApiMediaFormat } from '../../api/types'; -import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment'; +import { + IS_OPFS_SUPPORTED, + IS_OPUS_SUPPORTED, + IS_PROGRESSIVE_SUPPORTED, + IS_SAFARI, + MAX_BUFFER_SIZE, +} from '../../util/environment'; import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages'; import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo'; @@ -306,14 +312,22 @@ export function getAudioHasCover(media: ApiAudio) { export function getMessageMediaFormat( message: ApiMessage, target: Target, ): ApiMediaFormat { - const { video, audio, voice } = message.content; + const { + video, audio, voice, document, + } = message.content; const fullVideo = video || getMessageWebPageVideo(message); + const size = (video || audio || document)?.size!; + if (target === 'download' && IS_PROGRESSIVE_SUPPORTED && size > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED) { + return ApiMediaFormat.DownloadUrl; + } if (fullVideo && IS_PROGRESSIVE_SUPPORTED && ( target === 'viewerFull' || target === 'inline' )) { return ApiMediaFormat.Progressive; - } else if (audio || voice) { + } + + if (audio || voice) { // Safari if (voice && !IS_OPUS_SUPPORTED) { return ApiMediaFormat.BlobUrl; diff --git a/src/lib/gramjs/Utils.js b/src/lib/gramjs/Utils.js index ad957dfef..91f727bd0 100644 --- a/src/lib/gramjs/Utils.js +++ b/src/lib/gramjs/Utils.js @@ -320,12 +320,35 @@ function getInputLocation(location) { */ /** - * Gets the appropriated part size when uploading or downloading files, + * Gets the appropriated part size when downloading files, * given an initial file size. * @param fileSize * @returns {Number} */ -function getAppropriatedPartSize(fileSize) { +function getDownloadPartSize(fileSize) { + if (fileSize <= 104857600) { // 100MB + return 128; + } + if (fileSize <= 786432000) { // 750MB + return 256; + } + if (fileSize <= 2097152000) { // 2000MB + return 512; + } + if (fileSize <= 4194304000) { // 4000MB + return 1024; + } + + throw new Error('File size too large'); +} + +/** + * Gets the appropriated part size when uploading files, + * given an initial file size. + * @param fileSize + * @returns {Number} + */ +function getUploadPartSize(fileSize) { if (fileSize <= 104857600) { // 100MB return 128; } @@ -683,7 +706,8 @@ module.exports = { getDisplayName, // resolveId, // isListLike, - getAppropriatedPartSize, + getDownloadPartSize, + getUploadPartSize, // getInputLocation, strippedPhotoToJpg, getDC, diff --git a/src/lib/gramjs/client/TelegramClient.d.ts b/src/lib/gramjs/client/TelegramClient.d.ts index 358ee9ca0..0e4fc42c7 100644 --- a/src/lib/gramjs/client/TelegramClient.d.ts +++ b/src/lib/gramjs/client/TelegramClient.d.ts @@ -1,9 +1,9 @@ -import { Api } from '..'; +import type { Api } from '..'; -import { BotAuthParams, UserAuthParams } from './auth'; -import { uploadFile, UploadFileParams } from './uploadFile'; -import { downloadFile, DownloadFileParams } from './downloadFile'; -import { TwoFaParams, updateTwoFaSettings } from './2fa'; +import type { BotAuthParams, UserAuthParams } from './auth'; +import type { uploadFile, UploadFileParams } from './uploadFile'; +import type { downloadFile, DownloadFileParams } from './downloadFile'; +import type { TwoFaParams, updateTwoFaSettings } from './2fa'; declare class TelegramClient { constructor(...args: any); diff --git a/src/lib/gramjs/client/downloadFile.ts b/src/lib/gramjs/client/downloadFile.ts index cfb31d6aa..bac3e7b39 100644 --- a/src/lib/gramjs/client/downloadFile.ts +++ b/src/lib/gramjs/client/downloadFile.ts @@ -1,8 +1,8 @@ import BigInt from 'big-integer'; import Api from '../tl/api'; import type TelegramClient from './TelegramClient'; -import { getAppropriatedPartSize } from '../Utils'; import { sleep, createDeferred } from '../Helpers'; +import { getDownloadPartSize } from '../Utils'; import errors from '../errors'; interface OnProgress { @@ -35,7 +35,6 @@ const MIN_CHUNK_SIZE = 4096; const DEFAULT_CHUNK_SIZE = 64; // kb const ONE_MB = 1024 * 1024; const DISCONNECT_SLEEP = 1000; -const MAX_BUFFER_SAFE_SIZE = 2000 * 1024 * 1024; // when the sender requests hangs for 60 second we will reimport const SENDER_TIMEOUT = 60 * 1000; @@ -83,7 +82,8 @@ class FileView { constructor(size?: number) { this.size = size; - this.type = (size && size > MAX_BUFFER_SAFE_SIZE) ? 'opfs' : 'memory'; + // eslint-disable-next-line no-restricted-globals + this.type = (size && size > (self as any).maxBufferSize) ? 'opfs' : 'memory'; } async init() { @@ -158,7 +158,7 @@ async function downloadFile2( end = end && end < fileSize ? end : fileSize - 1; if (!partSizeKb) { - partSizeKb = fileSize ? getAppropriatedPartSize(fileSize) : DEFAULT_CHUNK_SIZE; + partSizeKb = fileSize ? getDownloadPartSize(fileSize) : DEFAULT_CHUNK_SIZE; } const partSize = partSizeKb * 1024; diff --git a/src/lib/gramjs/client/uploadFile.ts b/src/lib/gramjs/client/uploadFile.ts index edc70a6f4..a673ef7c5 100644 --- a/src/lib/gramjs/client/uploadFile.ts +++ b/src/lib/gramjs/client/uploadFile.ts @@ -3,7 +3,7 @@ import { default as Api } from '../tl/api'; import type TelegramClient from './TelegramClient'; import { generateRandomBytes, readBigIntFromBuffer, sleep } from '../Helpers'; -import { getAppropriatedPartSize } from '../Utils'; +import { getUploadPartSize } from '../Utils'; import errors from '../errors'; interface OnProgress { @@ -34,7 +34,7 @@ export async function uploadFile( const fileId = readBigIntFromBuffer(generateRandomBytes(8), true, true); const isLarge = size > LARGE_FILE_THRESHOLD; - const partSize = getAppropriatedPartSize(size) * KB_TO_BYTES; + const partSize = getUploadPartSize(size) * KB_TO_BYTES; const partCount = Math.floor((size + partSize - 1) / partSize); // Make sure a new sender can be created before starting upload diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 2b910716b..a8d7e3b86 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -1,5 +1,6 @@ import { DEBUG } from './config'; import { respondForProgressive } from './serviceWorker/progressive'; +import { respondForDownload } from './serviceWorker/download'; import { respondWithCache, clearAssetCache } from './serviceWorker/assetCache'; import { handlePush, handleNotificationClick, handleClientMessage } from './serviceWorker/pushNotification'; import { pause } from './util/schedulers'; @@ -38,7 +39,6 @@ self.addEventListener('activate', (e) => { ); }); -// eslint-disable-next-line no-restricted-globals self.addEventListener('fetch', (e: FetchEvent) => { const { url } = e.request; @@ -47,6 +47,11 @@ self.addEventListener('fetch', (e: FetchEvent) => { return true; } + if (url.includes('/download/')) { + e.respondWith(respondForDownload(e)); + return true; + } + if (url.startsWith('http') && url.match(ASSET_CACHE_PATTERN)) { e.respondWith(respondWithCache(e)); return true; diff --git a/src/serviceWorker/download.ts b/src/serviceWorker/download.ts new file mode 100644 index 000000000..43728df5e --- /dev/null +++ b/src/serviceWorker/download.ts @@ -0,0 +1,99 @@ +import { DEBUG } from '../config'; +import { requestPart } from './progressive'; + +const DOWNLOAD_PART_SIZE = 1024 * 1024; +const TEST_PART_SIZE = 64 * 1024; + +const QUEUE_SIZE = 5; + +class FilePartQueue { + queue: Promise[]; + + constructor() { + this.queue = []; + } + + push(task: Promise) { + this.queue.push(task); + } + + async pop(): Promise { + const result = await this.queue.shift()!; + return result; + } + + get size() { + return this.queue.length; + } +} + +export async function respondForDownload(e: FetchEvent) { + const { url } = e.request; + + let partInfo; + try { + partInfo = await requestPart(e, { url, start: 0, end: TEST_PART_SIZE }); + } catch (err) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error('FETCH DOWNLOAD', err); + } + } + + if (!partInfo) { + return new Response('', { + status: 500, + statusText: 'Failed to fetch file to download', + }); + } + + const matchedFilename = e.request.url.match(/filename=(.*)/); + const filenameHeader = matchedFilename ? `filename="${decodeURIComponent(matchedFilename[1])}"` : ''; + const { fullSize, mimeType } = partInfo; + + const headers = [ + ['Content-Length', String(fullSize)], + ['Content-Type', mimeType], + ['Content-Disposition', `attachment; ${filenameHeader}`], + ]; + + const queue = new FilePartQueue(); + const enqueue = (offset: number) => { + queue.push(requestPart(e, { url, start: offset, end: offset + DOWNLOAD_PART_SIZE - 1 }) + .then((part) => part?.arrayBuffer)); + return offset + DOWNLOAD_PART_SIZE; + }; + let lastOffset = 0; + const stream = new ReadableStream({ + start() { + for (let i = 0; i < QUEUE_SIZE; i++) { + if (lastOffset >= fullSize) break; + lastOffset = enqueue(lastOffset); + } + }, + + async pull(controller) { + const buffer = await queue.pop(); + if (!buffer) { + controller.close(); + return; + } + controller.enqueue(new Uint8Array(buffer)); + + if (buffer.byteLength < DOWNLOAD_PART_SIZE) { + controller.close(); + return; + } + + if (lastOffset < fullSize) { + lastOffset = enqueue(lastOffset); + } + }, + }); + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }); +} diff --git a/src/serviceWorker/progressive.ts b/src/serviceWorker/progressive.ts index 4acf1eee8..fdd817230 100644 --- a/src/serviceWorker/progressive.ts +++ b/src/serviceWorker/progressive.ts @@ -137,16 +137,14 @@ async function saveToCache(cacheKey: string, arrayBuffer: ArrayBuffer, headers: ]); } -async function requestPart( +export async function requestPart( e: FetchEvent, params: { url: string; start: number; end: number }, ): Promise { - if (!e.clientId) { - return undefined; - } - - // eslint-disable-next-line no-restricted-globals - const client = await self.clients.get(e.clientId); + const isDownload = params.url.includes('/download/'); + const client = isDownload ? (await self.clients.matchAll()) + .find((c) => c.type === 'window' && c.frameType === 'top-level') + : await (self.clients.get(e.clientId)); if (!client) { return undefined; } diff --git a/src/util/environment.ts b/src/util/environment.ts index 508f5ce27..fa825e34c 100644 --- a/src/util/environment.ts +++ b/src/util/environment.ts @@ -114,6 +114,8 @@ export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window; // Smaller area reduces scroll jumps caused by `patchChromiumScroll` export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750; +export const MAX_BUFFER_SIZE = (IS_ANDROID || IS_IOS ? 512 : 2000) * 1024 ** 2; // 512 OR 2000 MB + function isLastEmojiVersionSupported() { const ALLOWABLE_CALCULATION_ERROR_SIZE = 5; const inlineEl = document.createElement('span'); diff --git a/src/util/mediaLoader.ts b/src/util/mediaLoader.ts index 65df847bc..82356c06d 100644 --- a/src/util/mediaLoader.ts +++ b/src/util/mediaLoader.ts @@ -13,18 +13,22 @@ import { import { callApi, cancelApiProgress } from '../api/gramjs'; import * as cacheApi from './cacheApi'; import { fetchBlob } from './files'; -import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported } from './environment'; +import { + IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported, +} from './environment'; import { oggToWav } from './oggToWav'; import { webpToPng } from './webpToPng'; const asCacheApiType = { [ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob, [ApiMediaFormat.Text]: cacheApi.Type.Text, + [ApiMediaFormat.DownloadUrl]: undefined, [ApiMediaFormat.Progressive]: undefined, [ApiMediaFormat.Stream]: undefined, }; const PROGRESSIVE_URL_PREFIX = './progressive/'; +const URL_DOWNLOAD_PREFIX = './download/'; const memoryCache = new Map(); const fetchPromises = new Map>(); @@ -46,6 +50,14 @@ export function fetch( ) as Promise; } + if (mediaFormat === ApiMediaFormat.DownloadUrl) { + return ( + IS_PROGRESSIVE_SUPPORTED + ? getDownloadUrl(url) + : fetch(url, ApiMediaFormat.BlobUrl, isHtmlAllowed, onProgress, callbackUniqueId) + ) as Promise; + } + if (!fetchPromises.has(url)) { const promise = fetchFromCacheOrRemote(url, mediaFormat, isHtmlAllowed) .catch((err) => { @@ -110,6 +122,10 @@ function getProgressive(url: string) { return Promise.resolve(progressiveUrl); } +function getDownloadUrl(url: string) { + return Promise.resolve(`${URL_DOWNLOAD_PREFIX}${url}`); +} + async function fetchFromCacheOrRemote( url: string, mediaFormat: ApiMediaFormat, isHtmlAllowed: boolean, ) {