From 97b9ac0626ef4362ae488ecab789eb32c50936a9 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 27 Nov 2021 13:23:05 +0530 Subject: [PATCH 01/10] add download all button --- src/components/icons/DownloadIcon.tsx | 25 +++++++++++++++++++ .../pages/gallery/SelectedFileOptions.tsx | 8 ++++++ src/pages/gallery/index.tsx | 8 ++++++ src/utils/file/index.ts | 6 +++++ 4 files changed, 47 insertions(+) create mode 100644 src/components/icons/DownloadIcon.tsx diff --git a/src/components/icons/DownloadIcon.tsx b/src/components/icons/DownloadIcon.tsx new file mode 100644 index 000000000..0172c6dc2 --- /dev/null +++ b/src/components/icons/DownloadIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export default function DownloadIcon(props) { + return ( + + + + + + + + + ); +} + +DownloadIcon.defaultProps = { + height: 24, + width: 24, + viewBox: '0 0 24 24', +}; diff --git a/src/components/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx index 5581fe7e5..7985e5a1c 100644 --- a/src/components/pages/gallery/SelectedFileOptions.tsx +++ b/src/components/pages/gallery/SelectedFileOptions.tsx @@ -23,6 +23,7 @@ import { FIX_CREATION_TIME_VISIBLE_TO_USER_IDS, User, } from 'services/userService'; +import DownloadIcon from 'components/icons/DownloadIcon'; interface Props { addToCollectionHelper: (collection: Collection) => void; @@ -34,6 +35,7 @@ interface Props { deleteFileHelper: (permanent?: boolean) => void; removeFromCollectionHelper: () => void; fixTimeHelper: () => void; + downloadHelper: () => void; count: number; clearSelection: () => void; archiveFilesHelper: () => void; @@ -79,6 +81,7 @@ const SelectedFileOptions = ({ setDialogMessage, setCollectionSelectorAttributes, deleteFileHelper, + downloadHelper, count, clearSelection, archiveFilesHelper, @@ -190,6 +193,11 @@ const SelectedFileOptions = ({ )} + + + + + diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index 1b2372cb0..a342ffcb1 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -50,6 +50,7 @@ import { LoadingOverlay } from 'components/LoadingOverlay'; import PhotoFrame from 'components/PhotoFrame'; import { changeFilesVisibility, + downloadFiles, getNonTrashedUniqueUserFiles, getSelectedFiles, mergeMetadata, @@ -545,6 +546,12 @@ export default function Gallery() { clearSelection(); }; + const downloadHelper = async () => { + const selectedFiles = getSelectedFiles(selected, files); + downloadFiles(selectedFiles); + clearSelection(); + }; + return ( Date: Sat, 27 Nov 2021 15:40:15 +0530 Subject: [PATCH 02/10] add download logic --- src/pages/gallery/index.tsx | 4 +++- src/utils/file/index.ts | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index a342ffcb1..58536e07a 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -548,8 +548,10 @@ export default function Gallery() { const downloadHelper = async () => { const selectedFiles = getSelectedFiles(selected, files); - downloadFiles(selectedFiles); clearSelection(); + loadingBar.current?.continuousStart(); + await downloadFiles(selectedFiles); + loadingBar.current.complete(); }; return ( diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 29cc98a3b..f801af68f 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -11,7 +11,7 @@ import { } from 'services/fileService'; import { decodeMotionPhoto } from 'services/motionPhotoService'; import { getMimeTypeFromBlob } from 'services/upload/readFileService'; -import DownloadManger from 'services/downloadManager'; +import DownloadManager from 'services/downloadManager'; import { logError } from 'utils/sentry'; import { User } from 'services/userService'; import CryptoWorker from 'utils/crypto'; @@ -37,10 +37,12 @@ export function downloadAsFile(filename: string, content: string) { a.remove(); } -export async function downloadFile(file) { +export async function downloadFile(file: File) { const a = document.createElement('a'); a.style.display = 'none'; - a.href = await DownloadManger.getFile(file); + a.href = URL.createObjectURL( + await new Response(await DownloadManager.downloadFile(file)).blob() + ); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; } else { @@ -469,6 +471,10 @@ export function getNonTrashedUniqueUserFiles(files: File[]) { export async function downloadFiles(files: File[]) { for (const file of files) { - await downloadFile(file); + try { + await downloadFile(file); + } catch (e) { + logError(e, 'download fail for file'); + } } } From 4b93b9e9a4e7422efc57cbdbab9fc05e8ca3b8d1 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Sat, 27 Nov 2021 16:47:54 +0530 Subject: [PATCH 03/10] only show progress bar if sync not in progress --- src/pages/gallery/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index 58536e07a..3f3976357 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -549,9 +549,9 @@ export default function Gallery() { const downloadHelper = async () => { const selectedFiles = getSelectedFiles(selected, files); clearSelection(); - loadingBar.current?.continuousStart(); + !syncInProgress.current && loadingBar.current?.continuousStart(); await downloadFiles(selectedFiles); - loadingBar.current.complete(); + !syncInProgress.current && loadingBar.current.complete(); }; return ( From f688d620cc1b7bcbbcd75bacd023d4f2cea1c064 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 10:42:46 +0530 Subject: [PATCH 04/10] prevent duplicate file download calls --- src/services/downloadManager.ts | 6 ++++++ src/utils/file/index.ts | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 77b8f656d..20c89ff92 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -99,6 +99,12 @@ class DownloadManager { } }; + public async getCachedFile(file: File) { + return await this.fileObjectUrlPromise.get( + `${file.id}_forPreview=false` + ); + } + async downloadFile(file: File) { const worker = await new CryptoWorker(); const token = getToken(); diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index f801af68f..4eac2426d 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -40,9 +40,13 @@ export function downloadAsFile(filename: string, content: string) { export async function downloadFile(file: File) { const a = document.createElement('a'); a.style.display = 'none'; - a.href = URL.createObjectURL( - await new Response(await DownloadManager.downloadFile(file)).blob() - ); + const cachedFileUrl = await DownloadManager.getCachedFile(file); + const fileURL = + cachedFileUrl ?? + URL.createObjectURL( + await new Response(await DownloadManager.downloadFile(file)).blob() + ); + a.href = fileURL; if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; } else { From f997c7457a70c26d895b4e138c6b6efbe54692e5 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 11:18:28 +0530 Subject: [PATCH 05/10] use converted suffix instead of forPreview to to avoid duplicate download of unconverted files --- src/services/downloadManager.ts | 36 ++++++++++++++++++--------------- src/utils/file/index.ts | 22 ++++++++++++++++---- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 20c89ff92..c08edacce 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -1,7 +1,11 @@ import { getToken } from 'utils/common/key'; import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil'; import CryptoWorker from 'utils/crypto'; -import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file'; +import { + generateStreamFromArrayBuffer, + convertForPreview, + needsConversionForPreview, +} from 'utils/file'; import HTTPService from './HTTPService'; import { File, FILE_TYPE } from './fileService'; import { logError } from 'utils/sentry'; @@ -73,36 +77,36 @@ class DownloadManager { }; getFile = async (file: File, forPreview = false) => { - let fileUID: string; - if (file.metadata.fileType === FILE_TYPE.VIDEO) { - fileUID = file.id.toString(); - } else { - fileUID = `${file.id}_forPreview=${forPreview}`; - } + const shouldBeConverted = forPreview && needsConversionForPreview(file); + const fileKey = shouldBeConverted + ? `${file.id}_converted` + : `${file.id}`; try { - const getFilePromise = async () => { + const getFilePromise = async (convert: boolean) => { const fileStream = await this.downloadFile(file); let fileBlob = await new Response(fileStream).blob(); - if (forPreview) { + if (convert) { fileBlob = await convertForPreview(file, fileBlob); } return URL.createObjectURL(fileBlob); }; - if (!this.fileObjectUrlPromise.get(fileUID)) { - this.fileObjectUrlPromise.set(fileUID, getFilePromise()); + if (!this.fileObjectUrlPromise.get(fileKey)) { + this.fileObjectUrlPromise.set( + fileKey, + getFilePromise(shouldBeConverted) + ); } - return await this.fileObjectUrlPromise.get(fileUID); + const fileURL = await this.fileObjectUrlPromise.get(fileKey); + return fileURL; } catch (e) { - this.fileObjectUrlPromise.delete(fileUID); + this.fileObjectUrlPromise.delete(fileKey); logError(e, 'Failed to get File'); throw e; } }; public async getCachedFile(file: File) { - return await this.fileObjectUrlPromise.get( - `${file.id}_forPreview=false` - ); + return await this.fileObjectUrlPromise.get(file.id.toString()); } async downloadFile(file: File) { diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 4eac2426d..bcefe21c4 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -57,10 +57,11 @@ export async function downloadFile(file: File) { a.remove(); } -export function fileIsHEIC(mimeType: string) { +export function isFileHEIC(mimeType: string) { return ( - mimeType.toLowerCase().endsWith(TYPE_HEIC) || - mimeType.toLowerCase().endsWith(TYPE_HEIF) + mimeType && + (mimeType.toLowerCase().endsWith(TYPE_HEIC) || + mimeType.toLowerCase().endsWith(TYPE_HEIF)) ); } @@ -277,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) { const mimeType = (await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension; - if (fileIsHEIC(mimeType)) { + if (isFileHEIC(mimeType)) { fileBlob = await worker.convertHEIC2JPEG(fileBlob); } return fileBlob; @@ -482,3 +483,16 @@ export async function downloadFiles(files: File[]) { } } } + +export function needsConversionForPreview(file: File) { + const fileExtension = splitFilenameAndExtension(file.metadata.title)[1]; + if ( + file.metadata.fileType === FILE_TYPE.LIVE_PHOTO || + (file.metadata.fileType === FILE_TYPE.IMAGE && + isFileHEIC(fileExtension)) + ) { + return true; + } else { + return false; + } +} From 9e3c2947754447708b556fd93e1415ec0e3ada02 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 12:08:53 +0530 Subject: [PATCH 06/10] check in memory cache for thumbnail before hitting browser cache --- src/services/downloadManager.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index c08edacce..a4182973b 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -20,21 +20,24 @@ class DownloadManager { if (!token) { return null; } - const thumbnailCache = await caches.open('thumbs'); - const cacheResp: Response = await thumbnailCache.match( - file.id.toString() - ); - if (cacheResp) { - return URL.createObjectURL(await cacheResp.blob()); - } if (!this.thumbnailObjectUrlPromise.get(file.id)) { - const downloadPromise = this.downloadThumb( - token, - thumbnailCache, - file - ); - this.thumbnailObjectUrlPromise.set(file.id, downloadPromise); + const downloadPromise = async () => { + const thumbnailCache = await caches.open('thumbs'); + const cacheResp: Response = await thumbnailCache.match( + file.id.toString() + ); + if (cacheResp) { + return URL.createObjectURL(await cacheResp.blob()); + } + return await this.downloadThumb( + token, + thumbnailCache, + file + ); + }; + this.thumbnailObjectUrlPromise.set(file.id, downloadPromise()); } + return await this.thumbnailObjectUrlPromise.get(file.id); } catch (e) { this.thumbnailObjectUrlPromise.delete(file.id); From 1f3734b577d8a08d49609b6b4d32c19d58ec6326 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 12:09:21 +0530 Subject: [PATCH 07/10] fix isFileHEIC check --- src/services/upload/thumbnailService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index dd79222f3..8872150df 100644 --- a/src/services/upload/thumbnailService.ts +++ b/src/services/upload/thumbnailService.ts @@ -4,7 +4,7 @@ import { logError } from 'utils/sentry'; import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64'; import FFmpegService from 'services/ffmpegService'; import { convertToHumanReadable } from 'utils/billingUtil'; -import { fileIsHEIC } from 'utils/file'; +import { isFileHEIC } from 'utils/file'; import { FileTypeInfo } from './readFileService'; const MAX_THUMBNAIL_DIMENSION = 720; @@ -31,7 +31,7 @@ export async function generateThumbnail( let thumbnail: Uint8Array; try { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - const isHEIC = fileIsHEIC(fileTypeInfo.exactType); + const isHEIC = isFileHEIC(fileTypeInfo.exactType); canvas = await generateImageThumbnail(worker, file, isHEIC); } else { try { From a1672d3c2f08cc808e6953858ea16f89c47d6f54 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 14:11:41 +0530 Subject: [PATCH 08/10] handle saving to cache in getPreview itself --- src/services/downloadManager.ts | 35 +++++++++---------------- src/services/migrateThumbnailService.ts | 2 +- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index a4182973b..583e79f8c 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -29,11 +29,17 @@ class DownloadManager { if (cacheResp) { return URL.createObjectURL(await cacheResp.blob()); } - return await this.downloadThumb( - token, - thumbnailCache, - file - ); + const thumb = await this.downloadThumb(token, file); + const thumbBlob = new Blob([thumb]); + try { + await thumbnailCache.put( + file.id.toString(), + new Response(thumbBlob) + ); + } catch (e) { + // TODO: handle storage full exception. + } + return URL.createObjectURL(thumbBlob); }; this.thumbnailObjectUrlPromise.set(file.id, downloadPromise()); } @@ -46,24 +52,7 @@ class DownloadManager { } } - private downloadThumb = async ( - token: string, - thumbnailCache: Cache, - file: File - ) => { - const thumb = await this.getThumbnail(token, file); - try { - await thumbnailCache.put( - file.id.toString(), - new Response(new Blob([thumb])) - ); - } catch (e) { - // TODO: handle storage full exception. - } - return URL.createObjectURL(new Blob([thumb])); - }; - - getThumbnail = async (token: string, file: File) => { + downloadThumb = async (token: string, file: File) => { const resp = await HTTPService.get( getThumbnailUrl(file.id), null, diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts index d11286ec5..4245a4538 100644 --- a/src/services/migrateThumbnailService.ts +++ b/src/services/migrateThumbnailService.ts @@ -67,7 +67,7 @@ export async function replaceThumbnail( current: idx, total: largeThumbnailFiles.length, }); - const originalThumbnail = await downloadManager.getThumbnail( + const originalThumbnail = await downloadManager.downloadThumb( token, file ); From 3a0e6b2f987f7644bf28be119bc387624ab60ad5 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 14:12:24 +0530 Subject: [PATCH 09/10] renamed get preview to getThumbnail --- src/components/PhotoFrame.tsx | 2 +- src/components/pages/gallery/PreviewCard.tsx | 2 +- src/services/downloadManager.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index d75947e9f..b91ea03ca 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -354,7 +354,7 @@ const PhotoFrame = ({ if (galleryContext.thumbs.has(item.id)) { url = galleryContext.thumbs.get(item.id); } else { - url = await DownloadManager.getPreview(item); + url = await DownloadManager.getThumbnail(item); galleryContext.thumbs.set(item.id, url); } updateUrl(item.dataIndex)(url); diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index b04963826..ddb4f7de0 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) { if (file && !file.msrc) { const main = async () => { try { - const url = await DownloadManager.getPreview(file); + const url = await DownloadManager.getThumbnail(file); if (isMounted.current) { setImgSrc(url); thumbs.set(file.id, url); diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 583e79f8c..ba353210d 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -14,7 +14,7 @@ class DownloadManager { private fileObjectUrlPromise = new Map>(); private thumbnailObjectUrlPromise = new Map>(); - public async getPreview(file: File) { + public async getThumbnail(file: File) { try { const token = getToken(); if (!token) { From 9454540175a675a37ff650aca5f8239d4a84b491 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Mon, 29 Nov 2021 14:13:56 +0530 Subject: [PATCH 10/10] update name to getCachedOriginalFile --- src/services/downloadManager.ts | 2 +- src/utils/file/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index ba353210d..5e4c4b856 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -97,7 +97,7 @@ class DownloadManager { } }; - public async getCachedFile(file: File) { + public async getCachedOriginalFile(file: File) { return await this.fileObjectUrlPromise.get(file.id.toString()); } diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index bcefe21c4..1e7b08f25 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -40,7 +40,7 @@ export function downloadAsFile(filename: string, content: string) { export async function downloadFile(file: File) { const a = document.createElement('a'); a.style.display = 'none'; - const cachedFileUrl = await DownloadManager.getCachedFile(file); + const cachedFileUrl = await DownloadManager.getCachedOriginalFile(file); const fileURL = cachedFileUrl ?? URL.createObjectURL(