From 1f8fa69f8bdab8b35148e7efa166a56ae211dce8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 13 Mar 2025 09:27:03 +0530 Subject: [PATCH] Towards new layering --- web/apps/photos/src/utils/file/index.ts | 7 +- .../gallery/components/viewer/FileViewer.tsx | 3 +- .../gallery/services/magic-metadata.ts | 37 +---- web/packages/media/file-metadata.ts | 128 ++++++++++++++---- .../new/photos/components/gallery/reducer.ts | 3 +- 5 files changed, 104 insertions(+), 74 deletions(-) diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index f827b6ea44..8e92807c97 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -4,10 +4,7 @@ import { type Electron } from "@/base/types/ipc"; import { downloadAndRevokeObjectURL } from "@/base/utils/web"; import { downloadManager } from "@/gallery/services/download"; import { updateFileMagicMetadata } from "@/gallery/services/file"; -import { - isArchivedFile, - updateMagicMetadata, -} from "@/gallery/services/magic-metadata"; +import { updateMagicMetadata } from "@/gallery/services/magic-metadata"; import { detectFileTypeInfo } from "@/gallery/utils/detect-type"; import { writeStream } from "@/gallery/utils/native-stream"; import { @@ -15,7 +12,7 @@ import { FileMagicMetadataProps, FileWithUpdatedMagicMetadata, } from "@/media/file"; -import { ItemVisibility } from "@/media/file-metadata"; +import { ItemVisibility, isArchivedFile } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import { deleteFromTrash, moveToTrash } from "@/new/photos/services/collection"; diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 67abf6e91d..9e150f989c 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -18,7 +18,7 @@ import { type FileInfoProps, } from "@/gallery/components/FileInfo"; import type { Collection } from "@/media/collection"; -import { ItemVisibility } from "@/media/file-metadata"; +import { fileVisibility, ItemVisibility } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; import type { EnteFile } from "@/media/file.js"; import { isHEICExtension, needsJPEGConversion } from "@/media/formats"; @@ -53,7 +53,6 @@ import React, { useRef, useState, } from "react"; -import { fileVisibility } from "../../services/magic-metadata"; import { fileInfoExifForFile, updateItemDataAlt, diff --git a/web/packages/gallery/services/magic-metadata.ts b/web/packages/gallery/services/magic-metadata.ts index 0d9b9fe134..a305d9ba53 100644 --- a/web/packages/gallery/services/magic-metadata.ts +++ b/web/packages/gallery/services/magic-metadata.ts @@ -3,41 +3,8 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { sharedCryptoWorker } from "@/base/crypto"; import type { Collection } from "@/media/collection"; -import { fileLogID, type EnteFile, type MagicMetadataCore } from "@/media/file"; -import { - ItemVisibility, - type PrivateMagicMetadata, -} from "@/media/file-metadata"; - -/** - * Return the magic metadata for an {@link EnteFile}. - * - * We are not expected to be in a scenario where the file gets to the UI without - * having its magic metadata decrypted, so this function is a sanity - * check and should be a no-op in usually. It'll throw if it finds its - * assumptions broken. Once the types have been refactored this entire - * check/cast shouldn't be needed, and this should become a trivial accessor. - */ -export const fileMagicMetadata = (file: EnteFile) => { - if (!file.magicMetadata) return undefined; - if (typeof file.magicMetadata.data == "string") { - throw new Error( - `Magic metadata for ${fileLogID(file)} had not been decrypted even when the file reached the UI layer`, - ); - } - // This cast is unavoidable in the current setup. We need to refactor the - // types so that this cast in not needed. - return file.magicMetadata.data as PrivateMagicMetadata; -}; - -/** - * Return the {@link ItemVisibility} for the given {@link file}. - */ -export const fileVisibility = (file: EnteFile) => - fileMagicMetadata(file)?.visibility; - -export const isArchivedFile = (item: EnteFile) => - fileVisibility(item) === ItemVisibility.archived; +import { type MagicMetadataCore } from "@/media/file"; +import { ItemVisibility } from "@/media/file-metadata"; export const isArchivedCollection = (item: Collection) => { if (!item) { diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 44936d9af5..7d7de527f3 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -152,6 +152,8 @@ export interface Metadata { * - Unlike {@link PublicMagicMetadata}, this is only available to the owner of * the file. * + * [Note: Private magic metadata is called magic metadata on remote] + * * For historical reasons, the unqualified phrase "magic metadata" in various * APIs refers to the (this) private metadata, even though the mutable public * metadata is the much more frequently used of the two. See: [Note: Metadatum]. @@ -294,6 +296,29 @@ const PublicMagicMetadata = z }) .passthrough(); +/** + * Return the private magic metadata for an {@link EnteFile}. + * + * We are not expected to be in a scenario where the file gets to the UI without + * having its private magic metadata decrypted, so this function is a sanity + * check and should be a no-op in usually. It'll throw if it finds its + * assumptions broken. Once the types have been refactored this entire + * check/cast shouldn't be needed, and this should become a trivial accessor. + */ +export const filePrivateMagicMetadata = (file: EnteFile) => { + // TODO: Audit the types. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!file.magicMetadata) return undefined; + if (typeof file.magicMetadata.data == "string") { + throw new Error( + `Private magic metadata for ${fileLogID(file)} had not been decrypted even when the file reached the UI layer`, + ); + } + // This cast is unavoidable in the current setup. We need to refactor the + // types so that this cast in not needed. + return file.magicMetadata.data as PrivateMagicMetadata; +}; + /** * Return the public magic metadata for an {@link EnteFile}. * @@ -410,6 +435,35 @@ export const fileCreationPhotoDate = ( file.metadata.creationTime, ); +/** + * Update the private magic metadata associated with a file on remote. + * + * @param file The {@link EnteFile} whose public magic metadata we want to + * update. + * + * @param metadataUpdates A subset of {@link PrivateMagicMetadata} containing the + * fields that we want to add or update. + */ +export const updateRemotePrivateMagicMetadata = async ( + file: EnteFile, + metadataUpdates: Partial, +) => { + const existingMetadata = filePrivateMagicMetadata(file); + + const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates }; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const metadataVersion = file.magicMetadata?.version ?? 1; + + const updateRequest = await updateMagicMetadataRequest( + file, + updatedMetadata, + metadataVersion, + ); + + return putFilesPrivateMagicMetadata(updateRequest); +}; + /** * Update the public magic metadata associated with a file on remote. * @@ -553,13 +607,14 @@ const updateMagicMetadataRequest = async ( }; /** - * Update the magic metadata for a list of files. + * Update the (private) magic metadata for a list of files. + * + * See: [Note: Private magic metadata is called magic metadata on remote] * * @param request The list of file ids and the updated encrypted magic metadata * associated with each of them. */ -// TODO: Remove export once this is used. -export const putFilesMagicMetadata = async ( +const putFilesPrivateMagicMetadata = async ( request: UpdateMagicMetadataRequest, ) => ensureOk( @@ -587,6 +642,46 @@ const putFilesPublicMagicMetadata = async ( }), ); +/** + * Return the {@link ItemVisibility} for the given {@link file}. + */ +export const fileVisibility = (file: EnteFile) => + filePrivateMagicMetadata(file)?.visibility; + +/** + * Return `true` if the {@link ItemVisibility} of the given {@link file} is + * archived. + */ +export const isArchivedFile = (item: EnteFile) => + fileVisibility(item) === ItemVisibility.archived; + +/** + * Return the GPS coordinates (if any) present in the given {@link EnteFile}. + */ +export const fileLocation = (file: EnteFile): Location | undefined => { + // TODO: EnteFile types. Need to verify that metadata itself, and + // metadata.lat/lng can not be null (I think they likely can, if so need to + // update the types). Need to supress the linter meanwhile. + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!file.metadata) return undefined; + + const latitude = nullToUndefined(file.metadata.latitude); + const longitude = nullToUndefined(file.metadata.longitude); + + if (latitude === undefined || longitude === undefined) return undefined; + if (Number.isNaN(latitude) || Number.isNaN(longitude)) return undefined; + + return { latitude, longitude }; +}; + +/** + * Return the caption, aka "description", (if any) attached to the given + * {@link EnteFile}. + */ +export const fileCaption = (file: EnteFile): string | undefined => + filePublicMagicMetadata(file)?.caption; + /** * Metadata about a file extracted from various sources (like Exif) when * uploading it into Ente. @@ -835,30 +930,3 @@ export const createPhotoDate = ( return new Date(dateLike / 1000); } }; - -/** - * Return the GPS coordinates (if any) present in the given {@link EnteFile}. - */ -export const fileLocation = (file: EnteFile): Location | undefined => { - // TODO: EnteFile types. Need to verify that metadata itself, and - // metadata.lat/lng can not be null (I think they likely can, if so need to - // update the types). Need to supress the linter meanwhile. - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!file.metadata) return undefined; - - const latitude = nullToUndefined(file.metadata.latitude); - const longitude = nullToUndefined(file.metadata.longitude); - - if (latitude === undefined || longitude === undefined) return undefined; - if (Number.isNaN(latitude) || Number.isNaN(longitude)) return undefined; - - return { latitude, longitude }; -}; - -/** - * Return the caption, aka "description", (if any) attached to the given - * {@link EnteFile}. - */ -export const fileCaption = (file: EnteFile): string | undefined => - filePublicMagicMetadata(file)?.caption; diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index 31d0c2fde9..4596f9cc0a 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -1,6 +1,5 @@ import { isArchivedCollection, - isArchivedFile, isPinnedCollection, } from "@/gallery/services/magic-metadata"; import { @@ -10,7 +9,7 @@ import { } from "@/media/collection"; import type { EnteFile } from "@/media/file"; import { mergeMetadata } from "@/media/file"; -import { ItemVisibility } from "@/media/file-metadata"; +import { isArchivedFile, ItemVisibility } from "@/media/file-metadata"; import { createCollectionNameByID, isHiddenCollection,