Use accessor

This commit is contained in:
Manav Rathi
2025-06-19 11:20:56 +05:30
parent 2c1dd14098
commit d9941a7711
16 changed files with 81 additions and 67 deletions

View File

@@ -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<FileListWithViewerProps> = ({
const handleDownload = useCallback(
(file: EnteFile) => {
const setSingleFileDownloadProgress =
setFilesDownloadProgressAttributesCreator!(file.metadata.title);
setFilesDownloadProgressAttributesCreator!(fileFileName(file));
void downloadSingleFile(file, setSingleFileDownloadProgress);
},
[setFilesDownloadProgressAttributesCreator],

View File

@@ -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);

View File

@@ -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(

View File

@@ -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`);

View File

@@ -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<FileInfoProps> = ({
{...rawExifVisibilityProps}
onInfoClose={onClose}
tags={exif?.tags}
fileName={file.metadata.title}
fileName={fileFileName(file)}
/>
</FileInfoSidebar>
);
@@ -739,7 +740,7 @@ const FileName: React.FC<FileNameProps> = ({
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);

View File

@@ -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)) {

View File

@@ -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<RenderableSourceURLs> => {
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) {

View File

@@ -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;
};

View File

@@ -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;
/**

View File

@@ -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) {

View File

@@ -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 (

View File

@@ -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<ImageEditorOverlayProps> = ({
const getEditedFile = async () => {
const originalSizeCanvas = originalSizeCanvasRef.current!;
const originalFileName = file.metadata.title;
const originalFileName = fileFileName(file);
return canvasToFile(originalSizeCanvas, originalFileName, mimeType);
};

View File

@@ -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

View File

@@ -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,

View File

@@ -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}`);

View File

@@ -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