diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 62071ed114..5edfb77f6c 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -846,7 +846,17 @@ const Page: React.FC = () => { dispatch({ type: "addPendingVisibilityUpdate", fileID }); try { await updateFilesVisibility([file], visibility); - // TODO: Replace with file fetch? + // [Note: Interactive updates to file metadata] + // + // 1. Update the remote metadata. + // + // 2. Construct a fake a metadata object with the updates + // reflected in it. + // + // 3. The caller (eventually) triggers a remote sync in the + // background, but meanwhile uses this updated metadata. + // + // TODO(RE): Replace with file fetch? dispatch({ type: "unsyncedPrivateMagicMetadataUpdate", fileID, diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 73540f7ea8..17d568ea7c 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -1,17 +1,8 @@ -import { encryptMetadataJSON } from "ente-base/crypto"; -import { authenticatedRequestHeaders, ensureOk } from "ente-base/http"; -import { apiURL } from "ente-base/origins"; import { type Location } from "ente-base/types"; -import { - type EnteFile, - type EnteFile2, - type FileMagicMetadata, - type FilePrivateMagicMetadata, -} from "ente-media/file"; +import { type EnteFile, type EnteFile2 } from "ente-media/file"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; import { FileType } from "./file-type"; -import type { RemoteMagicMetadata } from "./magic-metadata"; /** * Information about the file that never changes post upload. @@ -489,155 +480,6 @@ export const fileCreationPhotoDate = (file: EnteFile) => 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 FilePrivateMagicMetadataData} containing - * the fields that we want to add or update. - * - * @returns An updated {@link FilePrivateMagicMetadataData} object containing the - * (decrypted) metadata updates we just made. This is effectively what we would - * get if we to ask the remote for the latest file for this ID, except we don't - * do an actual sync and instead reconstruct it piecemeal. - * - * [Note: Interactive updates to file metadata] - * - * This function updates the magic metadata on remote, and returns a magic - * metadata object with the updated (and decrypted) values, but it does not - * update the state of the file objects in our databases. - * - * The caller needs to ensure that we subsequently sync with remote to fetch the - * updates as part of the diff and update the {@link EnteFile} that is persisted - * in our local db. - * - * This partial update approach is used because a full sync requires multiple - * API calls, which can cause a slow experience for interactive operations (e.g. - * archiving a file). So this function does not immediately perform the sync, - * but instead expects the caller to arrange for an eventual delayed sync in the - * background without waiting for it to complete. - * - * Returning a modified in-memory object is essential because in addition to the - * updated metadata itself, the metadatum (See: [Note: Metadatum]) contain a - * version field that is incremented for each change. So if we were not to - * update the version, and if the user were to perform another operation on that - * file before the asynchronous remote sync completes, the client will send a - * stale version of the metadata, and remote will reject the update. - * - * The overall sequence is thus: - * - * 1. This function modifies the remote metadata. - * - * 2. It returns a metadata object with the updates reflected in it. - * - * 3. The caller (eventually) triggers a remote sync in the background, but - * meanwhile uses this updated metadata. - */ -export const updateRemotePrivateMagicMetadata = async ( - file: EnteFile, - metadataUpdates: Partial, -): Promise => { - const existingMetadata = file.magicMetadata?.data; - - const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates }; - - const metadataVersion = file.magicMetadata?.version ?? 1; - - const updateRequest = await updateMagicMetadataRequest( - file, - updatedMetadata, - metadataVersion, - ); - - const updatedEnvelope = updateRequest.metadataList[0]!.magicMetadata; - - await putFilesPrivateMagicMetadata(updateRequest); - - // See: [Note: Interactive updates to file metadata] - - // Use the updated envelope we sent as a starting point for the metadata we - // will use for the updated file. - const updatedMagicMetadata = updatedEnvelope as FileMagicMetadata; - // The correct version will come in the updated EnteFile we get in the - // response of the /diff. Temporarily bump it to reflect our latest edit. - updatedMagicMetadata.version = metadataVersion + 1; - // Set the contents (data) to the updated metadata contents we just PUT. - updatedMagicMetadata.data = updatedMetadata; - - return updatedMagicMetadata; -}; - -/** - * The shape of the JSON body payload expected by the APIs that update the - * public and private magic metadata fields associated with a file. - */ -interface UpdateMagicMetadataRequest { - /** The list of (file id, new magic metadata) pairs to update */ - metadataList: { - /** File ID */ - id: number; - /** The new metadata to use */ - magicMetadata: RemoteMagicMetadata; - }[]; -} - -/** - * Construct an remote update request payload from the public or private magic - * metadata JSON object for a {@link file}, using the provided - * {@link encryptMetadataF} function to encrypt the JSON. - */ -const updateMagicMetadataRequest = async ( - file: EnteFile, - metadata: FilePrivateMagicMetadataData | FilePublicMagicMetadataData, - metadataVersion: number, -): Promise => { - // Drop all null or undefined values to obtain the syncable entries. - // See: [Note: Optional magic metadata keys]. - const validEntries = Object.entries(metadata).filter( - ([, v]) => v !== null && v !== undefined, - ); - - const { encryptedData, decryptionHeader } = await encryptMetadataJSON( - Object.fromEntries(validEntries), - file.key, - ); - - return { - metadataList: [ - { - id: file.id, - magicMetadata: { - version: metadataVersion, - count: validEntries.length, - data: encryptedData, - header: decryptionHeader, - }, - }, - ], - }; -}; - -/** - * 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. - */ -const putFilesPrivateMagicMetadata = async ( - request: UpdateMagicMetadataRequest, -) => - ensureOk( - await fetch(await apiURL("/files/magic-metadata"), { - method: "PUT", - headers: await authenticatedRequestHeaders(), - body: JSON.stringify(request), - }), - ); - /** * Return the GPS coordinates (if any) present in the given {@link EnteFile}. */