diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index 5e7680e3e6..73064593de 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -7,6 +7,7 @@ import { } from "ente-gallery/components/viewer/FileViewer"; import type { Collection } from "ente-media/collection"; import { EnteFile } from "ente-media/file"; +import { fileFileName } from "ente-media/file-metadata"; import { moveToTrash } from "ente-new/photos/services/collection"; import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; import { t } from "i18next"; @@ -139,7 +140,7 @@ export const FileListWithViewer: React.FC = ({ const handleDownload = useCallback( (file: EnteFile) => { const setSingleFileDownloadProgress = - setFilesDownloadProgressAttributesCreator!(file.metadata.title); + setFilesDownloadProgressAttributesCreator!(fileFileName(file)); void downloadSingleFile(file, setSingleFileDownloadProgress); }, [setFilesDownloadProgressAttributesCreator], diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx index 03b3faf050..73a7053a2f 100644 --- a/web/apps/photos/src/components/FixCreationTime.tsx +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -20,6 +20,7 @@ import { fileLogID, type EnteFile } from "ente-media/file"; import { decryptPublicMagicMetadata, fileCreationPhotoDate, + fileFileName, updateRemotePublicMagicMetadata, type ParsedMetadataDate, } from "ente-media/file-metadata"; @@ -324,7 +325,7 @@ const updateEnteFileDate = async ( }; } else if (enteFile.metadata.fileType == FileType.image) { const blob = await downloadManager.fileBlob(enteFile); - const file = new File([blob], enteFile.metadata.title); + const file = new File([blob], fileFileName(enteFile)); const { DateTimeOriginal, DateTimeDigitized, MetadataDate, DateTime } = await extractExifDates(file); diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index a3afa279a3..b8a8e8d1f6 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -13,7 +13,11 @@ import { FileMagicMetadataProps, FileWithUpdatedMagicMetadata, } from "ente-media/file"; -import { ItemVisibility, isArchivedFile } from "ente-media/file-metadata"; +import { + ItemVisibility, + fileFileName, + isArchivedFile, +} from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; import { @@ -47,9 +51,10 @@ export type FileOp = 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(file.metadata.title, fileBlob); + await decodeLivePhoto(fileName, fileBlob); const image = new File([imageData], imageFileName); const imageType = await detectFileTypeInfo(image); const tempImageURL = URL.createObjectURL( @@ -67,11 +72,11 @@ export async function downloadFile(file: EnteFile) { downloadAndRevokeObjectURL(tempVideoURL, videoFileName); } else { const fileType = await detectFileTypeInfo( - new File([fileBlob], file.metadata.title), + new File([fileBlob], fileName), ); fileBlob = new Blob([fileBlob], { type: fileType.mimeType }); const tempURL = URL.createObjectURL(fileBlob); - downloadAndRevokeObjectURL(tempURL, file.metadata.title); + downloadAndRevokeObjectURL(tempURL, fileName); } } catch (e) { log.error("failed to download file", e); @@ -276,11 +281,12 @@ async function downloadFileDesktop( const fs = electron.fs; const stream = await downloadManager.fileStream(file); + const fileName = fileFileName(file); if (file.metadata.fileType === FileType.livePhoto) { const fileBlob = await new Response(stream).blob(); const { imageFileName, imageData, videoFileName, videoData } = - await decodeLivePhoto(file.metadata.title, fileBlob); + await decodeLivePhoto(fileName, fileBlob); const imageExportName = await safeFileName( downloadDir, imageFileName, @@ -311,7 +317,7 @@ async function downloadFileDesktop( } else { const fileExportName = await safeFileName( downloadDir, - file.metadata.title, + fileName, fs.exists, ); await writeStream( diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index ceb31acbc7..471a54cc5f 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -7,6 +7,7 @@ import { matchJSONMetadata, metadataJSONMapKeyForJSON, } from "ente-gallery/services/upload/metadata-json"; +import { fileFileName } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { getLocalCollections } from "ente-new/photos/services/collections"; import { @@ -233,9 +234,8 @@ async function thumbnailGenerationFailedFilesCheck(expectedState) { } }, ); - const fileNamesWithStaticThumbnail = uniqueFilesWithStaticThumbnail.map( - (file) => file.metadata.title, - ); + const fileNamesWithStaticThumbnail = + uniqueFilesWithStaticThumbnail.map(fileFileName); if ( expectedState.thumbnail_generation_failure.count < @@ -275,9 +275,7 @@ async function livePhotoClubbingCheck(expectedState) { } }); - const livePhotoFileNames = uniqueLivePhotos.map( - (file) => file.metadata.title, - ); + const livePhotoFileNames = uniqueLivePhotos.map(fileFileName); if (expectedState.live_photo.count !== livePhotoFileNames.length) { throw Error( @@ -300,7 +298,7 @@ async function exifDataParsingCheck(expectedState) { const files = await getLocalFiles(); Object.entries(expectedState.exif).map(([fileName, exifValues]) => { const matchingFile = files.find( - (file) => file.metadata.title === fileName, + (file) => fileFileName(file) == fileName, ); if (!matchingFile) { throw Error(`exifDataParsingCheck failed , ${fileName} missing`); @@ -340,7 +338,7 @@ async function fileDimensionExtractionCheck(expectedState) { Object.entries(expectedState.file_dimensions).map( ([fileName, dimensions]) => { const matchingFile = files.find( - (file) => file.metadata.title === fileName, + (file) => fileFileName(file) == fileName, ); if (!matchingFile) { throw Error( @@ -366,7 +364,7 @@ async function googleMetadataReadingCheck(expectedState) { const files = await getLocalFiles(); Object.entries(expectedState.google_import).map(([fileName, metadata]) => { const matchingFile = files.find( - (file) => file.metadata.title === fileName, + (file) => fileFileName(file) == fileName, ); if (!matchingFile) { throw Error(`exifDataParsingCheck failed , ${fileName} missing`); diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index 62e169aca4..d43b42db7d 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -65,6 +65,7 @@ import { type EnteFile } from "ente-media/file"; import { fileCaption, fileCreationPhotoDate, + fileFileName, fileLocation, filePublicMagicMetadata, updateRemotePublicMagicMetadata, @@ -386,7 +387,7 @@ export const FileInfo: React.FC = ({ {...rawExifVisibilityProps} onInfoClose={onClose} tags={exif?.tags} - fileName={file.metadata.title} + fileName={fileFileName(file)} /> ); @@ -739,7 +740,7 @@ const FileName: React.FC = ({ const { show: showRename, props: renameVisibilityProps } = useModalVisibility(); - const fileName = file.metadata.title; + const fileName = fileFileName(file); const handleRename = async (newFileName: string) => { const updatedFile = await changeFileName(file, newFileName); diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 357c3d2e6b..12f4d6e590 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -37,7 +37,11 @@ import { type FileInfoProps, } from "ente-gallery/components/FileInfo"; import type { Collection } from "ente-media/collection"; -import { fileVisibility, ItemVisibility } from "ente-media/file-metadata"; +import { + fileFileName, + fileVisibility, + ItemVisibility, +} from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import type { EnteFile } from "ente-media/file.js"; import { isHEICExtension, needsJPEGConversion } from "ente-media/formats"; @@ -1253,7 +1257,7 @@ const fileIsEditableImage = (file: EnteFile) => { // Only images are editable. if (file.metadata.fileType !== FileType.image) return false; - const extension = lowercaseExtension(file.metadata.title); + const extension = lowercaseExtension(fileFileName(file)); // Assume it is editable; let isRenderable = true; if (extension && needsJPEGConversion(extension)) { diff --git a/web/packages/gallery/services/download.ts b/web/packages/gallery/services/download.ts index 20f8281b6e..bfedb1ca84 100644 --- a/web/packages/gallery/services/download.ts +++ b/web/packages/gallery/services/download.ts @@ -16,6 +16,7 @@ import log from "ente-base/log"; import { apiURL, customAPIOrigin } from "ente-base/origins"; import { ensureAuthToken } from "ente-base/token"; import type { EnteFile } from "ente-media/file"; +import { fileFileName } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; import { playableVideoURL, renderableImageBlob } from "./convert"; @@ -612,7 +613,7 @@ const createRenderableSourceURLs = async ( ): Promise => { const originalFileURL = await originalFileURLPromise; const fileBlob = await fetch(originalFileURL).then((res) => res.blob()); - const fileName = file.metadata.title; + const fileName = fileFileName(file); const fileType = file.metadata.fileType; switch (fileType) { diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 4dbbfb581a..e8c2e5b324 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -35,6 +35,7 @@ import type { FilePublicMagicMetadataProps, } from "ente-media/file"; import { + fileFileName, metadataHash, type FileMetadata, type ParsedMetadata, @@ -642,7 +643,7 @@ export const upload = async ( ); const matches = existingFiles.filter((file) => - areFilesSame(file.metadata, metadata), + areFilesSame(file, metadata), ); const anyMatch = matches.length > 0 ? matches[0] : undefined; @@ -1220,17 +1221,24 @@ const computeHash = async (uploadItem: UploadItem, worker: CryptoWorker) => { }; /** - * Return true if the two files, as represented by their metadata, are same. + * Return true if the given file is the same as provided metadata. * * Note that the metadata includes the hash of the file's contents (when * available), so this also in effect compares the contents of the files, not * just the "meta" information about them. */ -const areFilesSame = (f: FileMetadata, g: FileMetadata) => { - if (f.fileType !== g.fileType || f.title !== g.title) return false; +const areFilesSame = (fFile: EnteFile, gm: FileMetadata) => { + const fm = fFile.metadata; - const fh = metadataHash(f); - const gh = metadataHash(g); + // File name is different + if (fileFileName(fFile) !== gm.title) return false; + + // File type is different + if (fm.fileType !== gm.fileType) return false; + + // Name and type is same, compare hash. + const fh = metadataHash(fm); + const gh = metadataHash(gm); return fh && gh && fh == gh; }; diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index ce1ac92ff3..926d793370 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -95,7 +95,8 @@ export interface FileMetadata { /** * The name of the file (including its extension). * - * See: [Note: File name for local EnteFile objects] + * Don't use this property directly, use {@link fileFileName} instead which + * takes into account subsequent edits too. */ title: string; /** diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index 2873fa9a85..addb9547c3 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -8,7 +8,7 @@ import log from "ente-base/log"; import { nullishToBlank, nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; import { ignore } from "./collection"; -import { FileMetadata, ItemVisibility } from "./file-metadata"; +import { fileFileName, FileMetadata, ItemVisibility } from "./file-metadata"; import { FileType } from "./file-type"; import { decryptMagicMetadata, RemoteMagicMetadata } from "./magic-metadata"; @@ -588,14 +588,16 @@ export interface EncryptedTrashItem { export type Trash = TrashItem[]; /** + * A short identifier for a file in log messages. + * + * e.g. "file flower.png (827233681)" + * * @returns a string to use as an identifier when logging information about the * given {@link file}. The returned string contains the file name (for ease of * debugging) and the file ID (for exactness). */ export const fileLogID = (file: EnteFile) => - // TODO: Remove this when file/metadata types have optionality annotations. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `file ${file.metadata.title ?? "-"} (${file.id})`; + `file ${fileFileName(file)} (${file.id})`; /** * Return the date when the file will be deleted permanently. Only valid for @@ -819,23 +821,13 @@ const transformDecryptedMetadataJSON = ( * * This function updates a single file, see {@link mergeMetadata} for a * convenience function to run it on an array of files. - * - * [Note: File name for local EnteFile objects] - * - * The title property in a file's metadata is the original file's name. The - * metadata of a file cannot be edited. So if later on the file's name is - * changed, then the edit is stored in the `editedName` property of the public - * metadata of the file. - * - * This function merges these edits onto the file object that we use locally. - * Effectively, post this step, the file's metadata.title can be used in lieu of - * its filename. */ export const mergeMetadata1 = (file: EnteFile): EnteFile => { const mutableMetadata = file.pubMagicMetadata?.data; if (mutableMetadata) { const { editedTime, editedName, lat, long } = mutableMetadata; if (editedTime) file.metadata.creationTime = editedTime; + // Not needed, use fileFileName. if (editedName) file.metadata.title = editedName; // Use (lat, long) only if both are present and nonzero. if (lat && long) { diff --git a/web/packages/new/photos/components/Export.tsx b/web/packages/new/photos/components/Export.tsx index ed5b79a834..d008f00289 100644 --- a/web/packages/new/photos/components/Export.tsx +++ b/web/packages/new/photos/components/Export.tsx @@ -37,6 +37,7 @@ import { formattedNumber } from "ente-base/i18n"; import { formattedDateTime } from "ente-base/i18n-date"; import log from "ente-base/log"; import { type EnteFile } from "ente-media/file"; +import { fileFileName } from "ente-media/file-metadata"; import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; import { CustomError } from "ente-shared/error"; import { t } from "i18next"; @@ -601,7 +602,7 @@ const ExportPendingListItem: React.FC< const { pendingFiles, collectionNameByID } = data; const file = pendingFiles[index]!; - const fileName = file.metadata.title; + const fileName = fileFileName(file); const collectionName = collectionNameByID.get(file.collectionID); return ( diff --git a/web/packages/new/photos/components/ImageEditorOverlay.tsx b/web/packages/new/photos/components/ImageEditorOverlay.tsx index 4db3ef883f..1175e8285e 100644 --- a/web/packages/new/photos/components/ImageEditorOverlay.tsx +++ b/web/packages/new/photos/components/ImageEditorOverlay.tsx @@ -49,6 +49,7 @@ import { downloadAndRevokeObjectURL } 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"; +import { fileFileName } from "ente-media/file-metadata"; import { getLocalCollections } from "ente-new/photos/services/collections"; import { t } from "i18next"; import React, { @@ -463,7 +464,7 @@ export const ImageEditorOverlay: React.FC = ({ const getEditedFile = async () => { const originalSizeCanvas = originalSizeCanvasRef.current!; - const originalFileName = file.metadata.title; + const originalFileName = fileFileName(file); return canvasToFile(originalSizeCanvas, originalFileName, mimeType); }; diff --git a/web/packages/new/photos/services/export-migration.ts b/web/packages/new/photos/services/export-migration.ts index 99fb6630c0..7b99283389 100644 --- a/web/packages/new/photos/services/export-migration.ts +++ b/web/packages/new/photos/services/export-migration.ts @@ -11,6 +11,7 @@ import { exportMetadataDirectoryName } from "ente-gallery/export-dirs"; import { downloadManager } from "ente-gallery/services/download"; import type { Collection } from "ente-media/collection"; import { mergeMetadata, type EnteFile } from "ente-media/file"; +import { fileFileName } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; import { getLocalCollections } from "ente-new/photos/services/collections"; @@ -267,7 +268,7 @@ async function migrateFiles( exportMetadataDirectoryName, ); - const oldFileName = `${file.id}_${oldSanitizeName(file.metadata.title)}`; + const oldFileName = `${file.id}_${oldSanitizeName(fileFileName(file))}`; // @ts-ignore const oldFilePath = joinPath(collectionPath, oldFileName); const oldFileMetadataPath = joinPath( @@ -278,7 +279,7 @@ async function migrateFiles( const newFileName = await safeFileName( // @ts-ignore collectionPath, - file.metadata.title, + fileFileName(file), fs.exists, ); // @ts-ignore @@ -367,6 +368,7 @@ async function getFileExportNamesFromExportedFiles( () => `collection path for ${file.collectionID} is ${collectionPath}`, ); + const fileName = fileFileName(file); let fileExportName: string; /* For Live Photos we need to download the file to get the image and video name @@ -374,7 +376,7 @@ async function getFileExportNamesFromExportedFiles( if (file.metadata.fileType === FileType.livePhoto) { const fileBlob = await downloadManager.fileBlob(file); const { imageFileName, videoFileName } = await decodeLivePhoto( - file.metadata.title, + fileName, fileBlob, ); const imageExportName = getUniqueFileExportNameForMigration( @@ -397,13 +399,12 @@ async function getFileExportNamesFromExportedFiles( fileExportName = getUniqueFileExportNameForMigration( // @ts-ignore collectionPath, - file.metadata.title, + fileName, usedFilePaths, ); } log.debug( - () => - `file export name for ${file.metadata.title} is ${fileExportName}`, + () => `file export name for ${fileName} is ${fileExportName}`, ); exportedFileNames = { // @ts-ignore diff --git a/web/packages/new/photos/services/export.ts b/web/packages/new/photos/services/export.ts index e80e427366..c14472c35b 100644 --- a/web/packages/new/photos/services/export.ts +++ b/web/packages/new/photos/services/export.ts @@ -15,8 +15,8 @@ import { import { downloadManager } from "ente-gallery/services/download"; import { writeStream } from "ente-gallery/utils/native-stream"; import type { Collection } from "ente-media/collection"; -import { mergeMetadata, type EnteFile } from "ente-media/file"; -import { fileLocation } from "ente-media/file-metadata"; +import { fileLogID, mergeMetadata, type EnteFile } from "ente-media/file"; +import { fileFileName, fileLocation } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; import { @@ -680,9 +680,7 @@ class ExportService { try { for (const file of files) { log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( + `exporting ${fileLogID(file)} from collection ${collectionIDNameMap.get( file.collectionID, )}`, ); @@ -726,15 +724,13 @@ class ExportService { ); incrementSuccess(); log.info( - `exporting file ${file.metadata.title} with id ${ - file.id - } from collection ${collectionIDNameMap.get( + `exporting ${fileLogID(file)} from collection ${collectionIDNameMap.get( file.collectionID, )} successful`, ); } catch (e) { incrementFailed(); - log.error("export failed for a file", e); + log.error(`export failed for a ${fileLogID(file)}`, e); if ( // @ts-ignore e.message === @@ -1029,7 +1025,7 @@ class ExportService { } else { const fileExportName = await safeFileName( collectionExportPath, - file.metadata.title, + fileFileName(file), electron.fs.exists, ); await this.saveMetadataFile( @@ -1064,7 +1060,7 @@ class ExportService { ) { const fs = ensureElectron().fs; const fileBlob = await new Response(fileStream).blob(); - const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); + const livePhoto = await decodeLivePhoto(fileFileName(file), fileBlob); const imageExportName = await safeFileName( collectionExportPath, livePhoto.imageFileName, diff --git a/web/packages/new/photos/services/ml/blob.ts b/web/packages/new/photos/services/ml/blob.ts index 0039fa4091..9e79634c0d 100644 --- a/web/packages/new/photos/services/ml/blob.ts +++ b/web/packages/new/photos/services/ml/blob.ts @@ -9,6 +9,7 @@ import { } from "ente-gallery/services/upload"; import { readStream } from "ente-gallery/utils/native-stream"; import type { EnteFile } from "ente-media/file"; +import { fileFileName } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; @@ -109,7 +110,7 @@ const fetchRenderableUploadItemBlob = async ( return undefined; } const blob = await readNonVideoUploadItem(uploadItem, electron); - return renderableImageBlob(blob, file.metadata.title); + return renderableImageBlob(blob, fileFileName(file)); } }; @@ -170,12 +171,12 @@ export const fetchRenderableEnteFileBlob = async ( if (fileType == FileType.livePhoto) { const { imageFileName, imageData } = await decodeLivePhoto( - file.metadata.title, + fileFileName(file), originalFileBlob, ); return renderableImageBlob(new Blob([imageData]), imageFileName); } else if (fileType == FileType.image) { - return await renderableImageBlob(originalFileBlob, file.metadata.title); + return await renderableImageBlob(originalFileBlob, fileFileName(file)); } else { // A layer above us should've already filtered these out. throw new Error(`Cannot index unsupported file type ${fileType}`); diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index e12aa4bb80..2ee8e51a82 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -8,6 +8,7 @@ import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { fileCreationPhotoDate, + fileFileName, fileLocation, filePublicMagicMetadata, } from "ente-media/file-metadata"; @@ -178,7 +179,7 @@ const fileNameSuggestion = ( const sn = Number(s) || undefined; const fileIDs = files - .filter(({ id, metadata }) => id === sn || re.test(metadata.title)) + .filter((f) => f.id === sn || re.test(fileFileName(f))) .map((f) => f.id); return fileIDs.length