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