|
@@ -1,11 +1,7 @@
|
|
|
-import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
|
|
-import { getFileURL, getThumbnailURL } from '@ente/shared/network/api';
|
|
|
import {
|
|
|
generateStreamFromArrayBuffer,
|
|
|
getRenderableFileURL,
|
|
|
- createTypedObjectURL,
|
|
|
} from 'utils/file';
|
|
|
-import HTTPService from '@ente/shared/network/HTTPService';
|
|
|
import { EnteFile } from 'types/file';
|
|
|
|
|
|
import { logError } from '@ente/shared/sentry';
|
|
@@ -17,173 +13,282 @@ import { CACHES } from '@ente/shared/storage/cacheStorage/constants';
|
|
|
import { Remote } from 'comlink';
|
|
|
import { DedicatedCryptoWorker } from '@ente/shared/crypto/internal/crypto.worker';
|
|
|
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
|
|
|
-import { retryAsyncFunction } from 'utils/network';
|
|
|
import { addLogLine } from '@ente/shared/logging';
|
|
|
+import { APPS } from '@ente/shared/apps/constants';
|
|
|
+import { PhotosDownloadClient } from './clients/photos';
|
|
|
+import { PublicAlbumsDownloadClient } from './clients/publicAlbums';
|
|
|
+import isElectron from 'is-electron';
|
|
|
+import { isInternalUser } from 'utils/user';
|
|
|
|
|
|
-class DownloadManager {
|
|
|
- private fileObjectURLPromise = new Map<
|
|
|
- string,
|
|
|
- Promise<{ original: string[]; converted: string[] }>
|
|
|
- >();
|
|
|
- private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
|
|
+export type LivePhotoSourceURL = {
|
|
|
+ image: () => Promise<string>;
|
|
|
+ video: () => Promise<string>;
|
|
|
+};
|
|
|
+
|
|
|
+export type LoadedLivePhotoSourceURL = {
|
|
|
+ image: string;
|
|
|
+ video: string;
|
|
|
+};
|
|
|
+
|
|
|
+export type SourceURLs = {
|
|
|
+ url: string | LivePhotoSourceURL | LoadedLivePhotoSourceURL;
|
|
|
+ isOriginal: boolean;
|
|
|
+ isRenderable: boolean;
|
|
|
+ type: 'normal' | 'livePhoto';
|
|
|
+};
|
|
|
+
|
|
|
+export type OnDownloadProgress = (event: {
|
|
|
+ loaded: number;
|
|
|
+ total: number;
|
|
|
+}) => void;
|
|
|
+
|
|
|
+export interface DownloadClient {
|
|
|
+ updateTokens: (token: string, passwordToken?: string) => void;
|
|
|
+ updateTimeout: (timeout: number) => void;
|
|
|
+ downloadThumbnail: (
|
|
|
+ file: EnteFile,
|
|
|
+ timeout?: number
|
|
|
+ ) => Promise<Uint8Array>;
|
|
|
+ downloadFile: (
|
|
|
+ file: EnteFile,
|
|
|
+ onDownloadProgress: OnDownloadProgress
|
|
|
+ ) => Promise<Uint8Array>;
|
|
|
+ downloadFileStream: (file: EnteFile) => Promise<Response>;
|
|
|
+}
|
|
|
+
|
|
|
+const FILE_CACHE_LIMIT = 5 * 1024 * 1024 * 1024; // 5GB
|
|
|
+
|
|
|
+class DownloadManagerImpl {
|
|
|
+ private ready: boolean = false;
|
|
|
+ private downloadClient: DownloadClient;
|
|
|
+ private thumbnailCache?: LimitedCache;
|
|
|
+ // disk cache is only available on electron
|
|
|
+ private diskFileCache?: LimitedCache;
|
|
|
+ private cryptoWorker: Remote<DedicatedCryptoWorker>;
|
|
|
+
|
|
|
+ private fileObjectURLPromises = new Map<number, Promise<SourceURLs>>();
|
|
|
+ private fileConversionPromises = new Map<number, Promise<SourceURLs>>();
|
|
|
+ private thumbnailObjectURLPromises = new Map<number, Promise<string>>();
|
|
|
|
|
|
private fileDownloadProgress = new Map<number, number>();
|
|
|
|
|
|
private progressUpdater: (value: Map<number, number>) => void = () => {};
|
|
|
|
|
|
- private thumbnailCache: LimitedCache;
|
|
|
+ async init(
|
|
|
+ app: APPS,
|
|
|
+ tokens?: { token: string; passwordToken?: string } | { token: string },
|
|
|
+ timeout?: number
|
|
|
+ ) {
|
|
|
+ try {
|
|
|
+ if (this.ready) {
|
|
|
+ addLogLine('DownloadManager already initialized');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.downloadClient = createDownloadClient(app, tokens, timeout);
|
|
|
+ this.thumbnailCache = await openThumbnailCache();
|
|
|
+ this.diskFileCache = isElectron() && (await openDiskFileCache());
|
|
|
+ this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
|
|
+ this.ready = true;
|
|
|
+ } catch (e) {
|
|
|
+ logError(e, 'DownloadManager init failed');
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updateToken(token: string, passwordToken?: string) {
|
|
|
+ this.downloadClient.updateTokens(token, passwordToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ updateCryptoWorker(cryptoWorker: Remote<DedicatedCryptoWorker>) {
|
|
|
+ this.cryptoWorker = cryptoWorker;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateTimeout(timeout: number) {
|
|
|
+ this.downloadClient.updateTimeout(timeout);
|
|
|
+ }
|
|
|
|
|
|
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
|
|
|
this.progressUpdater = progressUpdater;
|
|
|
}
|
|
|
|
|
|
- private async getThumbnailCache() {
|
|
|
- try {
|
|
|
- if (!this.thumbnailCache) {
|
|
|
- this.thumbnailCache = await CacheStorageService.open(
|
|
|
- CACHES.THUMBS
|
|
|
- );
|
|
|
- }
|
|
|
- return this.thumbnailCache;
|
|
|
- } catch (e) {
|
|
|
- return null;
|
|
|
- // ignore
|
|
|
- }
|
|
|
+ async reloadCaches() {
|
|
|
+ this.thumbnailCache = await openThumbnailCache();
|
|
|
+ this.diskFileCache = isElectron() && (await openDiskFileCache());
|
|
|
}
|
|
|
|
|
|
- public async getCachedThumbnail(file: EnteFile) {
|
|
|
+ private async getCachedThumbnail(fileID: number) {
|
|
|
try {
|
|
|
- const thumbnailCache = await this.getThumbnailCache();
|
|
|
- const cacheResp: Response = await thumbnailCache?.match(
|
|
|
- file.id.toString()
|
|
|
+ const cacheResp: Response = await this.thumbnailCache?.match(
|
|
|
+ fileID.toString()
|
|
|
);
|
|
|
|
|
|
if (cacheResp) {
|
|
|
- return URL.createObjectURL(await cacheResp.blob());
|
|
|
+ return new Uint8Array(await cacheResp.arrayBuffer());
|
|
|
}
|
|
|
- return null;
|
|
|
} catch (e) {
|
|
|
logError(e, 'failed to get cached thumbnail');
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- public async getThumbnail(
|
|
|
- file: EnteFile,
|
|
|
- tokenOverride?: string,
|
|
|
- usingWorker?: Remote<DedicatedCryptoWorker>,
|
|
|
- timeout?: number
|
|
|
- ) {
|
|
|
+ private async getCachedFile(file: EnteFile): Promise<Response> {
|
|
|
try {
|
|
|
- const token = tokenOverride || getToken();
|
|
|
- if (!token) {
|
|
|
+ if (!this.diskFileCache) {
|
|
|
return null;
|
|
|
}
|
|
|
- if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
|
|
- const downloadPromise = async () => {
|
|
|
- const thumbnailCache = await this.getThumbnailCache();
|
|
|
- const cachedThumb = await this.getCachedThumbnail(file);
|
|
|
- if (cachedThumb) {
|
|
|
- return cachedThumb;
|
|
|
- }
|
|
|
- const thumb = await this.downloadThumb(
|
|
|
- token,
|
|
|
- file,
|
|
|
- usingWorker,
|
|
|
- timeout
|
|
|
- );
|
|
|
- const thumbBlob = new Blob([thumb]);
|
|
|
+ const cacheResp: Response = await this.diskFileCache?.match(
|
|
|
+ file.id.toString(),
|
|
|
+ { sizeInBytes: file.info?.fileSize }
|
|
|
+ );
|
|
|
+ return cacheResp?.clone();
|
|
|
+ } catch (e) {
|
|
|
+ logError(e, 'failed to get cached file');
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- thumbnailCache
|
|
|
- ?.put(file.id.toString(), new Response(thumbBlob))
|
|
|
- .catch((e) => {
|
|
|
- logError(e, 'cache put failed');
|
|
|
- // TODO: handle storage full exception.
|
|
|
- });
|
|
|
- return URL.createObjectURL(thumbBlob);
|
|
|
- };
|
|
|
- this.thumbnailObjectURLPromise.set(file.id, downloadPromise());
|
|
|
+ private downloadThumb = async (file: EnteFile) => {
|
|
|
+ const encrypted = await this.downloadClient.downloadThumbnail(file);
|
|
|
+ const decrypted = await this.cryptoWorker.decryptThumbnail(
|
|
|
+ encrypted,
|
|
|
+ await this.cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
|
|
+ file.key
|
|
|
+ );
|
|
|
+ return decrypted;
|
|
|
+ };
|
|
|
+
|
|
|
+ async getThumbnail(file: EnteFile, localOnly = false) {
|
|
|
+ try {
|
|
|
+ if (!this.ready) {
|
|
|
+ throw Error(CustomError.DOWNLOAD_MANAGER_NOT_READY);
|
|
|
+ }
|
|
|
+ const cachedThumb = await this.getCachedThumbnail(file.id);
|
|
|
+ if (cachedThumb) {
|
|
|
+ return cachedThumb;
|
|
|
+ }
|
|
|
+ if (localOnly) {
|
|
|
+ return null;
|
|
|
}
|
|
|
+ const thumb = await this.downloadThumb(file);
|
|
|
+
|
|
|
+ this.thumbnailCache
|
|
|
+ ?.put(file.id.toString(), new Response(thumb))
|
|
|
+ .catch((e) => {
|
|
|
+ logError(e, 'thumb cache put failed');
|
|
|
+ // TODO: handle storage full exception.
|
|
|
+ });
|
|
|
+ return thumb;
|
|
|
+ } catch (e) {
|
|
|
+ logError(e, 'getThumbnail failed');
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- return await this.thumbnailObjectURLPromise.get(file.id);
|
|
|
+ async getThumbnailForPreview(file: EnteFile, localOnly = false) {
|
|
|
+ try {
|
|
|
+ if (!this.ready) {
|
|
|
+ throw Error(CustomError.DOWNLOAD_MANAGER_NOT_READY);
|
|
|
+ }
|
|
|
+ if (!this.thumbnailObjectURLPromises.has(file.id)) {
|
|
|
+ const thumbPromise = this.getThumbnail(file, localOnly);
|
|
|
+ const thumbURLPromise = thumbPromise.then(
|
|
|
+ (thumb) => thumb && URL.createObjectURL(new Blob([thumb]))
|
|
|
+ );
|
|
|
+ this.thumbnailObjectURLPromises.set(file.id, thumbURLPromise);
|
|
|
+ }
|
|
|
+ let thumb = await this.thumbnailObjectURLPromises.get(file.id);
|
|
|
+ if (!thumb && !localOnly) {
|
|
|
+ this.thumbnailObjectURLPromises.delete(file.id);
|
|
|
+ thumb = await this.getThumbnailForPreview(file, localOnly);
|
|
|
+ }
|
|
|
+ return thumb;
|
|
|
} catch (e) {
|
|
|
- this.thumbnailObjectURLPromise.delete(file.id);
|
|
|
+ this.thumbnailObjectURLPromises.delete(file.id);
|
|
|
logError(e, 'get DownloadManager preview Failed');
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- downloadThumb = async (
|
|
|
- token: string,
|
|
|
+ getFileForPreview = async (
|
|
|
file: EnteFile,
|
|
|
- usingWorker?: Remote<DedicatedCryptoWorker>,
|
|
|
- timeout?: number
|
|
|
- ) => {
|
|
|
- const resp = await HTTPService.get(
|
|
|
- getThumbnailURL(file.id),
|
|
|
- null,
|
|
|
- { 'X-Auth-Token': token },
|
|
|
- { responseType: 'arraybuffer', timeout }
|
|
|
- );
|
|
|
- if (typeof resp.data === 'undefined') {
|
|
|
- throw Error(CustomError.REQUEST_FAILED);
|
|
|
+ forceConvert = false
|
|
|
+ ): Promise<SourceURLs> => {
|
|
|
+ try {
|
|
|
+ if (!this.ready) {
|
|
|
+ throw Error(CustomError.DOWNLOAD_MANAGER_NOT_READY);
|
|
|
+ }
|
|
|
+ const getFileForPreviewPromise = async () => {
|
|
|
+ const fileBlob = await new Response(
|
|
|
+ await this.getFile(file, true)
|
|
|
+ ).blob();
|
|
|
+ const { url: originalFileURL } =
|
|
|
+ await this.fileObjectURLPromises.get(file.id);
|
|
|
+
|
|
|
+ const converted = await getRenderableFileURL(
|
|
|
+ file,
|
|
|
+ fileBlob,
|
|
|
+ originalFileURL as string,
|
|
|
+ forceConvert
|
|
|
+ );
|
|
|
+ return converted;
|
|
|
+ };
|
|
|
+ if (forceConvert || !this.fileConversionPromises.has(file.id)) {
|
|
|
+ this.fileConversionPromises.set(
|
|
|
+ file.id,
|
|
|
+ getFileForPreviewPromise()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ const fileURLs = await this.fileConversionPromises.get(file.id);
|
|
|
+ return fileURLs;
|
|
|
+ } catch (e) {
|
|
|
+ this.fileConversionPromises.delete(file.id);
|
|
|
+ logError(e, 'download manager getFileForPreview Failed');
|
|
|
+ throw e;
|
|
|
}
|
|
|
- const cryptoWorker =
|
|
|
- usingWorker || (await ComlinkCryptoWorker.getInstance());
|
|
|
- const decrypted = await cryptoWorker.decryptThumbnail(
|
|
|
- new Uint8Array(resp.data),
|
|
|
- await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
|
|
- file.key
|
|
|
- );
|
|
|
- return decrypted;
|
|
|
};
|
|
|
|
|
|
- getFile = async (file: EnteFile, forPreview = false) => {
|
|
|
- const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
|
|
+ async getFile(
|
|
|
+ file: EnteFile,
|
|
|
+ cacheInMemory = false
|
|
|
+ ): Promise<ReadableStream<Uint8Array>> {
|
|
|
try {
|
|
|
- const getFilePromise = async () => {
|
|
|
+ if (!this.ready) {
|
|
|
+ throw Error(CustomError.DOWNLOAD_MANAGER_NOT_READY);
|
|
|
+ }
|
|
|
+ const getFilePromise = async (): Promise<SourceURLs> => {
|
|
|
const fileStream = await this.downloadFile(file);
|
|
|
const fileBlob = await new Response(fileStream).blob();
|
|
|
- if (forPreview) {
|
|
|
- return await getRenderableFileURL(file, fileBlob);
|
|
|
- } else {
|
|
|
- const fileURL = await createTypedObjectURL(
|
|
|
- fileBlob,
|
|
|
- file.metadata.title
|
|
|
- );
|
|
|
- return { converted: [fileURL], original: [fileURL] };
|
|
|
- }
|
|
|
+ return {
|
|
|
+ url: URL.createObjectURL(fileBlob),
|
|
|
+ isOriginal: true,
|
|
|
+ isRenderable: false,
|
|
|
+ type: 'normal',
|
|
|
+ };
|
|
|
};
|
|
|
- if (!this.fileObjectURLPromise.get(fileKey)) {
|
|
|
- this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
|
|
+ if (!this.fileObjectURLPromises.has(file.id)) {
|
|
|
+ if (!cacheInMemory) {
|
|
|
+ return await this.downloadFile(file);
|
|
|
+ }
|
|
|
+ this.fileObjectURLPromises.set(file.id, getFilePromise());
|
|
|
+ }
|
|
|
+ const fileURLs = await this.fileObjectURLPromises.get(file.id);
|
|
|
+ if (fileURLs.isOriginal) {
|
|
|
+ const fileStream = (await fetch(fileURLs.url as string)).body;
|
|
|
+ return fileStream;
|
|
|
+ } else {
|
|
|
+ return await this.downloadFile(file);
|
|
|
}
|
|
|
- const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
|
|
- return fileURLs;
|
|
|
} catch (e) {
|
|
|
- this.fileObjectURLPromise.delete(fileKey);
|
|
|
- logError(e, 'download manager Failed to get File');
|
|
|
+ this.fileObjectURLPromises.delete(file.id);
|
|
|
+ logError(e, 'download manager getFile Failed');
|
|
|
throw e;
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- public async getCachedOriginalFile(file: EnteFile) {
|
|
|
- return (await this.fileObjectURLPromise.get(file.id.toString()))
|
|
|
- ?.original;
|
|
|
}
|
|
|
|
|
|
- async downloadFile(
|
|
|
- file: EnteFile,
|
|
|
- tokenOverride?: string,
|
|
|
- usingWorker?: Remote<DedicatedCryptoWorker>,
|
|
|
- timeout?: number
|
|
|
- ) {
|
|
|
+ private async downloadFile(
|
|
|
+ file: EnteFile
|
|
|
+ ): Promise<ReadableStream<Uint8Array>> {
|
|
|
try {
|
|
|
- const cryptoWorker =
|
|
|
- usingWorker || (await ComlinkCryptoWorker.getInstance());
|
|
|
- const token = tokenOverride || getToken();
|
|
|
- if (!token) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ addLogLine(`download attempted for fileID:${file.id}`);
|
|
|
const onDownloadProgress = this.trackDownloadProgress(
|
|
|
file.id,
|
|
|
file.info?.fileSize
|
|
@@ -192,26 +297,30 @@ class DownloadManager {
|
|
|
file.metadata.fileType === FILE_TYPE.IMAGE ||
|
|
|
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
|
|
|
) {
|
|
|
- const resp = await retryAsyncFunction(() =>
|
|
|
- HTTPService.get(
|
|
|
- getFileURL(file.id),
|
|
|
- null,
|
|
|
- { 'X-Auth-Token': token },
|
|
|
- {
|
|
|
- responseType: 'arraybuffer',
|
|
|
- timeout,
|
|
|
- onDownloadProgress,
|
|
|
- }
|
|
|
- )
|
|
|
- );
|
|
|
- this.clearDownloadProgress(file.id);
|
|
|
- if (typeof resp.data === 'undefined') {
|
|
|
- throw Error(CustomError.REQUEST_FAILED);
|
|
|
+ let encrypted = await this.getCachedFile(file);
|
|
|
+ if (!encrypted) {
|
|
|
+ encrypted = new Response(
|
|
|
+ await this.downloadClient.downloadFile(
|
|
|
+ file,
|
|
|
+ onDownloadProgress
|
|
|
+ )
|
|
|
+ );
|
|
|
+ if (this.diskFileCache) {
|
|
|
+ this.diskFileCache
|
|
|
+ .put(file.id.toString(), encrypted.clone())
|
|
|
+ .catch((e) => {
|
|
|
+ logError(e, 'file cache put failed');
|
|
|
+ // TODO: handle storage full exception.
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
+ this.clearDownloadProgress(file.id);
|
|
|
try {
|
|
|
- const decrypted = await cryptoWorker.decryptFile(
|
|
|
- new Uint8Array(resp.data),
|
|
|
- await cryptoWorker.fromB64(file.file.decryptionHeader),
|
|
|
+ const decrypted = await this.cryptoWorker.decryptFile(
|
|
|
+ new Uint8Array(await encrypted.arrayBuffer()),
|
|
|
+ await this.cryptoWorker.fromB64(
|
|
|
+ file.file.decryptionHeader
|
|
|
+ ),
|
|
|
file.key
|
|
|
);
|
|
|
return generateStreamFromArrayBuffer(decrypted);
|
|
@@ -231,27 +340,35 @@ class DownloadManager {
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
- const resp = await retryAsyncFunction(() =>
|
|
|
- fetch(getFileURL(file.id), {
|
|
|
- headers: {
|
|
|
- 'X-Auth-Token': token,
|
|
|
- },
|
|
|
- })
|
|
|
- );
|
|
|
+
|
|
|
+ let resp: Response = await this.getCachedFile(file);
|
|
|
+ if (!resp) {
|
|
|
+ resp = await this.downloadClient.downloadFileStream(file);
|
|
|
+ if (this.diskFileCache) {
|
|
|
+ this.diskFileCache
|
|
|
+ .put(file.id.toString(), resp.clone())
|
|
|
+ .catch((e) => {
|
|
|
+ logError(e, 'file cache put failed');
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
const reader = resp.body.getReader();
|
|
|
|
|
|
const contentLength = +resp.headers.get('Content-Length') ?? 0;
|
|
|
let downloadedBytes = 0;
|
|
|
|
|
|
const stream = new ReadableStream({
|
|
|
- async start(controller) {
|
|
|
+ start: async (controller) => {
|
|
|
try {
|
|
|
- const decryptionHeader = await cryptoWorker.fromB64(
|
|
|
- file.file.decryptionHeader
|
|
|
+ const decryptionHeader =
|
|
|
+ await this.cryptoWorker.fromB64(
|
|
|
+ file.file.decryptionHeader
|
|
|
+ );
|
|
|
+ const fileKey = await this.cryptoWorker.fromB64(
|
|
|
+ file.key
|
|
|
);
|
|
|
- const fileKey = await cryptoWorker.fromB64(file.key);
|
|
|
const { pullState, decryptionChunkSize } =
|
|
|
- await cryptoWorker.initChunkDecryption(
|
|
|
+ await this.cryptoWorker.initChunkDecryption(
|
|
|
decryptionHeader,
|
|
|
fileKey
|
|
|
);
|
|
@@ -285,7 +402,7 @@ class DownloadManager {
|
|
|
);
|
|
|
try {
|
|
|
const { decryptedData } =
|
|
|
- await cryptoWorker.decryptFileChunk(
|
|
|
+ await this.cryptoWorker.decryptFileChunk(
|
|
|
fileData,
|
|
|
pullState
|
|
|
);
|
|
@@ -329,7 +446,7 @@ class DownloadManager {
|
|
|
if (data) {
|
|
|
try {
|
|
|
const { decryptedData } =
|
|
|
- await cryptoWorker.decryptFileChunk(
|
|
|
+ await this.cryptoWorker.decryptFileChunk(
|
|
|
data,
|
|
|
pullState
|
|
|
);
|
|
@@ -412,4 +529,58 @@ class DownloadManager {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-export default new DownloadManager();
|
|
|
+const DownloadManager = new DownloadManagerImpl();
|
|
|
+
|
|
|
+export default DownloadManager;
|
|
|
+
|
|
|
+async function openThumbnailCache() {
|
|
|
+ try {
|
|
|
+ return await CacheStorageService.open(CACHES.THUMBS);
|
|
|
+ } catch (e) {
|
|
|
+ logError(e, 'Failed to open thumbnail cache');
|
|
|
+ if (isInternalUser()) {
|
|
|
+ throw e;
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function openDiskFileCache() {
|
|
|
+ try {
|
|
|
+ if (!isElectron()) {
|
|
|
+ throw Error(CustomError.NOT_AVAILABLE_ON_WEB);
|
|
|
+ }
|
|
|
+ return await CacheStorageService.open(CACHES.FILES, FILE_CACHE_LIMIT);
|
|
|
+ } catch (e) {
|
|
|
+ logError(e, 'Failed to open file cache');
|
|
|
+ if (isInternalUser()) {
|
|
|
+ throw e;
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function createDownloadClient(
|
|
|
+ app: APPS,
|
|
|
+ tokens?: { token: string; passwordToken?: string } | { token: string },
|
|
|
+ timeout?: number
|
|
|
+): DownloadClient {
|
|
|
+ if (!timeout) {
|
|
|
+ timeout = 300000; // 5 minute
|
|
|
+ }
|
|
|
+ if (app === APPS.ALBUMS) {
|
|
|
+ if (!tokens) {
|
|
|
+ tokens = { token: undefined, passwordToken: undefined };
|
|
|
+ }
|
|
|
+ const { token, passwordToken } = tokens as {
|
|
|
+ token: string;
|
|
|
+ passwordToken: string;
|
|
|
+ };
|
|
|
+ return new PublicAlbumsDownloadClient(token, passwordToken, timeout);
|
|
|
+ } else {
|
|
|
+ const { token } = tokens;
|
|
|
+ return new PhotosDownloadClient(token, timeout);
|
|
|
+ }
|
|
|
+}
|