diff --git a/src/components/Sidebar/DebugLogs.tsx b/src/components/Sidebar/DebugLogs.tsx index 185698285..2f4283d80 100644 --- a/src/components/Sidebar/DebugLogs.tsx +++ b/src/components/Sidebar/DebugLogs.tsx @@ -4,6 +4,9 @@ import { downloadAsFile } from 'utils/file'; import constants from 'utils/strings/constants'; import { addLogLine, getDebugLogs } from 'utils/logging'; import SidebarButton from './Button'; +import { getData, LS_KEYS } from 'utils/storage/localStorage'; +import { User } from 'types/user'; +import { getSentryUserID } from 'utils/user'; export default function DebugLogs() { const appContext = useContext(AppContext); @@ -22,6 +25,11 @@ export default function DebugLogs() { }); const downloadDebugLogs = () => { + addLogLine( + 'latest commit id :' + process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH + ); + addLogLine(`user sentry id ${getSentryUserID()}`); + addLogLine(`ente userID ${(getData(LS_KEYS.USER) as User)?.id}`); addLogLine('exporting logs'); const logs = getDebugLogs(); const logString = logs.join('\n'); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 8fb4f531e..ed219cfd2 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -27,7 +27,6 @@ import { getRoadmapRedirectURL, } from 'services/userService'; import { CustomError } from 'utils/error'; -import { getSentryUserID } from 'utils/user'; export const MessageContainer = styled('div')` background-color: #111; @@ -208,10 +207,6 @@ export default function App({ Component, err }) { useEffect(() => { addLogLine(`app started`); - addLogLine( - `latest commit id :${process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH}` - ); - addLogLine(`user sentry id ${getSentryUserID()}`); }, []); useEffect(() => setMessageDialogView(true), [dialogMessage]); diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx index c972e0973..2fba50807 100644 --- a/src/pages/credentials/index.tsx +++ b/src/pages/credentials/index.tsx @@ -25,7 +25,7 @@ import FormPaperFooter from 'components/Form/FormPaper/Footer'; import LinkButton from 'components/pages/gallery/LinkButton'; import { CustomError } from 'utils/error'; import isElectron from 'is-electron'; -import desktopService from 'services/desktopService'; +import safeStorageService from 'services/electron/safeStorage'; import VerticallyCentered from 'components/Container'; import EnteSpinner from 'components/EnteSpinner'; import { Input } from '@mui/material'; @@ -43,7 +43,7 @@ export default function Credentials() { const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); let key = getKey(SESSION_KEYS.ENCRYPTION_KEY); if (!key && isElectron()) { - key = await desktopService.getEncryptionKey(); + key = await safeStorageService.getEncryptionKey(); if (key) { await saveKeyInSessionStore( SESSION_KEYS.ENCRYPTION_KEY, diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index e99203b21..7bf1172d7 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -14,7 +14,7 @@ import { trashFiles, deleteFromTrash, } from 'services/fileService'; -import { styled } from '@mui/material'; +import { styled, Typography } from '@mui/material'; import { syncCollections, getFavItemIds, @@ -101,6 +101,7 @@ import UploadInputs from 'components/UploadSelectorInputs'; import useFileInput from 'hooks/useFileInput'; import { User } from 'types/user'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; +import { CenteredFlex } from 'components/Container'; export const DeadCenter = styled('div')` flex: 1; @@ -110,12 +111,6 @@ export const DeadCenter = styled('div')` text-align: center; flex-direction: column; `; -const AlertContainer = styled('div')` - background-color: #111; - padding: 5px 0; - font-size: 14px; - text-align: center; -`; const defaultGalleryContext: GalleryContextType = { thumbs: new Map(), @@ -609,9 +604,11 @@ export default function Gallery() { )} {isFirstLoad && ( - - {constants.INITIAL_LOAD_DELAY_WARNING} - + + + {constants.INITIAL_LOAD_DELAY_WARNING} + + )} ( @@ -267,21 +270,23 @@ export default function PublicCollectionGallery() { } if (isPasswordProtected && !passwordJWTToken.current) { return ( - - - - - {constants.LINK_PASSWORD} - - - - - + + + {constants.PASSWORD} + + {constants.LINK_PASSWORD} + + + + ); } if (!publicFiles) { diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts new file mode 100644 index 000000000..e24302a77 --- /dev/null +++ b/src/services/cacheService.ts @@ -0,0 +1,31 @@ +import electronService from './electron/common'; +import electronCacheService from './electron/cache'; +import { logError } from 'utils/sentry'; + +const THUMB_CACHE = 'thumbs'; + +export function getCacheProvider() { + if (electronService.checkIsBundledApp()) { + return electronCacheService; + } else { + return caches; + } +} + +export async function openThumbnailCache() { + try { + return await getCacheProvider().open(THUMB_CACHE); + } catch (e) { + logError(e, 'openThumbnailCache failed'); + // log and ignore + } +} + +export async function deleteThumbnailCache() { + try { + return await getCacheProvider().delete(THUMB_CACHE); + } catch (e) { + logError(e, 'deleteThumbnailCache failed'); + // dont throw + } +} diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index b81253ce5..ca45c22ae 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -3,8 +3,7 @@ import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil'; import CryptoWorker from 'utils/crypto'; import { generateStreamFromArrayBuffer, - convertForPreview, - needsConversionForPreview, + getRenderableFileURL, createTypedObjectURL, } from 'utils/file'; import HTTPService from './HTTPService'; @@ -13,6 +12,7 @@ import { EnteFile } from 'types/file'; import { logError } from 'utils/sentry'; import { FILE_TYPE } from 'constants/file'; import { CustomError } from 'utils/error'; +import { openThumbnailCache } from './cacheService'; class DownloadManager { private fileObjectURLPromise = new Map>(); @@ -26,14 +26,7 @@ class DownloadManager { } if (!this.thumbnailObjectURLPromise.get(file.id)) { const downloadPromise = async () => { - const thumbnailCache = await (async () => { - try { - return await caches.open('thumbs'); - } catch (e) { - return null; - // ignore - } - })(); + const thumbnailCache = await openThumbnailCache(); const cacheResp: Response = await thumbnailCache?.match( file.id.toString() @@ -43,14 +36,13 @@ class DownloadManager { } 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. - } + + 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()); @@ -84,38 +76,24 @@ class DownloadManager { }; getFile = async (file: EnteFile, forPreview = false) => { - const shouldBeConverted = forPreview && needsConversionForPreview(file); - const fileKey = shouldBeConverted - ? `${file.id}_converted` - : `${file.id}`; + const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; try { - const getFilePromise = async (convert: boolean) => { + const getFilePromise = async () => { const fileStream = await this.downloadFile(file); const fileBlob = await new Response(fileStream).blob(); - if (convert) { - const convertedBlobs = await convertForPreview( - file, - fileBlob - ); - return await Promise.all( - convertedBlobs.map( - async (blob) => - await createTypedObjectURL( - blob, - file.metadata.title - ) - ) - ); + if (forPreview) { + return await getRenderableFileURL(file, fileBlob); + } else { + return [ + await createTypedObjectURL( + fileBlob, + file.metadata.title + ), + ]; } - return [ - await createTypedObjectURL(fileBlob, file.metadata.title), - ]; }; if (!this.fileObjectURLPromise.get(fileKey)) { - this.fileObjectURLPromise.set( - fileKey, - getFilePromise(shouldBeConverted) - ); + this.fileObjectURLPromise.set(fileKey, getFilePromise()); } const fileURLs = await this.fileObjectURLPromise.get(fileKey); return fileURLs; diff --git a/src/services/electron/cache.ts b/src/services/electron/cache.ts new file mode 100644 index 000000000..3c7d213a4 --- /dev/null +++ b/src/services/electron/cache.ts @@ -0,0 +1,24 @@ +import { runningInBrowser } from 'utils/common'; + +class ElectronCacheService { + private ElectronAPIs: any; + private allElectronAPIsExist: boolean = false; + + constructor() { + this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs']; + this.allElectronAPIsExist = !!this.ElectronAPIs?.openDiskCache; + } + async open(cacheName: string): Promise { + if (this.allElectronAPIsExist) { + return await this.ElectronAPIs.openDiskCache(cacheName); + } + } + + async delete(cacheName: string): Promise { + if (this.allElectronAPIsExist) { + return await this.ElectronAPIs.deleteDiskCache(cacheName); + } + } +} + +export default new ElectronCacheService(); diff --git a/src/services/electron/common.ts b/src/services/electron/common.ts new file mode 100644 index 000000000..8f29837b1 --- /dev/null +++ b/src/services/electron/common.ts @@ -0,0 +1,18 @@ +import isElectron from 'is-electron'; +import { runningInBrowser } from 'utils/common'; + +class ElectronService { + private ElectronAPIs: any; + private isBundledApp: boolean = false; + + constructor() { + this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs']; + this.isBundledApp = !!this.ElectronAPIs?.openDiskCache; + } + + checkIsBundledApp() { + return isElectron() && this.isBundledApp; + } +} + +export default new ElectronService(); diff --git a/src/services/desktopService.tsx b/src/services/electron/safeStorage.tsx similarity index 90% rename from src/services/desktopService.tsx rename to src/services/electron/safeStorage.tsx index a302e8bc0..58c31c180 100644 --- a/src/services/desktopService.tsx +++ b/src/services/electron/safeStorage.tsx @@ -1,7 +1,7 @@ import { runningInBrowser } from 'utils/common'; import { logError } from 'utils/sentry'; -class DesktopService { +class SafeStorageService { private ElectronAPIs: any; private allElectronAPIsExist: boolean = false; constructor() { @@ -35,8 +35,8 @@ class DesktopService { return await this.ElectronAPIs.clearElectronStore(); } } catch (e) { - logError(e, 'getEncryptionKey failed'); + logError(e, 'clearElectronStore failed'); } } } -export default new DesktopService(); +export default new SafeStorageService(); diff --git a/src/services/heicConverter/heicConverterService.ts b/src/services/heicConverter/heicConverterService.ts index e93ec3871..bc478e0b8 100644 --- a/src/services/heicConverter/heicConverterService.ts +++ b/src/services/heicConverter/heicConverterService.ts @@ -4,6 +4,7 @@ import { createNewConvertWorker } from 'utils/heicConverter'; import { retryAsyncFunction } from 'utils/network'; import { logError } from 'utils/sentry'; import { addLogLine } from 'utils/logging'; +import { makeHumanReadableStorage } from 'utils/billing'; const WORKER_POOL_SIZE = 2; const MAX_CONVERSION_IN_PARALLEL = 1; @@ -40,11 +41,21 @@ class HEICConverter { const timeout = setTimeout(() => { reject(Error('wait time exceeded')); }, WAIT_TIME_IN_MICROSECONDS); - const convertedHEIC = + const startTime = Date.now(); + const convertedHEIC: Blob = await comlink.convertHEIC( fileBlob, format ); + addLogLine( + `originalFileSize:${makeHumanReadableStorage( + fileBlob?.size + )},convertedFileSize:${makeHumanReadableStorage( + convertedHEIC?.size + )}, heic conversion time: ${ + Date.now() - startTime + }ms ` + ); clearTimeout(timeout); resolve(convertedHEIC); } catch (e) { @@ -54,6 +65,20 @@ class HEICConverter { main(); } ); + if (!convertedHEIC || convertedHEIC?.size === 0) { + logError( + Error(`converted heic fileSize is Zero`), + 'converted heic fileSize is Zero', + { + originalFileSize: makeHumanReadableStorage( + fileBlob?.size ?? 0 + ), + convertedFileSize: makeHumanReadableStorage( + convertedHEIC?.size ?? 0 + ), + } + ); + } await new Promise((resolve) => { setTimeout( () => resolve(null), diff --git a/src/services/publicCollectionDownloadManager.ts b/src/services/publicCollectionDownloadManager.ts index 0d62ebf06..9b56b3bbb 100644 --- a/src/services/publicCollectionDownloadManager.ts +++ b/src/services/publicCollectionDownloadManager.ts @@ -5,8 +5,8 @@ import { import CryptoWorker from 'utils/crypto'; import { generateStreamFromArrayBuffer, - convertForPreview, - needsConversionForPreview, + getRenderableFileURL, + createTypedObjectURL, } from 'utils/file'; import HTTPService from './HTTPService'; import { EnteFile } from 'types/file'; @@ -107,34 +107,28 @@ class PublicCollectionDownloadManager { passwordToken: string, forPreview = false ) => { - const shouldBeConverted = forPreview && needsConversionForPreview(file); - const fileKey = shouldBeConverted - ? `${file.id}_converted` - : `${file.id}`; + const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; try { - const getFilePromise = async (convert: boolean) => { + const getFilePromise = async () => { const fileStream = await this.downloadFile( token, passwordToken, file ); const fileBlob = await new Response(fileStream).blob(); - if (convert) { - const convertedBlobs = await convertForPreview( - file, - fileBlob - ); - return convertedBlobs.map((blob) => - URL.createObjectURL(blob) - ); + if (forPreview) { + return await getRenderableFileURL(file, fileBlob); + } else { + return [ + await createTypedObjectURL( + fileBlob, + file.metadata.title + ), + ]; } - return [URL.createObjectURL(fileBlob)]; }; if (!this.fileObjectURLPromise.get(fileKey)) { - this.fileObjectURLPromise.set( - fileKey, - getFilePromise(shouldBeConverted) - ); + this.fileObjectURLPromise.set(fileKey, getFilePromise()); } const fileURLs = await this.fileObjectURLPromise.get(fileKey); return fileURLs; diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index 6170198e8..d5b13046a 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 'constants/upload'; import FFmpegService from 'services/ffmpeg/ffmpegService'; import { convertBytesToHumanReadable } from 'utils/file/size'; -import { isFileHEIC } from 'utils/file'; +import { isExactTypeHEIC } from 'utils/file'; import { ElectronFile, FileTypeInfo } from 'types/upload'; import { getUint8ArrayView } from '../readerService'; import HEICConverter from 'services/heicConverter/heicConverterService'; @@ -37,7 +37,7 @@ export async function generateThumbnail( file = new File([await file.blob()], file.name); } if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - const isHEIC = isFileHEIC(fileTypeInfo.exactType); + const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType); canvas = await generateImageThumbnail(file, isHEIC); } else { try { diff --git a/src/services/userService.ts b/src/services/userService.ts index 152636380..832455d1d 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -20,7 +20,8 @@ import { import { getLocalFamilyData, isPartOfFamily } from 'utils/billing'; import { ServerErrorCodes } from 'utils/error'; import isElectron from 'is-electron'; -import desktopService from './desktopService'; +import safeStorageService from './electron/safeStorage'; +import { deleteThumbnailCache } from './cacheService'; const ENDPOINT = getEndpoint(); @@ -118,13 +119,13 @@ export const logoutUser = async () => { clearKeys(); clearData(); try { - await caches.delete('thumbs'); + await deleteThumbnailCache(); } catch (e) { // ignore } await clearFiles(); if (isElectron()) { - desktopService.clearElectronStore(); + safeStorageService.clearElectronStore(); } router.push(PAGES.ROOT); } catch (e) { diff --git a/src/utils/crypto/index.ts b/src/utils/crypto/index.ts index 2c4546c55..38f303b24 100644 --- a/src/utils/crypto/index.ts +++ b/src/utils/crypto/index.ts @@ -8,7 +8,7 @@ import { setRecoveryKey } from 'services/userService'; import { logError } from 'utils/sentry'; import { ComlinkWorker } from 'utils/comlink'; import isElectron from 'is-electron'; -import desktopService from 'services/desktopService'; +import safeStorageService from 'services/electron/safeStorage'; export interface B64EncryptionResult { encryptedData: string; @@ -103,7 +103,7 @@ export const saveKeyInSessionStore = async ( const sessionKeyAttributes = await cryptoWorker.encryptToB64(key); setKey(keyType, sessionKeyAttributes); if (isElectron() && !fromDesktop) { - desktopService.setEncryptionKey(key); + safeStorageService.setEncryptionKey(key); } }; diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 98ce23524..bbb0689e8 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -26,7 +26,8 @@ import ffmpegService from 'services/ffmpeg/ffmpegService'; import { NEW_FILE_MAGIC_METADATA, VISIBILITY_STATE } from 'types/magicMetadata'; import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata'; import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection'; - +import { addLogLine } from 'utils/logging'; +import { makeHumanReadableStorage } from 'utils/billing'; export function downloadAsFile(filename: string, content: string) { const file = new Blob([content], { type: 'text/plain', @@ -131,14 +132,6 @@ function downloadUsingAnchor(link: string, name: string) { a.remove(); } -export function isFileHEIC(mimeType: string) { - return ( - mimeType && - (mimeType.toLowerCase().endsWith(TYPE_HEIC) || - mimeType.toLowerCase().endsWith(TYPE_HEIF)) - ); -} - export function sortFilesIntoCollections(files: EnteFile[]) { const collectionWiseFiles = new Map([ [ARCHIVE_SECTION, []], @@ -327,40 +320,71 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) { }); } -export async function convertForPreview( +export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) { + switch (file.metadata.fileType) { + case FILE_TYPE.IMAGE: { + const convertedBlob = await getRenderableImage( + file.metadata.title, + fileBlob + ); + return [URL.createObjectURL(convertedBlob)]; + } + case FILE_TYPE.LIVE_PHOTO: { + const livePhoto = await getRenderableLivePhoto(file, fileBlob); + return livePhoto.map((asset) => URL.createObjectURL(asset)); + } + default: + return [URL.createObjectURL(fileBlob)]; + } +} + +async function getRenderableLivePhoto( file: EnteFile, fileBlob: Blob ): Promise { - const convertIfHEIC = async (fileName: string, fileBlob: Blob) => { - const mimeType = ( - await getFileType(new File([fileBlob], file.metadata.title)) - ).exactType; - if (isFileHEIC(mimeType)) { - fileBlob = await HEICConverter.convert(fileBlob); - } - return fileBlob; - }; + const originalName = fileNameWithoutExtension(file.metadata.title); + const motionPhoto = await decodeMotionPhoto(fileBlob, originalName); + const imageBlob = new Blob([motionPhoto.image]); + return await Promise.all([ + getRenderableImage(motionPhoto.imageNameTitle, imageBlob), + getPlayableVideo(motionPhoto.videoNameTitle, motionPhoto.video), + ]); +} - if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - const originalName = fileNameWithoutExtension(file.metadata.title); - const motionPhoto = await decodeMotionPhoto(fileBlob, originalName); - let image = new Blob([motionPhoto.image]); +async function getPlayableVideo(videoNameTitle: string, video: Uint8Array) { + const mp4ConvertedVideo = await ffmpegService.convertToMP4( + video, + videoNameTitle + ); + return new Blob([mp4ConvertedVideo]); +} - // can run conversion in parellel as video and image - // have different processes - const convertedVideo = ffmpegService.convertToMP4( - motionPhoto.video, - motionPhoto.videoNameTitle +async function getRenderableImage(fileName: string, imageBlob: Blob) { + if (await isFileHEIC(imageBlob, fileName)) { + addLogLine( + `HEICConverter called for ${fileName}-${makeHumanReadableStorage( + imageBlob.size + )}` ); - - image = await convertIfHEIC(motionPhoto.imageNameTitle, image); - const video = new Blob([await convertedVideo]); - - return [image, video]; + const convertedImageBlob = await HEICConverter.convert(imageBlob); + addLogLine(`${fileName} successfully converted`); + return convertedImageBlob; + } else { + return imageBlob; } +} - fileBlob = await convertIfHEIC(file.metadata.title, fileBlob); - return [fileBlob]; +export async function isFileHEIC(fileBlob: Blob, fileName: string) { + const tempFile = new File([fileBlob], fileName); + const { exactType } = await getFileType(tempFile); + return isExactTypeHEIC(exactType); +} + +export function isExactTypeHEIC(exactType: string) { + return ( + exactType.toLowerCase().endsWith(TYPE_HEIC) || + exactType.toLowerCase().endsWith(TYPE_HEIF) + ); } export async function changeFilesVisibility( @@ -510,17 +534,15 @@ export async function downloadFiles(files: EnteFile[]) { } } -export function needsConversionForPreview(file: EnteFile) { - const fileExtension = splitFilenameAndExtension(file.metadata.title)[1]; - if ( +export async function needsConversionForPreview( + file: EnteFile, + fileBlob: Blob +) { + const isHEIC = await isFileHEIC(fileBlob, file.metadata.title); + return ( file.metadata.fileType === FILE_TYPE.LIVE_PHOTO || - (file.metadata.fileType === FILE_TYPE.IMAGE && - isFileHEIC(fileExtension)) - ) { - return true; - } else { - return false; - } + (file.metadata.fileType === FILE_TYPE.IMAGE && isHEIC) + ); } export const isLivePhoto = (file: EnteFile) => diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 37e58b985..91e6fc5a4 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -227,7 +227,7 @@ const englishConstants = { target="_blank" style={{ color: '#51cd7c' }} rel="noreferrer"> - android + Android {' '} or{' '} - ios app{' '} + iOS app{' '} to automatically backup all your photos