diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
deleted file mode 100644
index 636ecc8b8b..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import log from "@/base/log";
-import type { ParsedMetadataDate } from "@/media/file-metadata";
-import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
-import { EnteFile } from "@/new/photos/types/file";
-import { FlexWrapper } from "@ente/shared/components/Container";
-import { formatDate, formatTime } from "@ente/shared/time/format";
-import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
-import { useState } from "react";
-import {
- changeFileCreationTime,
- updateExistingFilePubMetadata,
-} from "utils/file";
-import InfoItem from "./InfoItem";
-
-export function RenderCreationTime({
- shouldDisableEdits,
- file,
- scheduleUpdate,
-}: {
- shouldDisableEdits: boolean;
- file: EnteFile;
- scheduleUpdate: () => void;
-}) {
- const [loading, setLoading] = useState(false);
- const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
- const [isInEditMode, setIsInEditMode] = useState(false);
-
- const openEditMode = () => setIsInEditMode(true);
- const closeEditMode = () => setIsInEditMode(false);
-
- const saveEdits = async (pickedTime: ParsedMetadataDate) => {
- try {
- setLoading(true);
- if (isInEditMode && file) {
- const unixTimeInMicroSec = pickedTime.timestamp;
- if (unixTimeInMicroSec === file?.metadata.creationTime) {
- closeEditMode();
- return;
- }
- const updatedFile = await changeFileCreationTime(
- file,
- unixTimeInMicroSec,
- );
- updateExistingFilePubMetadata(file, updatedFile);
- scheduleUpdate();
- }
- } catch (e) {
- log.error("failed to update creationTime", e);
- } finally {
- closeEditMode();
- setLoading(false);
- }
- };
-
- return (
- <>
-
- }
- title={formatDate(originalCreationTime)}
- caption={formatTime(originalCreationTime)}
- openEditor={openEditMode}
- loading={loading}
- hideEditOption={shouldDisableEdits || isInEditMode}
- />
- {isInEditMode && (
-
- )}
-
- >
- );
-}
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index 8c1bb6321f..c947c577f2 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -1,9 +1,16 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { Titlebar } from "@/base/components/Titlebar";
import { nameAndExtension } from "@/base/file";
+import log from "@/base/log";
import type { ParsedMetadata } from "@/media/file-metadata";
+import {
+ getUICreationDate,
+ updateRemotePublicMagicMetadata,
+ type ParsedMetadataDate,
+} from "@/media/file-metadata";
import { FileType } from "@/media/file-type";
import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
+import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
import { isMLEnabled } from "@/new/photos/services/ml";
@@ -12,8 +19,11 @@ import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
+import ComlinkCryptoWorker from "@ente/shared/crypto";
+import { getPublicMagicMetadataMTSync } from "@ente/shared/file-metadata";
import { formatDate, formatTime } from "@ente/shared/time/format";
import BackupOutlined from "@mui/icons-material/BackupOutlined";
+import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CameraOutlined from "@mui/icons-material/CameraOutlined";
import FolderOutlined from "@mui/icons-material/FolderOutlined";
import LocationOnOutlined from "@mui/icons-material/LocationOnOutlined";
@@ -44,7 +54,6 @@ import { FileNameEditDialog } from "./FileNameEditDialog";
import InfoItem from "./InfoItem";
import MapBox from "./MapBox";
import { RenderCaption } from "./RenderCaption";
-import { RenderCreationTime } from "./RenderCreationTime";
export interface FileInfoExif {
tags: RawExifTags | undefined;
@@ -140,8 +149,8 @@ export const FileInfo: React.FC = ({
}}
/>
-
(
},
});
+interface CreationTimeProps {
+ enteFile: EnteFile;
+ shouldDisableEdits: boolean;
+ scheduleUpdate: () => void;
+}
+
+export const CreationTime: React.FC = ({
+ enteFile,
+ shouldDisableEdits,
+ scheduleUpdate,
+}) => {
+ const [loading, setLoading] = useState(false);
+ const [isInEditMode, setIsInEditMode] = useState(false);
+
+ const openEditMode = () => setIsInEditMode(true);
+ const closeEditMode = () => setIsInEditMode(false);
+
+ const publicMagicMetadata = getPublicMagicMetadataMTSync(enteFile);
+ const originalDate = getUICreationDate(enteFile, publicMagicMetadata);
+
+ const saveEdits = async (pickedTime: ParsedMetadataDate) => {
+ try {
+ setLoading(true);
+ if (isInEditMode && enteFile) {
+ // Use the updated date time (both in its canonical dateTime
+ // form, and also as the legacy timestamp). But don't use the
+ // offset. The offset here will be the offset of the computer
+ // where this user is making this edit, not the offset of the
+ // place where the photo was taken. In a future iteration of the
+ // date time editor, we can provide functionality for the user
+ // to edit the associated offset, but right now it is not even
+ // surfaced, so don't also potentially overwrite it.
+ const { dateTime, timestamp } = pickedTime;
+ if (timestamp == originalDate.getTime()) {
+ // Same as before.
+ closeEditMode();
+ return;
+ }
+
+ const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+ await updateRemotePublicMagicMetadata(
+ enteFile,
+ { dateTime, editedTime: timestamp },
+ cryptoWorker.encryptMetadata,
+ cryptoWorker.decryptMetadata,
+ );
+
+ scheduleUpdate();
+ }
+ } catch (e) {
+ log.error("failed to update creationTime", e);
+ } finally {
+ closeEditMode();
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+ }
+ title={formatDate(originalDate)}
+ caption={formatTime(originalDate)}
+ openEditor={openEditMode}
+ loading={loading}
+ hideEditOption={shouldDisableEdits || isInEditMode}
+ />
+ {isInEditMode && (
+
+ )}
+
+ >
+ );
+};
+
interface RenderFileNameProps {
file: EnteFile;
shouldDisableEdits: boolean;
diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts
index cecbe8765f..acd01c86d8 100644
--- a/web/packages/media/file-metadata.ts
+++ b/web/packages/media/file-metadata.ts
@@ -1,7 +1,10 @@
-import { encryptMetadata } from "@/base/crypto/ente";
+import { encryptMetadata, type decryptMetadata } from "@/base/crypto/ente";
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
import { apiURL } from "@/base/origins";
import { type EnteFile } from "@/new/photos/types/file";
+import { mergeMetadata1 } from "@/new/photos/utils/file";
+import { ensure } from "@/utils/ensure";
+import { z } from "zod";
import { FileType } from "./file-type";
/**
@@ -134,8 +137,42 @@ export enum ItemVisibility {
* with whom the file has been shared.
*
* For more details, see [Note: Metadatum].
+ *
+ * ---
+ *
+ * [Note: Optional magic metadata keys]
+ *
+ * Remote does not support nullish (`undefined` or `null`) values for the keys
+ * in the magic metadata associated with a file. All of the keys themselves are
+ * optional though.
+ *
+ * That is, all magic metadata properties are of the form:
+ *
+ * foo?: T
+ *
+ * And never like:
+ *
+ * foo: T | undefined
+ *
+ * Also see: [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet].
*/
export interface PublicMagicMetadata {
+ /**
+ * A ISO 8601 date time string without a timezone, indicating the local time
+ * where the photo was taken.
+ *
+ * e.g. "2022-01-26T13:08:20".
+ *
+ * See: [Note: Photos are always in local date/time].
+ */
+ dateTime?: string;
+ /**
+ * When available, a "±HH:mm" string indicating the UTC offset of the place
+ * where the photo was taken.
+ *
+ * e.g. "+02:00".
+ */
+ offsetTime?: string;
/**
* Modified value of the date time associated with an {@link EnteFile}.
*
@@ -164,6 +201,191 @@ export interface PublicMagicMetadata {
h?: number;
}
+/**
+ * Zod schema for the {@link PublicMagicMetadata} type.
+ *
+ * See: [Note: Duplicated Zod schema and TypeScript type]
+ *
+ * ---
+ *
+ * [Note: Use passthrough for metadata Zod schemas]
+ *
+ * It is important to (recursively) use the {@link passthrough} option when
+ * definining Zod schemas for the various metadata types (the plaintext JSON
+ * objects) because we want to retain all the fields we get from remote. There
+ * might be other, newer, clients out there adding fields that the current
+ * client might not we aware of, and we don't want to overwrite them.
+ */
+const PublicMagicMetadata = z
+ .object({
+ // [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet]
+ //
+ // Using `optional` is accurate here. The key is optional, but the value
+ // itself is not optional. Zod doesn't work with
+ // `exactOptionalPropertyTypes` yet, but it seems to be on the roadmap so we
+ // suppress these mismatches.
+ //
+ // See: https://github.com/colinhacks/zod/issues/635#issuecomment-2196579063
+ editedTime: z.number().optional(),
+ })
+ .passthrough();
+
+/**
+ * A function that can be used to encrypt the contents of a metadata field
+ * associated with a file.
+ *
+ * This is parameterized to allow us to use either the regular
+ * {@link encryptMetadata} (if we're already running in a web worker) or its web
+ * worker wrapper (if we're running on the main thread).
+ */
+export type EncryptMetadataF = typeof encryptMetadata;
+
+/**
+ * A function that can be used to decrypt the contents of a metadata field
+ * associated with a file.
+ *
+ * This is parameterized to allow us to use either the regular
+ * {@link encryptMetadata} (if we're already running in a web worker) or its web
+ * worker wrapper (if we're running on the main thread).
+ */
+export type DecryptMetadataF = typeof decryptMetadata;
+
+/**
+ * Return the public magic metadata for the given {@link enteFile}.
+ *
+ * The file we persist in our local db has the metadata in the encrypted form
+ * that we get it from remote. We decrypt when we read it, and also hang the
+ * decrypted version to the in-memory {@link EnteFile} as a cache.
+ *
+ * If the file doesn't have any public magic metadata attached to it, return
+ * `undefined`.
+ */
+export const decryptPublicMagicMetadata = async (
+ enteFile: EnteFile,
+ decryptMetadataF: DecryptMetadataF,
+): Promise => {
+ const envelope = enteFile.pubMagicMetadata;
+ // TODO: The underlying types need auditing.
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!envelope) return undefined;
+
+ // TODO: This function can be optimized to directly return the cached value
+ // instead of reparsing it using Zod. But that requires us (a) first fix the
+ // types, and (b) guarantee that we're the only ones putting that parsed
+ // data there, so that it is in a known good state (currently we exist in
+ // parallel with other functions that do the similar things).
+
+ const jsonValue =
+ typeof envelope.data == "string"
+ ? await decryptMetadataF(
+ envelope.data,
+ envelope.header,
+ enteFile.key,
+ )
+ : envelope.data;
+ const result = PublicMagicMetadata.parse(
+ // TODO: Can we avoid this cast?
+ withoutNullAndUndefinedValues(jsonValue as object),
+ );
+
+ // -@ts-expect-error [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet]
+ // We can't use -@ts-expect-error since this code is also included in the
+ // packages which don't have strict mode enabled (and thus don't error).
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
+ // @ts-ignore
+ envelope.data = result;
+
+ // -@ts-expect-error [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet]
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
+ // @ts-ignore
+ return result;
+};
+
+const withoutNullAndUndefinedValues = (o: object) =>
+ Object.fromEntries(
+ Object.entries(o).filter(([, v]) => v !== null && v !== undefined),
+ );
+
+/**
+ * Return the file's creation date in a form suitable for using in the UI.
+ *
+ * For all the details and nuance, see {@link toUIDate}.
+ */
+export const getUICreationDate = (
+ enteFile: EnteFile,
+ publicMagicMetadata: PublicMagicMetadata,
+) =>
+ toUIDate(
+ publicMagicMetadata.dateTime ??
+ publicMagicMetadata.editedTime ??
+ enteFile.metadata.creationTime,
+ );
+
+/**
+ * Update the public magic metadata associated with a file on remote.
+ *
+ * This function updates the public magic metadata on remote, and as a
+ * convenience also modifies the provided {@link EnteFile} object in place with
+ * the updated values, but it does not update the state of the local 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.
+ *
+ * @param enteFile The {@link EnteFile} whose public magic metadata we want to
+ * update.
+ *
+ * @param metadataUpdates A subset of {@link PublicMagicMetadata} containing the
+ * fields that we want to add or update.
+ *
+ * @param encryptMetadataF A function that is used to encrypt the updated
+ * metadata.
+ *
+ * @param decryptMetadataF A function that is used to decrypt the existing
+ * metadata.
+ */
+export const updateRemotePublicMagicMetadata = async (
+ enteFile: EnteFile,
+ metadataUpdates: Partial,
+ encryptMetadataF: EncryptMetadataF,
+ decryptMetadataF: DecryptMetadataF,
+) => {
+ const existingMetadata = await decryptPublicMagicMetadata(
+ enteFile,
+ decryptMetadataF,
+ );
+
+ const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates };
+
+ // The underlying types of enteFile.pubMagicMetadata are incorrect
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ const metadataVersion = enteFile.pubMagicMetadata?.version ?? 1;
+
+ const updateRequest = await updateMagicMetadataRequest(
+ enteFile,
+ updatedMetadata,
+ metadataVersion,
+ encryptMetadataF,
+ );
+
+ const updatedEnvelope = ensure(updateRequest.metadataList[0]).magicMetadata;
+
+ await putFilesPublicMagicMetadata(updateRequest);
+
+ // Modify the in-memory object to use the updated envelope. This steps are
+ // quite ad-hoc, as is the concept of updating the object in place.
+ enteFile.pubMagicMetadata =
+ updatedEnvelope as typeof enteFile.pubMagicMetadata;
+ // The correct version will come in the updated EnteFile we get in the
+ // response of the /diff. Temporarily bump it for the in place edits.
+ enteFile.pubMagicMetadata.version = enteFile.pubMagicMetadata.version + 1;
+ // Re-read the data.
+ await decryptPublicMagicMetadata(enteFile, decryptMetadataF);
+ // Re-jig the other bits of EnteFile that depend on its public magic
+ // metadata.
+ mergeMetadata1(enteFile);
+};
+
/**
* Magic metadata, either public and private, as persisted and used by remote.
*
@@ -178,8 +400,8 @@ interface RemoteMagicMetadata {
/**
* Monotonically increasing iteration of this metadata object.
*
- * The version starts at 1. Each time a client updates the underlying magic
- * metadata JSONs for a file, it increments this version number.
+ * The version starts at 1. Remote increments this version number each time
+ * a client updates the corresponding magic metadata field for the file.
*/
version: number;
/**
@@ -221,27 +443,19 @@ interface UpdateMagicMetadataRequest {
}[];
}
-/**
- * A function that can be used to encrypt the contents of a metadata field
- * associated with a file.
- *
- * This is parameterized to allow us to use either the regular
- * {@link encryptMetadata} or the web worker wrapper for it.
- */
-export type EncryptMetadataF = typeof encryptMetadata;
-
/**
* Construct an remote update request payload from the public or private magic
* metadata JSON object for an {@link enteFile}, using the provided
* {@link encryptMetadataF} function to encrypt the JSON.
*/
-export const updateMagicMetadataRequest = async (
+const updateMagicMetadataRequest = async (
enteFile: EnteFile,
metadata: PrivateMagicMetadata | PublicMagicMetadata,
metadataVersion: number,
encryptMetadataF: EncryptMetadataF,
): 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,
);
@@ -272,6 +486,7 @@ export const updateMagicMetadataRequest = async (
* @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 (
request: UpdateMagicMetadataRequest,
) =>
@@ -289,7 +504,7 @@ export const putFilesMagicMetadata = async (
* @param request The list of file ids and the updated encrypted magic metadata
* associated with each of them.
*/
-export const putFilesPublicMagicMetadata = async (
+const putFilesPublicMagicMetadata = async (
request: UpdateMagicMetadataRequest,
) =>
ensureOk(
@@ -400,13 +615,13 @@ export interface ParsedMetadataDate {
* This is an optional UTC offset string of the form "±HH:mm" or "Z",
* specifying the timezone offset for {@link dateTime} when available.
*/
- offsetTime: string | undefined;
+ offset: string | undefined;
/**
* UTC epoch microseconds derived from {@link dateTime} and
- * {@link offsetTime}.
+ * {@link offset}.
*
- * When the {@link offsetTime} is present, this will accurately reflect a
- * UTC timestamp. When the {@link offsetTime} is not present it convert to a
+ * When the {@link offset} is present, this will accurately reflect a
+ * UTC timestamp. When the {@link offset} is not present it convert to a
* UTC timestamp by assuming that the given {@link dateTime} is in the local
* time where this code is running. This is a good assumption but not always
* correct (e.g. vacation photos).
@@ -451,7 +666,7 @@ export const parseMetadataDate = (
// Now we try to massage s into two parts - the local date/time string, and
// an UTC offset string.
- let offsetTime: string | undefined;
+ let offset: string | undefined;
let sWithoutOffset: string;
// Check to see if there is a time-zone descriptor of the form "Z" or
@@ -459,7 +674,7 @@ export const parseMetadataDate = (
const m = s.match(/Z|[+-]\d\d:?\d\d$/);
if (m?.index) {
sWithoutOffset = s.substring(0, m.index);
- offsetTime = s.substring(m.index);
+ offset = s.substring(m.index);
} else {
sWithoutOffset = s;
}
@@ -499,7 +714,44 @@ export const parseMetadataDate = (
// any time zone descriptor.
const dateTime = dropLast(date.toISOString());
- return { dateTime, offsetTime, timestamp };
+ return { dateTime, offset, timestamp };
};
const dropLast = (s: string) => (s ? s.substring(0, s.length - 1) : s);
+
+/**
+ * Return a date that can be used on the UI from a {@link ParsedMetadataDate},
+ * or its {@link dateTime} component, or the legacy epoch timestamps.
+ *
+ * These dates are all hypothetically in the timezone of the place where the
+ * photo was taken. Different photos might've been taken in different timezones,
+ * which is why it is hypothetical, so concretely these are all mapped to the
+ * current timezone.
+ *
+ * The difference is subtle, but we should not think of these as absolute points
+ * on the UTC timeline. They are instead better thought of as dates without an
+ * associated timezone. For the purpose of mapping them all to a comparable
+ * dimension them we all contingently use the current timezone - this makes it
+ * easy to use JavaScript Date constructor which assumes that any date/time
+ * string without an associated timezone is in the current timezone.
+ *
+ * Whenever we're surfacing them in the UI, or using them for grouping (say by
+ * day), we should use their current timezone representation, not the UTC one.
+ *
+ * See also: [Note: Photos are always in local date/time].
+ */
+export const toUIDate = (dateLike: ParsedMetadataDate | string | number) => {
+ switch (typeof dateLike) {
+ case "object":
+ // A ISO 8601 string without a timezone. The Date constructor will
+ // assume the timezone to be the current timezone.
+ return new Date(dateLike.dateTime);
+ case "string":
+ // This is expected to be a string with the same meaning as
+ // `ParsedMetadataDate.dateTime`.
+ return new Date(dateLike);
+ case "number":
+ // A legacy epoch microseconds value.
+ return new Date(dateLike / 1000);
+ }
+};
diff --git a/web/packages/new/photos/components/PhotoDateTimePicker.tsx b/web/packages/new/photos/components/PhotoDateTimePicker.tsx
index cab2a51ddd..86859a3672 100644
--- a/web/packages/new/photos/components/PhotoDateTimePicker.tsx
+++ b/web/packages/new/photos/components/PhotoDateTimePicker.tsx
@@ -147,14 +147,14 @@ const parseMetadataDateFromDayjs = (d: Dayjs): ParsedMetadataDate => {
const s = d.format();
let dateTime: string;
- let offsetTime: string | undefined;
+ let offset: string | undefined;
// Check to see if there is a time-zone descriptor of the form "Z" or
// "±05:30" or "±0530" at the end of s.
const m = s.match(/Z|[+-]\d\d:?\d\d$/);
if (m?.index) {
dateTime = s.substring(0, m.index);
- offsetTime = s.substring(m.index);
+ offset = s.substring(m.index);
} else {
throw new Error(
`Dayjs.format returned a string "${s}" without a timezone offset`,
@@ -163,5 +163,5 @@ const parseMetadataDateFromDayjs = (d: Dayjs): ParsedMetadataDate => {
const timestamp = d.valueOf() * 1000;
- return { dateTime, offsetTime, timestamp };
+ return { dateTime, offset, timestamp };
};
diff --git a/web/packages/new/photos/types/file.ts b/web/packages/new/photos/types/file.ts
index aff511a337..c07c79649f 100644
--- a/web/packages/new/photos/types/file.ts
+++ b/web/packages/new/photos/types/file.ts
@@ -46,8 +46,18 @@ export interface EnteFile
> {
metadata: Metadata;
magicMetadata: FileMagicMetadata;
+ /**
+ * The envelope containing the public magic metadata associated with this
+ * file.
+ */
pubMagicMetadata: FilePublicMagicMetadata;
isTrashed?: boolean;
+ /**
+ * The base64 encoded encryption key associated with this file.
+ *
+ * This key is used to encrypt both the file's contents, and any associated
+ * data (e.g., metadatum, thumbnail) for the file.
+ */
key: string;
src?: string;
srcURLs?: SourceURLs;
diff --git a/web/packages/new/photos/utils/file.ts b/web/packages/new/photos/utils/file.ts
index f63d775319..3b9281e468 100644
--- a/web/packages/new/photos/utils/file.ts
+++ b/web/packages/new/photos/utils/file.ts
@@ -39,20 +39,22 @@ export const fileLogID = (enteFile: EnteFile) =>
* its filename.
*/
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
- return files.map((file) => {
- // TODO: Until the types reflect reality
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (file.pubMagicMetadata?.data.editedTime) {
- file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
- }
- // TODO: Until the types reflect reality
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (file.pubMagicMetadata?.data.editedName) {
- file.metadata.title = file.pubMagicMetadata.data.editedName;
- }
+ return files.map((file) => mergeMetadata1(file));
+}
- return file;
- });
+export function mergeMetadata1(file: EnteFile): EnteFile {
+ // TODO: Until the types reflect reality
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (file.pubMagicMetadata?.data.editedTime) {
+ file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
+ }
+ // TODO: Until the types reflect reality
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (file.pubMagicMetadata?.data.editedName) {
+ file.metadata.title = file.pubMagicMetadata.data.editedName;
+ }
+
+ return file;
}
/**
diff --git a/web/packages/shared/crypto/internal/crypto.worker.ts b/web/packages/shared/crypto/internal/crypto.worker.ts
index d825ba5a57..356bde8580 100644
--- a/web/packages/shared/crypto/internal/crypto.worker.ts
+++ b/web/packages/shared/crypto/internal/crypto.worker.ts
@@ -8,12 +8,16 @@ import type { StateAddress } from "libsodium-wrappers";
* specific layer (base/crypto/ente.ts) or the internal libsodium layer
* (internal/libsodium.ts).
*
- * Running these in a web worker allows us to use potentially CPU-intensive
- * crypto operations from the main thread without stalling the UI.
+ * Use these when running on the main thread, since running these in a web
+ * worker allows us to use potentially CPU-intensive crypto operations from the
+ * main thread without stalling the UI.
+ *
+ * If the code that needs this functionality is already running in the context
+ * of a web worker, then use the underlying functions directly.
*
* See: [Note: Crypto code hierarchy].
*
- * Note: Keep these methods logic free. They should just act as trivial proxies.
+ * Note: Keep these methods logic free. They are meant to be trivial proxies.
*/
export class DedicatedCryptoWorker {
async decryptThumbnail(
diff --git a/web/packages/shared/file-metadata.ts b/web/packages/shared/file-metadata.ts
new file mode 100644
index 0000000000..869cc9db6b
--- /dev/null
+++ b/web/packages/shared/file-metadata.ts
@@ -0,0 +1,48 @@
+import { decryptMetadata } from "@/base/crypto/ente";
+import { isDevBuild } from "@/base/env";
+import {
+ decryptPublicMagicMetadata,
+ type PublicMagicMetadata,
+} from "@/media/file-metadata";
+import { EnteFile } from "@/new/photos/types/file";
+import { fileLogID } from "@/new/photos/utils/file";
+import ComlinkCryptoWorker from "@ente/shared/crypto";
+
+/**
+ * On-demand decrypt the public magic metadata for an {@link EnteFile} for code
+ * running on the main thread.
+ *
+ * It both modifies the given file object, and also returns the decrypted
+ * metadata.
+ */
+export const getPublicMagicMetadataMT = async (enteFile: EnteFile) =>
+ decryptPublicMagicMetadata(
+ enteFile,
+ (await ComlinkCryptoWorker.getInstance()).decryptMetadata,
+ );
+
+/**
+ * On-demand decrypt the public magic metadata for an {@link EnteFile} for code
+ * running on the main thread, but do it synchronously.
+ *
+ * It both modifies the given file object, and also returns the decrypted
+ * metadata.
+ *
+ * We are not expected to be in a scenario where the file gets to the UI without
+ * having its public magic metadata decrypted, so this function is a sanity
+ * check and should be a no-op in usually. On debug builds it'll throw if it
+ * finds its assumptions broken.
+ */
+export const getPublicMagicMetadataMTSync = (enteFile: EnteFile) => {
+ if (!enteFile.pubMagicMetadata) return undefined;
+ if (typeof enteFile.pubMagicMetadata.data == "string") {
+ if (isDevBuild)
+ throw new Error(
+ `Public magic metadata for ${fileLogID(enteFile)} had not been decrypted even when the file reached the UI layer`,
+ );
+ decryptPublicMagicMetadata(enteFile, decryptMetadata);
+ }
+ // This cast is unavoidable in the current setup. We need to refactor the
+ // types so that this cast in not needed.
+ return enteFile.pubMagicMetadata.data as PublicMagicMetadata;
+};