From 0c81d2ff56d71be28011afe10ebffc5d2d587608 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 05:19:10 +0530 Subject: [PATCH 1/7] Prune --- web/apps/photos/src/utils/file/index.ts | 4 ++-- web/packages/gallery/services/upload/upload-service.ts | 1 - web/packages/media/file-type.ts | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index ac0f9426fd..d53d9ac652 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -61,9 +61,9 @@ export async function downloadFile(file: EnteFile) { new Blob([imageData], { type: imageType.mimeType }), ); const video = new File([videoData], videoFileName); - const videoType = await detectFileTypeInfo(video); + const { mimeType } = await detectFileTypeInfo(video); const tempVideoURL = URL.createObjectURL( - new Blob([videoData], { type: videoType.mimeType }), + new Blob([videoData], { type: mimeType }), ); downloadAndRevokeObjectURL(tempImageURL, imageFileName); // Downloading multiple works everywhere except, you guessed it, diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 165c50e1e1..9db7b88068 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -1002,7 +1002,6 @@ const readLivePhotoDetails = async ({ image, video }: LivePhotoAssets) => { fileType: FileType.livePhoto, extension: `${img.fileTypeInfo.extension}+${vid.fileTypeInfo.extension}`, imageType: img.fileTypeInfo.extension, - videoType: vid.fileTypeInfo.extension, }, fileSize: img.fileSize + vid.fileSize, lastModifiedMs: img.lastModifiedMs, diff --git a/web/packages/media/file-type.ts b/web/packages/media/file-type.ts index e8dcf6b2c9..224507bfee 100644 --- a/web/packages/media/file-type.ts +++ b/web/packages/media/file-type.ts @@ -50,7 +50,6 @@ export interface FileTypeInfo { extension: string; mimeType?: string; imageType?: string; - videoType?: string; } // list of format that were missed by type-detection for some files. From 6e5fb95e8f9908efec2451fe34c0136e7fcec238 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 05:46:49 +0530 Subject: [PATCH 2/7] Re --- web/apps/photos/src/components/Sidebar.tsx | 4 +- web/apps/photos/src/utils/file/index.ts | 74 +++++++++---------- .../accounts/components/RecoveryKey.tsx | 8 +- web/packages/base/utils/web.ts | 13 ++-- .../photos/components/ImageEditorOverlay.tsx | 4 +- 5 files changed, 51 insertions(+), 52 deletions(-) diff --git a/web/apps/photos/src/components/Sidebar.tsx b/web/apps/photos/src/components/Sidebar.tsx index 1b67d8cb51..70659a67ff 100644 --- a/web/apps/photos/src/components/Sidebar.tsx +++ b/web/apps/photos/src/components/Sidebar.tsx @@ -59,7 +59,7 @@ import { import log from "ente-base/log"; import { savedLogs } from "ente-base/log-web"; import { customAPIHost } from "ente-base/origins"; -import { downloadString } from "ente-base/utils/web"; +import { saveStringAsFile } from "ente-base/utils/web"; import { isHLSGenerationSupported, toggleHLSGeneration, @@ -1079,7 +1079,7 @@ const Help: React.FC = ({ log.info("Viewing logs"); const electron = globalThis.electron; if (electron) electron.openLogDirectory(); - else downloadString(savedLogs(), `ente-web-logs-${Date.now()}.txt`); + else saveStringAsFile(savedLogs(), `ente-web-logs-${Date.now()}.txt`); }; return ( diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index d53d9ac652..ae687d0c77 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -2,7 +2,7 @@ import type { User } from "ente-accounts/services/user"; import { joinPath } from "ente-base/file-name"; import log from "ente-base/log"; import { type Electron } from "ente-base/types/ipc"; -import { downloadAndRevokeObjectURL } from "ente-base/utils/web"; +import { saveAsFileAndRevokeObjectURL } from "ente-base/utils/web"; import { downloadManager } from "ente-gallery/services/download"; import { updateFileMagicMetadata } from "ente-gallery/services/file"; import { updateMagicMetadata } from "ente-gallery/services/magic-metadata"; @@ -48,42 +48,6 @@ export type FileOp = | "trash" | "deletePermanently"; -export async function downloadFile(file: EnteFile) { - try { - let fileBlob = await downloadManager.fileBlob(file); - const fileName = fileFileName(file); - if (file.metadata.fileType == FileType.livePhoto) { - const { imageFileName, imageData, videoFileName, videoData } = - await decodeLivePhoto(fileName, fileBlob); - const image = new File([imageData], imageFileName); - const imageType = await detectFileTypeInfo(image); - const tempImageURL = URL.createObjectURL( - new Blob([imageData], { type: imageType.mimeType }), - ); - const video = new File([videoData], videoFileName); - const { mimeType } = await detectFileTypeInfo(video); - const tempVideoURL = URL.createObjectURL( - new Blob([videoData], { type: mimeType }), - ); - downloadAndRevokeObjectURL(tempImageURL, imageFileName); - // Downloading multiple works everywhere except, you guessed it, - // Safari. Make up for their incompetence by adding a setTimeout. - await wait(300) /* arbitrary constant, 300ms */; - downloadAndRevokeObjectURL(tempVideoURL, videoFileName); - } else { - const { mimeType } = await detectFileTypeInfo( - new File([fileBlob], fileName), - ); - fileBlob = new Blob([fileBlob], { type: mimeType }); - const tempURL = URL.createObjectURL(fileBlob); - downloadAndRevokeObjectURL(tempURL, fileName); - } - } catch (e) { - log.error("failed to download file", e); - throw e; - } -} - function getSelectedFileIds(selectedFiles: SelectedState) { const filesIDs: number[] = []; for (const [key, val] of Object.entries(selectedFiles)) { @@ -240,7 +204,7 @@ export async function downloadFiles( if (progressBarUpdater?.isCancelled()) { return; } - await downloadFile(file); + await saveAsFile(file); progressBarUpdater?.increaseSuccess(); } catch (e) { log.error("download fail for file", e); @@ -249,6 +213,40 @@ export async function downloadFiles( } } +/** + * Save the given {@link EnteFile} as a file in the user's download folder. + */ +const saveAsFile = async (file: EnteFile) => { + const fileBlob = await downloadManager.fileBlob(file); + const fileName = fileFileName(file); + if (file.metadata.fileType == FileType.livePhoto) { + const { imageFileName, imageData, videoFileName, videoData } = + await decodeLivePhoto(fileName, fileBlob); + + await saveBlobPartAsFile(imageData, imageFileName); + + // Downloading multiple works everywhere except, you guessed it, + // Safari. Make up for their incompetence by adding a setTimeout. + await wait(300) /* arbitrary constant, 300ms */; + await saveBlobPartAsFile(videoData, videoFileName); + } else { + await saveBlobPartAsFile(fileBlob, fileName); + } +}; + +/** + * Save the given {@link blob} as a file in the user's download folder. + */ +const saveBlobPartAsFile = async (blobPart: BlobPart, fileName: string) => { + const { mimeType } = await detectFileTypeInfo( + new File([blobPart], fileName), + ); + saveAsFileAndRevokeObjectURL( + URL.createObjectURL(new Blob([blobPart], { type: mimeType })), + fileName, + ); +}; + async function downloadFilesDesktop( electron: Electron, files: EnteFile[], diff --git a/web/packages/accounts/components/RecoveryKey.tsx b/web/packages/accounts/components/RecoveryKey.tsx index 525e1bc53c..16c8899736 100644 --- a/web/packages/accounts/components/RecoveryKey.tsx +++ b/web/packages/accounts/components/RecoveryKey.tsx @@ -14,7 +14,7 @@ import { errorDialogAttributes } from "ente-base/components/utils/dialog"; import { useIsSmallWidth } from "ente-base/components/utils/hooks"; import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; import log from "ente-base/log"; -import { downloadString } from "ente-base/utils/web"; +import { saveStringAsFile } from "ente-base/utils/web"; import { t } from "i18next"; import { useCallback, useEffect, useState } from "react"; import { @@ -55,7 +55,7 @@ export const RecoveryKey: React.FC = ({ }, [open, handleLoadError]); const handleSaveClick = () => { - downloadRecoveryKeyMnemonic(recoveryKey!); + saveRecoveryKeyMnemonicAsFile(recoveryKey!); onClose(); }; @@ -117,5 +117,5 @@ export const RecoveryKey: React.FC = ({ const getUserRecoveryKeyMnemonic = async () => recoveryKeyToMnemonic(await getUserRecoveryKey()); -const downloadRecoveryKeyMnemonic = (key: string) => - downloadString(key, "ente-recovery-key.txt"); +const saveRecoveryKeyMnemonicAsFile = (key: string) => + saveStringAsFile(key, "ente-recovery-key.txt"); diff --git a/web/packages/base/utils/web.ts b/web/packages/base/utils/web.ts index cb45280990..f0120f4a35 100644 --- a/web/packages/base/utils/web.ts +++ b/web/packages/base/utils/web.ts @@ -2,13 +2,14 @@ * Download the asset at the given {@link url} to a file on the user's download * folder by appending a temporary element to the DOM. * - * @param url The URL that we want to download. See also - * {@link downloadAndRevokeObjectURL} and {@link downloadString}. The URL is - * revoked after initiating the download. + * @param url The URL that we want to download. The URL is revoked after + * initiating the download. * * @param fileName The name of downloaded file. + * + * See also {@link saveStringAsFile}. */ -export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { +export const saveAsFileAndRevokeObjectURL = (url: string, fileName: string) => { const a = document.createElement("a"); a.style.display = "none"; a.href = url; @@ -26,8 +27,8 @@ export const downloadAndRevokeObjectURL = (url: string, fileName: string) => { * * @param fileName The name of the file that gets saved. */ -export const downloadString = (s: string, fileName: string) => { +export const saveStringAsFile = (s: string, fileName: string) => { const file = new Blob([s], { type: "text/plain" }); const fileURL = URL.createObjectURL(file); - downloadAndRevokeObjectURL(fileURL, fileName); + saveAsFileAndRevokeObjectURL(fileURL, fileName); }; diff --git a/web/packages/new/photos/components/ImageEditorOverlay.tsx b/web/packages/new/photos/components/ImageEditorOverlay.tsx index 1175e8285e..ed0e84f13b 100644 --- a/web/packages/new/photos/components/ImageEditorOverlay.tsx +++ b/web/packages/new/photos/components/ImageEditorOverlay.tsx @@ -45,7 +45,7 @@ import type { ModalVisibilityProps } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; import { nameAndExtension } from "ente-base/file-name"; import log from "ente-base/log"; -import { downloadAndRevokeObjectURL } from "ente-base/utils/web"; +import { saveAsFileAndRevokeObjectURL } from "ente-base/utils/web"; import { downloadManager } from "ente-gallery/services/download"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; @@ -472,7 +472,7 @@ export const ImageEditorOverlay: React.FC = ({ if (!canvasRef.current) return; const f = await getEditedFile(); - downloadAndRevokeObjectURL(URL.createObjectURL(f), f.name); + saveAsFileAndRevokeObjectURL(URL.createObjectURL(f), f.name); }; const saveCopyToEnte = async () => { From bdfaf6dcd23868cda05e621334c016c971064e08 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 05:55:30 +0530 Subject: [PATCH 3/7] Unused (ft enum is now a number) --- web/packages/media/file-type.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/web/packages/media/file-type.ts b/web/packages/media/file-type.ts index 224507bfee..278d6fc781 100644 --- a/web/packages/media/file-type.ts +++ b/web/packages/media/file-type.ts @@ -21,15 +21,6 @@ export const FileType = { * containing both the parts. */ livePhoto: 2, - /** - * An unknown file type. - * - * The exact value here doesn't matter (and won't likely match what we get - * from remote). This instead is serving as a placeholder, forcing us to - * deal with the scenario that an EnteFile's type can be different from one - * of the above. - */ - other: 3, } as const; /** From 3ba6ecc3c265735ba0bdb1208884cc14d52a57c4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 06:11:30 +0530 Subject: [PATCH 4/7] Rename --- web/packages/gallery/services/upload/upload-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 9db7b88068..2738aa301d 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -1363,7 +1363,7 @@ const readLivePhoto = async ( fileStreamOrData: imageFileStreamOrData, thumbnail, hasStaticThumbnail, - } = await withThumbnail( + } = await augmentWithThumbnail( livePhotoAssets.image, // TODO: Update underlying type // @ts-ignore @@ -1402,7 +1402,7 @@ const readImageOrVideo = async ( fileTypeInfo: FileTypeInfo, ) => { const fileStream = await readUploadItem(uploadItem); - return withThumbnail(uploadItem, fileTypeInfo, fileStream); + return augmentWithThumbnail(uploadItem, fileTypeInfo, fileStream); }; /** @@ -1417,7 +1417,7 @@ const readImageOrVideo = async ( * Note: The `fileStream` in the returned {@link ThumbnailedFile} may be * different from the one passed to the function. */ -const withThumbnail = async ( +const augmentWithThumbnail = async ( uploadItem: UploadItem, fileTypeInfo: FileTypeInfo, fileStream: FileStream, From f6b6cfa4d0153030d80ad262ca85c6101d626706 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 06:21:02 +0530 Subject: [PATCH 5/7] Tweak --- web/apps/photos/src/utils/file/index.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index ae687d0c77..540e2e6306 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -237,14 +237,15 @@ const saveAsFile = async (file: EnteFile) => { /** * Save the given {@link blob} as a file in the user's download folder. */ -const saveBlobPartAsFile = async (blobPart: BlobPart, fileName: string) => { - const { mimeType } = await detectFileTypeInfo( - new File([blobPart], fileName), - ); - saveAsFileAndRevokeObjectURL( - URL.createObjectURL(new Blob([blobPart], { type: mimeType })), - fileName, +const saveBlobPartAsFile = async (blobPart: BlobPart, fileName: string) => + createTypedObjectURL(blobPart, fileName).then((url) => + saveAsFileAndRevokeObjectURL(url, fileName), ); + +const createTypedObjectURL = async (blobPart: BlobPart, fileName: string) => { + const blob = blobPart instanceof Blob ? blobPart : new Blob([blobPart]); + const { mimeType } = await detectFileTypeInfo(new File([blob], fileName)); + return URL.createObjectURL(new Blob([blob], { type: mimeType })); }; async function downloadFilesDesktop( @@ -330,11 +331,6 @@ export const getArchivedFiles = (files: EnteFile[]) => { return files.filter(isArchivedFile).map((file) => file.id); }; -export const createTypedObjectURL = async (blob: Blob, fileName: string) => { - const type = await detectFileTypeInfo(new File([blob], fileName)); - return URL.createObjectURL(new Blob([blob], { type: type.mimeType })); -}; - export const getUserOwnedFiles = (files: EnteFile[]) => { const user: User = getData("user"); if (!user?.id) { From 6e3a0b1b943c8296a9d6be78c1c2f7957aba9daf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 06:33:18 +0530 Subject: [PATCH 6/7] Restrict --- .../gallery/services/upload/upload-service.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 2738aa301d..bce8cc55b1 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -704,7 +704,7 @@ export const upload = async ( const { metadata, publicMagicMetadata } = await extractAssetMetadata( uploadAsset, - fileTypeInfo, + fileTypeInfo.fileType, lastModifiedMs, collection.id, parsedMetadataJSONMap, @@ -1061,7 +1061,7 @@ const extractAssetMetadata = async ( pathPrefix, externalParsedMetadata, }: UploadAsset, - fileTypeInfo: FileTypeInfo, + fileType: FileType, lastModifiedMs: number, collectionID: number, parsedMetadataJSONMap: Map, @@ -1072,7 +1072,6 @@ const extractAssetMetadata = async ( // @ts-ignore livePhotoAssets, pathPrefix, - fileTypeInfo, lastModifiedMs, collectionID, parsedMetadataJSONMap, @@ -1083,7 +1082,7 @@ const extractAssetMetadata = async ( uploadItem, pathPrefix, externalParsedMetadata, - fileTypeInfo, + fileType, lastModifiedMs, collectionID, parsedMetadataJSONMap, @@ -1093,23 +1092,17 @@ const extractAssetMetadata = async ( const extractLivePhotoMetadata = async ( livePhotoAssets: LivePhotoAssets, pathPrefix: UploadPathPrefix | undefined, - fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, parsedMetadataJSONMap: Map, worker: CryptoWorker, ) => { - const imageFileTypeInfo: FileTypeInfo = { - fileType: FileType.image, - // @ts-ignore - extension: fileTypeInfo.imageType, - }; const { metadata: imageMetadata, publicMagicMetadata } = await extractImageOrVideoMetadata( livePhotoAssets.image, pathPrefix, undefined, - imageFileTypeInfo, + FileType.image, lastModifiedMs, collectionID, parsedMetadataJSONMap, @@ -1136,14 +1129,13 @@ const extractImageOrVideoMetadata = async ( uploadItem: UploadItem, pathPrefix: UploadPathPrefix | undefined, externalParsedMetadata: ExternalParsedMetadata | undefined, - fileTypeInfo: FileTypeInfo, + fileType: FileType, lastModifiedMs: number, collectionID: number, parsedMetadataJSONMap: Map, worker: CryptoWorker, ) => { const fileName = uploadItemFileName(uploadItem); - const { fileType } = fileTypeInfo; let parsedMetadata: (ParsedMetadata & ExternalParsedMetadata) | undefined; if (fileType == FileType.image) { From c4f6ed693856f666f073b2215adf2932a3b17dc1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 06:44:19 +0530 Subject: [PATCH 7/7] Fix --- .../gallery/services/upload/upload-service.ts | 11 ++++++----- web/packages/media/file-type.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index bce8cc55b1..6a0cb9ba31 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -1000,8 +1000,9 @@ const readLivePhotoDetails = async ({ image, video }: LivePhotoAssets) => { return { fileTypeInfo: { fileType: FileType.livePhoto, - extension: `${img.fileTypeInfo.extension}+${vid.fileTypeInfo.extension}`, - imageType: img.fileTypeInfo.extension, + // Use the extension of the image component as the extension of the + // live photo. + extension: img.fileTypeInfo.extension, }, fileSize: img.fileSize + vid.fileSize, lastModifiedMs: img.lastModifiedMs, @@ -1357,9 +1358,9 @@ const readLivePhoto = async ( hasStaticThumbnail, } = await augmentWithThumbnail( livePhotoAssets.image, - // TODO: Update underlying type - // @ts-ignore - { extension: fileTypeInfo.imageType, fileType: FileType.image }, + // For live photos, the extension field in the file type info is the + // extension of the image component of the live photo. + { fileType: FileType.image, extension: fileTypeInfo.extension }, await readUploadItem(livePhotoAssets.image), ); const videoFileStreamOrData = await readUploadItem(livePhotoAssets.video); diff --git a/web/packages/media/file-type.ts b/web/packages/media/file-type.ts index 278d6fc781..c0deee74bb 100644 --- a/web/packages/media/file-type.ts +++ b/web/packages/media/file-type.ts @@ -36,11 +36,11 @@ export interface FileTypeInfo { /** * A lowercased, standardized extension for files of the current type. * - * TODO(MR): This in not valid for live photos. + * For live photos, this is set to the extension of the image component of + * the live photo. */ extension: string; mimeType?: string; - imageType?: string; } // list of format that were missed by type-detection for some files.