This commit is contained in:
Manav Rathi
2025-06-20 17:44:26 +05:30
parent 012a3bef0a
commit d9fa30dbef
2 changed files with 12 additions and 160 deletions

View File

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

View File

@@ -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<FilePrivateMagicMetadataData>,
): Promise<FilePrivateMagicMetadata> => {
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<UpdateMagicMetadataRequest> => {
// 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}.
*/