|
|
@ -18,7 +18,6 @@ import {DcId} from '../../types'; |
|
|
|
import CacheStorageController from '../files/cacheStorage'; |
|
|
|
import CacheStorageController from '../files/cacheStorage'; |
|
|
|
import {logger, LogTypes} from '../logger'; |
|
|
|
import {logger, LogTypes} from '../logger'; |
|
|
|
import assumeType from '../../helpers/assumeType'; |
|
|
|
import assumeType from '../../helpers/assumeType'; |
|
|
|
import ctx from '../../environment/ctx'; |
|
|
|
|
|
|
|
import noop from '../../helpers/noop'; |
|
|
|
import noop from '../../helpers/noop'; |
|
|
|
import readBlobAsArrayBuffer from '../../helpers/blob/readBlobAsArrayBuffer'; |
|
|
|
import readBlobAsArrayBuffer from '../../helpers/blob/readBlobAsArrayBuffer'; |
|
|
|
import bytesToHex from '../../helpers/bytes/bytesToHex'; |
|
|
|
import bytesToHex from '../../helpers/bytes/bytesToHex'; |
|
|
@ -32,13 +31,15 @@ import type {Progress} from '../appManagers/appDownloadManager'; |
|
|
|
import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails'; |
|
|
|
import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails'; |
|
|
|
import networkStats from './networkStats'; |
|
|
|
import networkStats from './networkStats'; |
|
|
|
import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions'; |
|
|
|
import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions'; |
|
|
|
import {getServiceMessagePort} from './mtproto.worker'; |
|
|
|
|
|
|
|
import StreamWriter from '../files/streamWriter'; |
|
|
|
import StreamWriter from '../files/streamWriter'; |
|
|
|
import FileStorage from '../files/fileStorage'; |
|
|
|
import FileStorage from '../files/fileStorage'; |
|
|
|
import fileNameRFC from '../../helpers/string/fileNameRFC'; |
|
|
|
|
|
|
|
import {MAX_FILE_SAVE_SIZE} from './mtproto_config'; |
|
|
|
import {MAX_FILE_SAVE_SIZE} from './mtproto_config'; |
|
|
|
import throttle from '../../helpers/schedulers/throttle'; |
|
|
|
import throttle from '../../helpers/schedulers/throttle'; |
|
|
|
import makeError from '../../helpers/makeError'; |
|
|
|
import makeError from '../../helpers/makeError'; |
|
|
|
|
|
|
|
import readBlobAsUint8Array from '../../helpers/blob/readBlobAsUint8Array'; |
|
|
|
|
|
|
|
import DownloadStorage from '../files/downloadStorage'; |
|
|
|
|
|
|
|
import copy from '../../helpers/object/copy'; |
|
|
|
|
|
|
|
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; |
|
|
|
|
|
|
|
|
|
|
|
type Delayed = { |
|
|
|
type Delayed = { |
|
|
|
offset: number, |
|
|
|
offset: number, |
|
|
@ -82,10 +83,13 @@ export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile; |
|
|
|
// originalPayload: ReferenceBytes
|
|
|
|
// originalPayload: ReferenceBytes
|
|
|
|
// };
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_FILE_PART_SIZE = 1 * 1024 * 1024; |
|
|
|
const MAX_DOWNLOAD_FILE_PART_SIZE = 1 * 1024 * 1024; |
|
|
|
|
|
|
|
const MAX_UPLOAD_FILE_PART_SIZE = 512 * 1024; |
|
|
|
|
|
|
|
const MIN_PART_SIZE = 128 * 1024; |
|
|
|
|
|
|
|
const AVG_PART_SIZE = 512 * 1024; |
|
|
|
|
|
|
|
|
|
|
|
const REGULAR_DOWNLOAD_DELTA = 36; |
|
|
|
const REGULAR_DOWNLOAD_DELTA = (9 * 512 * 1024) / MIN_PART_SIZE; |
|
|
|
const PREMIUM_DOWNLOAD_DELTA = 72; |
|
|
|
const PREMIUM_DOWNLOAD_DELTA = REGULAR_DOWNLOAD_DELTA * 2; |
|
|
|
|
|
|
|
|
|
|
|
const IGNORE_ERRORS: Set<ErrorType> = new Set([ |
|
|
|
const IGNORE_ERRORS: Set<ErrorType> = new Set([ |
|
|
|
'DOWNLOAD_CANCELED', |
|
|
|
'DOWNLOAD_CANCELED', |
|
|
@ -96,11 +100,16 @@ const IGNORE_ERRORS: Set<ErrorType> = new Set([ |
|
|
|
|
|
|
|
|
|
|
|
export class ApiFileManager extends AppManager { |
|
|
|
export class ApiFileManager extends AppManager { |
|
|
|
private cacheStorage = new CacheStorageController('cachedFiles'); |
|
|
|
private cacheStorage = new CacheStorageController('cachedFiles'); |
|
|
|
|
|
|
|
private downloadStorage = new DownloadStorage(); |
|
|
|
|
|
|
|
|
|
|
|
private downloadPromises: { |
|
|
|
private downloadPromises: { |
|
|
|
[fileName: string]: DownloadPromise |
|
|
|
[fileName: string]: DownloadPromise |
|
|
|
} = {}; |
|
|
|
} = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// private downloadToDiscPromises: {
|
|
|
|
|
|
|
|
// [fileName: string]: DownloadPromise
|
|
|
|
|
|
|
|
// } = {};
|
|
|
|
|
|
|
|
|
|
|
|
private uploadPromises: { |
|
|
|
private uploadPromises: { |
|
|
|
[fileName: string]: CancellablePromise<InputFile> |
|
|
|
[fileName: string]: CancellablePromise<InputFile> |
|
|
|
} = {}; |
|
|
|
} = {}; |
|
|
@ -133,6 +142,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
|
|
|
|
|
|
|
|
private maxUploadParts = 4000; |
|
|
|
private maxUploadParts = 4000; |
|
|
|
private maxDownloadParts = 8000; |
|
|
|
private maxDownloadParts = 8000; |
|
|
|
|
|
|
|
private webFileDcId: DcId; |
|
|
|
|
|
|
|
|
|
|
|
protected after() { |
|
|
|
protected after() { |
|
|
|
setInterval(() => { // clear old promises
|
|
|
|
setInterval(() => { // clear old promises
|
|
|
@ -144,6 +154,10 @@ export class ApiFileManager extends AppManager { |
|
|
|
} |
|
|
|
} |
|
|
|
}, 1800e3); |
|
|
|
}, 1800e3); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.rootScope.addEventListener('config', (config) => { |
|
|
|
|
|
|
|
this.webFileDcId = config.webfile_dc_id; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.rootScope.addEventListener('app_config', (appConfig) => { |
|
|
|
this.rootScope.addEventListener('app_config', (appConfig) => { |
|
|
|
this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default; |
|
|
|
this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default; |
|
|
|
this.maxDownloadParts = appConfig.upload_max_fileparts_premium; |
|
|
|
this.maxDownloadParts = appConfig.upload_max_fileparts_premium; |
|
|
@ -173,7 +187,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
|
|
|
|
|
|
|
|
private downloadCheck(dcId: string | number) { |
|
|
|
private downloadCheck(dcId: string | number) { |
|
|
|
const downloadPull = this.downloadPulls[dcId]; |
|
|
|
const downloadPull = this.downloadPulls[dcId]; |
|
|
|
const downloadLimit = dcId === 'upload' ? 24 : (this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA); |
|
|
|
const downloadLimit = /* dcId === 'upload' ? 24 : */(this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA); |
|
|
|
// const downloadLimit = Infinity;
|
|
|
|
// const downloadLimit = Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) { |
|
|
|
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) { |
|
|
@ -187,7 +201,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
this.downloadActives[dcId] += activeDelta; |
|
|
|
this.downloadActives[dcId] += activeDelta; |
|
|
|
|
|
|
|
|
|
|
|
const promise = data.cb(); |
|
|
|
const promise = data.cb(); |
|
|
|
const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * 1024 * 128); |
|
|
|
const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * MIN_PART_SIZE); |
|
|
|
Promise.race([ |
|
|
|
Promise.race([ |
|
|
|
promise, |
|
|
|
promise, |
|
|
|
networkPromise |
|
|
|
networkPromise |
|
|
@ -235,7 +249,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
|
|
|
|
|
|
|
|
public requestWebFilePart(dcId: DcId, location: InputWebFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) { |
|
|
|
public requestWebFilePart(dcId: DcId, location: InputWebFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) { |
|
|
|
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
|
|
|
|
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
|
|
|
|
checkCancel && checkCancel(); |
|
|
|
checkCancel?.(); |
|
|
|
|
|
|
|
|
|
|
|
return this.apiManager.invokeApi('upload.getWebFile', { |
|
|
|
return this.apiManager.invokeApi('upload.getWebFile', { |
|
|
|
location, |
|
|
|
location, |
|
|
@ -248,17 +262,26 @@ export class ApiFileManager extends AppManager { |
|
|
|
}, this.getDelta(limit), queueId); |
|
|
|
}, this.getDelta(limit), queueId); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public requestFilePart(dcId: DcId, location: InputFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) { |
|
|
|
public requestFilePart( |
|
|
|
|
|
|
|
dcId: DcId, |
|
|
|
|
|
|
|
location: InputFileLocation, |
|
|
|
|
|
|
|
offset: number, |
|
|
|
|
|
|
|
limit: number, |
|
|
|
|
|
|
|
id = 0, |
|
|
|
|
|
|
|
queueId = 0, |
|
|
|
|
|
|
|
checkCancel?: () => void |
|
|
|
|
|
|
|
) { |
|
|
|
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
|
|
|
|
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
|
|
|
|
checkCancel && checkCancel(); |
|
|
|
checkCancel?.(); |
|
|
|
|
|
|
|
|
|
|
|
const invoke = async(): Promise<MyUploadFile> => { |
|
|
|
const invoke = async(): Promise<MyUploadFile> => { |
|
|
|
checkCancel && checkCancel(); // do not remove async, because checkCancel will throw an error
|
|
|
|
checkCancel?.(); // do not remove async, because checkCancel will throw an error
|
|
|
|
|
|
|
|
|
|
|
|
// * IMPORTANT: reference can be changed in previous request
|
|
|
|
// * IMPORTANT: reference can be changed in previous request
|
|
|
|
const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice(); |
|
|
|
const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice(); |
|
|
|
|
|
|
|
|
|
|
|
const promise = /* pause(1000).then(() => */this.apiManager.invokeApi('upload.getFile', { |
|
|
|
const promise = // pause(offset > (100 * 1024 * 1024) ? 10000000 : 0).then(() =>
|
|
|
|
|
|
|
|
this.apiManager.invokeApi('upload.getFile', { |
|
|
|
location, |
|
|
|
location, |
|
|
|
offset, |
|
|
|
offset, |
|
|
|
limit |
|
|
|
limit |
|
|
@ -268,6 +291,8 @@ export class ApiFileManager extends AppManager { |
|
|
|
}) as Promise<MyUploadFile>/* ) */; |
|
|
|
}) as Promise<MyUploadFile>/* ) */; |
|
|
|
|
|
|
|
|
|
|
|
return promise.catch((err: ApiError) => { |
|
|
|
return promise.catch((err: ApiError) => { |
|
|
|
|
|
|
|
checkCancel?.(); |
|
|
|
|
|
|
|
|
|
|
|
if(err.type === 'FILE_REFERENCE_EXPIRED') { |
|
|
|
if(err.type === 'FILE_REFERENCE_EXPIRED') { |
|
|
|
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke); |
|
|
|
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke); |
|
|
|
} |
|
|
|
} |
|
|
@ -295,19 +320,22 @@ export class ApiFileManager extends AppManager { |
|
|
|
} */ |
|
|
|
} */ |
|
|
|
|
|
|
|
|
|
|
|
private getDelta(bytes: number) { |
|
|
|
private getDelta(bytes: number) { |
|
|
|
return bytes / 1024 / 128; |
|
|
|
return bytes / MIN_PART_SIZE; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private getLimitPart(size: number, isUpload: boolean): number { |
|
|
|
private getLimitPart(size: number, isUpload: boolean): number { |
|
|
|
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
|
|
|
|
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
|
|
|
|
return 512 * 1024; |
|
|
|
return AVG_PART_SIZE; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let bytes = 128 * 1024; |
|
|
|
// return 1 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let bytes = MIN_PART_SIZE; |
|
|
|
|
|
|
|
|
|
|
|
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts; |
|
|
|
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts; |
|
|
|
|
|
|
|
const maxPartSize = isUpload ? MAX_UPLOAD_FILE_PART_SIZE : MAX_DOWNLOAD_FILE_PART_SIZE; |
|
|
|
// usually it will stick to 512Kb size if the file is too big
|
|
|
|
// usually it will stick to 512Kb size if the file is too big
|
|
|
|
while((size / bytes) > maxParts && bytes < MAX_FILE_PART_SIZE) { |
|
|
|
while((size / bytes) > maxParts && bytes < maxPartSize) { |
|
|
|
bytes *= 2; |
|
|
|
bytes *= 2; |
|
|
|
} |
|
|
|
} |
|
|
|
/* if(size < 1e6 || !size) bytes = 512; |
|
|
|
/* if(size < 1e6 || !size) bytes = 512; |
|
|
@ -399,201 +427,230 @@ export class ApiFileManager extends AppManager { |
|
|
|
return this.uploadPromises[fileName]; |
|
|
|
return this.uploadPromises[fileName]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public download(options: DownloadOptions): DownloadPromise { |
|
|
|
private getConvertMethod(mimeType: string) { |
|
|
|
const size = options.size ?? 0; |
|
|
|
|
|
|
|
const {dcId, location, downloadId} = options; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp']; |
|
|
|
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp']; |
|
|
|
|
|
|
|
if(mimeType === 'application/x-tgwallpattern') { |
|
|
|
if(downloadId) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else if(options.mimeType === 'application/x-tgwallpattern') { |
|
|
|
|
|
|
|
process = this.uncompressTGV; |
|
|
|
process = this.uncompressTGV; |
|
|
|
options.mimeType = 'image/svg+xml'; |
|
|
|
mimeType = 'image/svg+xml'; |
|
|
|
} else if(options.mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) { |
|
|
|
} else if(mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) { |
|
|
|
process = this.convertWebp; |
|
|
|
process = this.convertWebp; |
|
|
|
options.mimeType = 'image/png'; |
|
|
|
mimeType = 'image/png'; |
|
|
|
} else if(options.mimeType === 'application/x-tgsticker') { |
|
|
|
} else if(mimeType === 'application/x-tgsticker') { |
|
|
|
process = this.uncompressTGS; |
|
|
|
process = this.uncompressTGS; |
|
|
|
options.mimeType = 'application/json'; |
|
|
|
mimeType = 'application/json'; |
|
|
|
} else if(options.mimeType === 'audio/ogg' && !getEnvironment().IS_OPUS_SUPPORTED) { |
|
|
|
} else if(mimeType === 'audio/ogg' && !getEnvironment().IS_OPUS_SUPPORTED) { |
|
|
|
process = this.convertOpus; |
|
|
|
process = this.convertOpus; |
|
|
|
options.mimeType = 'audio/wav'; |
|
|
|
mimeType = 'audio/wav'; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const fileName = getDownloadFileNameFromOptions(options); |
|
|
|
return {mimeType, process}; |
|
|
|
const cachedPromise = options.downloadId ? undefined : this.downloadPromises[fileName]; |
|
|
|
} |
|
|
|
let fileStorage: FileStorage = this.getFileStorage(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.debug && this.log('downloadFile', fileName, size, location, options.mimeType); |
|
|
|
private allocateDeferredPromises(startOffset: number, size: number, limitPart: number) { |
|
|
|
|
|
|
|
const delayed: Delayed[] = []; |
|
|
|
|
|
|
|
let offset = startOffset; |
|
|
|
|
|
|
|
let writePromise: CancellablePromise<void> = Promise.resolve(), |
|
|
|
|
|
|
|
writeDeferred: CancellablePromise<void>; |
|
|
|
|
|
|
|
do { |
|
|
|
|
|
|
|
writeDeferred = deferredPromise<void>(); |
|
|
|
|
|
|
|
delayed.push({offset, writePromise, writeDeferred}); |
|
|
|
|
|
|
|
writePromise = writeDeferred; |
|
|
|
|
|
|
|
offset += limitPart; |
|
|
|
|
|
|
|
} while(offset < size); |
|
|
|
|
|
|
|
|
|
|
|
/* if(options.queueId) { |
|
|
|
return delayed; |
|
|
|
this.log.error('downloadFile queueId:', fileName, options.queueId); |
|
|
|
} |
|
|
|
} */ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(cachedPromise) { |
|
|
|
public download(options: DownloadOptions): DownloadPromise { |
|
|
|
// this.log('downloadFile cachedPromise');
|
|
|
|
const size = options.size ?? 0; |
|
|
|
|
|
|
|
const {dcId, location, downloadId} = options; |
|
|
|
|
|
|
|
|
|
|
|
if(size) { |
|
|
|
const originalMimeType = options.mimeType; |
|
|
|
return cachedPromise.then((blob) => { |
|
|
|
const convertMethod = this.getConvertMethod(originalMimeType); |
|
|
|
if(blob instanceof Blob && blob.size < size) { |
|
|
|
const {process} = convertMethod; |
|
|
|
this.debug && this.log('downloadFile need to deleteFile, wrong size:', blob.size, size); |
|
|
|
options.mimeType = convertMethod.mimeType || 'image/jpeg'; |
|
|
|
|
|
|
|
|
|
|
|
return this.delete(fileName).then(() => { |
|
|
|
const fileName = getDownloadFileNameFromOptions(options); |
|
|
|
return this.download(options); |
|
|
|
const cacheFileName = downloadId ? getDownloadFileNameFromOptions({...copy(options), downloadId: undefined}) : fileName; |
|
|
|
}).catch(() => { |
|
|
|
const cacheStorage: FileStorage = this.getFileStorage(); |
|
|
|
return this.download(options); |
|
|
|
const downloadStorage: FileStorage = downloadId ? this.downloadStorage : undefined; |
|
|
|
}); |
|
|
|
let deferred: DownloadPromise = downloadId ? undefined : this.downloadPromises[fileName]; |
|
|
|
} else { |
|
|
|
|
|
|
|
return blob; |
|
|
|
this.debug && this.log('downloadFile', fileName, options); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
if(deferred) { |
|
|
|
} else { |
|
|
|
return deferred; |
|
|
|
return cachedPromise; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const deferred: DownloadPromise = deferredPromise(); |
|
|
|
// if(deferred) {
|
|
|
|
const mimeType = options.mimeType || 'image/jpeg'; |
|
|
|
// if(size) {
|
|
|
|
|
|
|
|
// return deferred.then(async(blob) => {
|
|
|
|
|
|
|
|
// if(blob instanceof Blob && blob.size < size) {
|
|
|
|
|
|
|
|
// this.debug && this.log('downloadFile need to deleteFile, wrong size:', blob.size, size);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
|
|
|
// await this.delete(fileName);
|
|
|
|
|
|
|
|
// } finally {
|
|
|
|
|
|
|
|
// return this.download(options);
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
|
|
// return blob;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
|
|
// return deferred;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
let error: ApiError; |
|
|
|
const errorHandler = (item: typeof cachePrepared, error: ApiError) => { |
|
|
|
let resolved = false; |
|
|
|
if(item?.error) { |
|
|
|
let cacheWriter: StreamWriter; |
|
|
|
return; |
|
|
|
let errorHandler = (_error: typeof error) => { |
|
|
|
} |
|
|
|
error = _error; |
|
|
|
|
|
|
|
delete this.downloadPromises[fileName]; |
|
|
|
|
|
|
|
deferred.reject(error); |
|
|
|
|
|
|
|
errorHandler = () => {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(cacheWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) { |
|
|
|
for(const p of prepared) { |
|
|
|
cacheWriter.truncate?.(); |
|
|
|
if(item && item !== p) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p.error = error; |
|
|
|
|
|
|
|
p.deferred.reject(error); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const id = this.tempId++; |
|
|
|
const id = this.tempId++; |
|
|
|
|
|
|
|
const limitPart = options.limitPart || this.getLimitPart(size, false); |
|
|
|
|
|
|
|
|
|
|
|
if(downloadId) { |
|
|
|
let getFile: FileStorage['getFile'] = cacheStorage.getFile.bind(cacheStorage); |
|
|
|
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(); |
|
|
|
let cachePrepared: ReturnType<FileStorage['prepareWriting']> & {writer?: StreamWriter, error?: ApiError}, |
|
|
|
const promise = serviceMessagePort.invoke('download', { |
|
|
|
downloadPrepared: typeof cachePrepared; |
|
|
|
fileName, |
|
|
|
const prepared: (typeof cachePrepared)[] = []; |
|
|
|
headers, |
|
|
|
const possibleSize = size || limitPart; |
|
|
|
id: downloadId |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
promise.catch(errorHandler); |
|
|
|
const getErrorsCount = () => prepared.reduce((acc, item) => acc + +!!item.error, 0); |
|
|
|
deferred.catch(() => { |
|
|
|
|
|
|
|
getServiceMessagePort().invoke('downloadCancel', downloadId); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class f implements StreamWriter { |
|
|
|
const attach = (item: typeof cachePrepared, fileName: string) => { |
|
|
|
constructor() { |
|
|
|
const {deferred} = item; |
|
|
|
|
|
|
|
const _errorHandler = errorHandler.bind(null, item); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deferred.cancel = () => deferred.reject(makeError('DOWNLOAD_CANCELED')); |
|
|
|
|
|
|
|
deferred.catch((error) => { |
|
|
|
|
|
|
|
_errorHandler(error); |
|
|
|
|
|
|
|
item.writer?.truncate?.(); |
|
|
|
|
|
|
|
}).finally(() => { |
|
|
|
|
|
|
|
if(this.downloadPromises[fileName] === deferred) { |
|
|
|
|
|
|
|
delete this.downloadPromises[fileName]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public async write(part: Uint8Array, offset?: number) { |
|
|
|
delete item.writer; |
|
|
|
return serviceMessagePort.invoke('downloadChunk', { |
|
|
|
indexOfAndSplice(prepared, item); |
|
|
|
id: downloadId, |
|
|
|
|
|
|
|
chunk: part |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public finalize(saveToStorage?: boolean): Promise<Blob> { |
|
|
|
this.downloadPromises[fileName] = deferred; |
|
|
|
return serviceMessagePort.invoke('downloadFinalize', downloadId).then(() => null); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class d implements FileStorage { |
|
|
|
prepared.push(item); |
|
|
|
public getFile(fileName: string): Promise<any> { |
|
|
|
}; |
|
|
|
return Promise.reject(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter> { |
|
|
|
if(cacheStorage && (!downloadStorage || possibleSize <= MAX_FILE_SAVE_SIZE)) { |
|
|
|
return Promise.resolve(new f()); |
|
|
|
cachePrepared = cacheStorage.prepareWriting(cacheFileName, possibleSize, options.mimeType) |
|
|
|
|
|
|
|
attach(cachePrepared, cacheFileName); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(downloadStorage) { |
|
|
|
|
|
|
|
downloadPrepared = downloadStorage.prepareWriting({ |
|
|
|
|
|
|
|
fileName: options.fileName, // it's doc file_name
|
|
|
|
|
|
|
|
downloadId, |
|
|
|
|
|
|
|
size: possibleSize |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
attach(downloadPrepared, fileName); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(cachePrepared) { // cancel cache too
|
|
|
|
|
|
|
|
downloadPrepared.deferred.catch((err) => cachePrepared.deferred.reject(err)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fileStorage = new d(); |
|
|
|
// this.downloadToDiscPromises[cacheFileName] = deferred;
|
|
|
|
|
|
|
|
// deferred.catch(noop).finally(() => {
|
|
|
|
|
|
|
|
// if(this.downloadToDiscPromises[cacheFileName] === deferred) {
|
|
|
|
|
|
|
|
// delete this.downloadToDiscPromises[cacheFileName];
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// });
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fileStorage.getFile(fileName).then(async(blob: Blob) => { |
|
|
|
deferred = downloadPrepared?.deferred ?? cachePrepared.deferred; |
|
|
|
// throw '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(blob.size < size) { |
|
|
|
if(downloadStorage && process) { // then have to load file again
|
|
|
|
if(!options.onlyCache) { |
|
|
|
getFile = downloadStorage.getFile.bind(downloadStorage); |
|
|
|
await this.delete(fileName); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
throw false; |
|
|
|
getFile(cacheFileName).then(async(blob: Blob) => { |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// if(blob.size < size) {
|
|
|
|
|
|
|
|
// if(!options.onlyCache) {
|
|
|
|
|
|
|
|
// await this.delete(cacheFileName);
|
|
|
|
|
|
|
|
// checkCancel();
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// throw makeError('NO_ENTRY_FOUND');
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(downloadPrepared) { |
|
|
|
|
|
|
|
const writer = downloadPrepared.writer = downloadPrepared.getWriter(); |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const arr = await readBlobAsUint8Array(blob); |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
await writer.write(arr); |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
downloadPrepared.deferred.resolve(await writer.finalize()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
deferred.resolve(blob); |
|
|
|
if(cachePrepared) { |
|
|
|
|
|
|
|
cachePrepared.deferred.resolve(blob); |
|
|
|
|
|
|
|
} |
|
|
|
}).catch(async(err: ApiError) => { |
|
|
|
}).catch(async(err: ApiError) => { |
|
|
|
if(options.onlyCache) { |
|
|
|
if(options.onlyCache) { |
|
|
|
errorHandler(err); |
|
|
|
errorHandler(null, err); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// this.log('not cached', fileName);
|
|
|
|
prepared.forEach((p) => { |
|
|
|
const limit = options.limitPart || this.getLimitPart(size, false); |
|
|
|
p.writer = p.getWriter(); |
|
|
|
const writerPromise = fileStorage.getWriter(fileName, size || limit, mimeType); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const writer = cacheWriter = await writerPromise; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let offset: number; |
|
|
|
|
|
|
|
const startOffset = 0; |
|
|
|
|
|
|
|
let writePromise: CancellablePromise<void> = Promise.resolve(), |
|
|
|
|
|
|
|
writeDeferred: CancellablePromise<void>; |
|
|
|
|
|
|
|
// const maxRequests = 13107200 / limit; // * 100 Mb speed
|
|
|
|
|
|
|
|
const maxRequests = Infinity; |
|
|
|
const maxRequests = Infinity; |
|
|
|
|
|
|
|
|
|
|
|
const processDownloaded = async(bytes: Uint8Array) => { |
|
|
|
const isWebFile = location._ === 'inputWebFileLocation'; |
|
|
|
if(process) { |
|
|
|
const requestPart = (isWebFile ? this.requestWebFilePart : this.requestFilePart).bind(this); |
|
|
|
// 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; |
|
|
|
if(isWebFile && this.webFileDcId === undefined) { |
|
|
|
}; |
|
|
|
await this.apiManager.getConfig(); |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this); |
|
|
|
const delayed = this.allocateDeferredPromises(0, size, limitPart); |
|
|
|
|
|
|
|
|
|
|
|
const delayed: Delayed[] = []; |
|
|
|
const progress: Progress = {done: 0, offset: 0, total: size, fileName}; |
|
|
|
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 = () => { |
|
|
|
const dispatchProgress = () => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
progress.done = done; |
|
|
|
progress.done = done; |
|
|
|
deferred.notify?.(progress); |
|
|
|
this.rootScope.dispatchEvent('download_progress', progress); |
|
|
|
|
|
|
|
} catch(err) {} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const throttledDispatchProgress = throttle(dispatchProgress, 50, true); |
|
|
|
const throttledDispatchProgress = throttle(dispatchProgress, 50, true); |
|
|
|
|
|
|
|
|
|
|
|
let done = 0; |
|
|
|
let done = 0; |
|
|
|
const superpuper = async() => { |
|
|
|
const superpuper = async() => { |
|
|
|
// if(!delayed.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const {offset, writePromise, writeDeferred} = delayed.shift(); |
|
|
|
const {offset, writePromise, writeDeferred} = delayed.shift(); |
|
|
|
try { |
|
|
|
try { |
|
|
|
checkCancel(); |
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
const requestPerf = performance.now(); |
|
|
|
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel); |
|
|
|
const result = await requestPart(dcId, location as any, offset, limitPart, id, options.queueId, checkCancel); |
|
|
|
|
|
|
|
const requestTime = performance.now() - requestPerf; |
|
|
|
|
|
|
|
|
|
|
|
const bytes = result.bytes; |
|
|
|
const bytes = result.bytes; |
|
|
|
|
|
|
|
|
|
|
@ -603,7 +660,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
|
|
|
|
|
|
|
|
const byteLength = bytes.byteLength; |
|
|
|
const byteLength = bytes.byteLength; |
|
|
|
this.debug && this.log('downloadFile requestFilePart result:', fileName, result); |
|
|
|
this.debug && this.log('downloadFile requestFilePart result:', fileName, result); |
|
|
|
const isFinal = (offset + limit) >= size || !byteLength; |
|
|
|
const isFinal = (offset + limitPart) >= size || !byteLength; |
|
|
|
if(byteLength) { |
|
|
|
if(byteLength) { |
|
|
|
done += byteLength; |
|
|
|
done += byteLength; |
|
|
|
|
|
|
|
|
|
|
@ -613,68 +670,65 @@ export class ApiFileManager extends AppManager { |
|
|
|
throttledDispatchProgress(); |
|
|
|
throttledDispatchProgress(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const writeQueuePerf = performance.now(); |
|
|
|
await writePromise; |
|
|
|
await writePromise; |
|
|
|
checkCancel(); |
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
const writeQueueTime = performance.now() - writeQueuePerf; |
|
|
|
|
|
|
|
|
|
|
|
// const perf = performance.now();
|
|
|
|
const perf = performance.now(); |
|
|
|
await writer.write(bytes, offset); |
|
|
|
await Promise.all(prepared.map(({writer}) => writer?.write(bytes, offset))); |
|
|
|
checkCancel(); |
|
|
|
checkCancel(); |
|
|
|
// downloadId && this.log('write time', performance.now() - perf);
|
|
|
|
// downloadId && this.log('write time', performance.now() - perf, 'request time', requestTime, 'queue time', writeQueueTime);
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if(isFinal && process) { |
|
|
|
if(isFinal && process) { |
|
|
|
|
|
|
|
const promises = prepared |
|
|
|
|
|
|
|
.filter(({writer}) => writer?.getParts && writer.replaceParts) |
|
|
|
|
|
|
|
.map(async({writer}) => { |
|
|
|
const bytes = writer.getParts(); |
|
|
|
const bytes = writer.getParts(); |
|
|
|
const processedResult = await processDownloaded(bytes); |
|
|
|
const processedResult = await process(bytes, cacheFileName); |
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
writer.replaceParts(processedResult); |
|
|
|
writer.replaceParts(processedResult); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all(promises); |
|
|
|
|
|
|
|
checkCancel(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
writeDeferred.resolve(); |
|
|
|
writeDeferred.resolve(); |
|
|
|
|
|
|
|
|
|
|
|
if(isFinal) { |
|
|
|
if(isFinal) { |
|
|
|
resolved = true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const realSize = size || byteLength; |
|
|
|
const realSize = size || byteLength; |
|
|
|
if(!size) { |
|
|
|
if(!size || byteLength < size) { |
|
|
|
writer.trim(realSize); |
|
|
|
prepared.forEach(({writer}) => writer?.trim?.(realSize)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const saveToStorage = realSize <= MAX_FILE_SAVE_SIZE; |
|
|
|
|
|
|
|
prepared.forEach((item) => { |
|
|
|
|
|
|
|
const {deferred, writer} = item; |
|
|
|
|
|
|
|
if(deferred.isFulfilled || deferred.isRejected || !writer) { |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
deferred.resolve(await writer.finalize(realSize <= MAX_FILE_SAVE_SIZE)); |
|
|
|
const result = writer.finalize(saveToStorage); |
|
|
|
|
|
|
|
deferred.resolve(result); |
|
|
|
|
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch(err) { |
|
|
|
} catch(err) { |
|
|
|
errorHandler(err as ApiError); |
|
|
|
errorHandler(null, err as ApiError); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) { |
|
|
|
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) { |
|
|
|
superpuper(); |
|
|
|
superpuper(); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}).catch(noop); |
|
|
|
|
|
|
|
|
|
|
|
const checkCancel = () => { |
|
|
|
const checkCancel = () => { |
|
|
|
if(error) { |
|
|
|
if(getErrorsCount() === prepared.length) { |
|
|
|
throw error; |
|
|
|
throw prepared[0].error; |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
deferred.cancel = () => { |
|
|
|
|
|
|
|
if(!error && !resolved) { |
|
|
|
|
|
|
|
const error = makeError('DOWNLOAD_CANCELED'); |
|
|
|
|
|
|
|
errorHandler(error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deferred.notify = (progress: Progress) => { |
|
|
|
|
|
|
|
this.rootScope.dispatchEvent('download_progress', progress); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.downloadPromises[fileName] = deferred; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deferred.catch(noop).finally(() => { |
|
|
|
|
|
|
|
delete this.downloadPromises[fileName]; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return deferred; |
|
|
|
return deferred; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -736,23 +790,14 @@ export class ApiFileManager extends AppManager { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public upload({file, fileName}: {file: Blob | File, fileName?: string}) { |
|
|
|
public upload({file, fileName}: {file: Blob | File, fileName?: string}) { |
|
|
|
const fileSize = file.size, |
|
|
|
|
|
|
|
isBigFile = fileSize >= 10485760; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let canceled = false, |
|
|
|
|
|
|
|
resolved = false, |
|
|
|
|
|
|
|
doneParts = 0; |
|
|
|
|
|
|
|
const partSize = this.getLimitPart(fileSize, true); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileName ||= getFileNameForUpload(file); |
|
|
|
fileName ||= getFileNameForUpload(file); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fileSize = file.size; |
|
|
|
|
|
|
|
const isBigFile = fileSize >= 10485760; |
|
|
|
|
|
|
|
const partSize = this.getLimitPart(fileSize, true); |
|
|
|
const activeDelta = this.getDelta(partSize); |
|
|
|
const activeDelta = this.getDelta(partSize); |
|
|
|
|
|
|
|
|
|
|
|
const totalParts = Math.ceil(fileSize / partSize); |
|
|
|
const totalParts = Math.ceil(fileSize / partSize); |
|
|
|
const fileId = randomLong(); |
|
|
|
const fileId = randomLong(); |
|
|
|
|
|
|
|
|
|
|
|
let _part = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resultInputFile: InputFile = { |
|
|
|
const resultInputFile: InputFile = { |
|
|
|
_: isBigFile ? 'inputFileBig' : 'inputFile', |
|
|
|
_: isBigFile ? 'inputFileBig' : 'inputFile', |
|
|
|
id: fileId as any, |
|
|
|
id: fileId as any, |
|
|
@ -767,6 +812,7 @@ export class ApiFileManager extends AppManager { |
|
|
|
return deferred; |
|
|
|
return deferred; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let canceled = false, resolved = false; |
|
|
|
let errorHandler = (error: ApiError) => { |
|
|
|
let errorHandler = (error: ApiError) => { |
|
|
|
if(error?.type !== 'UPLOAD_CANCELED') { |
|
|
|
if(error?.type !== 'UPLOAD_CANCELED') { |
|
|
|
this.log.error('Up Error', error); |
|
|
|
this.log.error('Up Error', error); |
|
|
@ -774,58 +820,32 @@ export class ApiFileManager extends AppManager { |
|
|
|
|
|
|
|
|
|
|
|
deferred.reject(error); |
|
|
|
deferred.reject(error); |
|
|
|
canceled = true; |
|
|
|
canceled = true; |
|
|
|
errorHandler = () => {}; |
|
|
|
errorHandler = noop; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart'; |
|
|
|
const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart'; |
|
|
|
|
|
|
|
|
|
|
|
const id = this.tempId++; |
|
|
|
const id = this.tempId++; |
|
|
|
|
|
|
|
|
|
|
|
const self = this; |
|
|
|
const self = this; |
|
|
|
function* generator() { |
|
|
|
function* generator() { |
|
|
|
|
|
|
|
let _part = 0, doneParts = 0; |
|
|
|
for(let offset = 0; offset < fileSize; offset += partSize) { |
|
|
|
for(let offset = 0; offset < fileSize; offset += partSize) { |
|
|
|
const part = _part++; // 0, 1
|
|
|
|
const part = _part++; // 0, 1
|
|
|
|
yield self.downloadRequest('upload', id, () => { |
|
|
|
yield self.downloadRequest('upload', id, async() => { |
|
|
|
const blob = file.slice(offset, offset + partSize); |
|
|
|
checkCancel(); |
|
|
|
|
|
|
|
|
|
|
|
return readBlobAsArrayBuffer(blob).then((buffer) => { |
|
|
|
const blob = file.slice(offset, offset + partSize); |
|
|
|
if(canceled) { |
|
|
|
const buffer = await readBlobAsArrayBuffer(blob); |
|
|
|
throw makeError('UPLOAD_CANCELED'); |
|
|
|
checkCancel(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length); |
|
|
|
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length); |
|
|
|
|
|
|
|
|
|
|
|
/* const u = new Uint8Array(buffer.byteLength); |
|
|
|
|
|
|
|
for(let i = 0; i < u.length; ++i) { |
|
|
|
|
|
|
|
//u[i] = Math.random() * 255 | 0;
|
|
|
|
|
|
|
|
u[i] = 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
buffer = u.buffer; */ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* setTimeout(() => { |
|
|
|
|
|
|
|
doneParts++; |
|
|
|
|
|
|
|
uploadResolve(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//////this.log('Progress', doneParts * partSize / fileSize);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.log('done part', part, doneParts); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deferred.notify({done: doneParts * partSize, total: fileSize}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(doneParts >= totalParts) { |
|
|
|
|
|
|
|
deferred.resolve(resultInputFile); |
|
|
|
|
|
|
|
resolved = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, 1250); |
|
|
|
|
|
|
|
return; */ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return self.apiManager.invokeApi(method, { |
|
|
|
return self.apiManager.invokeApi(method, { |
|
|
|
file_id: fileId, |
|
|
|
file_id: fileId, |
|
|
|
file_part: part, |
|
|
|
file_part: part, |
|
|
|
file_total_parts: totalParts, |
|
|
|
file_total_parts: totalParts, |
|
|
|
bytes: buffer/* new Uint8Array(buffer) */ |
|
|
|
bytes: buffer |
|
|
|
} as any, { |
|
|
|
} as any, { |
|
|
|
// startMaxLength: partSize + 256,
|
|
|
|
|
|
|
|
fileUpload: true |
|
|
|
fileUpload: true |
|
|
|
}).then(() => { |
|
|
|
}).then(() => { |
|
|
|
if(canceled) { |
|
|
|
if(canceled) { |
|
|
@ -841,7 +861,6 @@ export class ApiFileManager extends AppManager { |
|
|
|
resolved = true; |
|
|
|
resolved = true; |
|
|
|
} |
|
|
|
} |
|
|
|
}, errorHandler); |
|
|
|
}, errorHandler); |
|
|
|
}); |
|
|
|
|
|
|
|
}, activeDelta).catch(errorHandler); |
|
|
|
}, activeDelta).catch(errorHandler); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -855,14 +874,16 @@ export class ApiFileManager extends AppManager { |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const maxRequests = Infinity; |
|
|
|
const maxRequests = Infinity; |
|
|
|
// const maxRequests = 10;
|
|
|
|
|
|
|
|
/* for(let i = 0; i < 10; ++i) { |
|
|
|
|
|
|
|
process(); |
|
|
|
|
|
|
|
} */ |
|
|
|
|
|
|
|
for(let i = 0, length = Math.min(maxRequests, totalParts); i < length; ++i) { |
|
|
|
for(let i = 0, length = Math.min(maxRequests, totalParts); i < length; ++i) { |
|
|
|
process(); |
|
|
|
process(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const checkCancel = () => { |
|
|
|
|
|
|
|
if(canceled) { |
|
|
|
|
|
|
|
throw makeError('UPLOAD_CANCELED'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
deferred.cancel = () => { |
|
|
|
deferred.cancel = () => { |
|
|
|
if(!canceled && !resolved) { |
|
|
|
if(!canceled && !resolved) { |
|
|
|
canceled = true; |
|
|
|
canceled = true; |
|
|
@ -875,7 +896,9 @@ export class ApiFileManager extends AppManager { |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
deferred.finally(() => { |
|
|
|
deferred.finally(() => { |
|
|
|
|
|
|
|
if(this.uploadPromises[fileName] === deferred) { |
|
|
|
delete this.uploadPromises[fileName]; |
|
|
|
delete this.uploadPromises[fileName]; |
|
|
|
|
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return this.uploadPromises[fileName] = deferred; |
|
|
|
return this.uploadPromises[fileName] = deferred; |
|
|
|