[web] Use the new date/time persistence format for edits (#2628)

This commit is contained in:
Manav Rathi
2024-08-07 11:13:42 +05:30
committed by GitHub
8 changed files with 449 additions and 120 deletions

View File

@@ -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 (
<>
<FlexWrapper>
<InfoItem
icon={<CalendarTodayIcon />}
title={formatDate(originalCreationTime)}
caption={formatTime(originalCreationTime)}
openEditor={openEditMode}
loading={loading}
hideEditOption={shouldDisableEdits || isInEditMode}
/>
{isInEditMode && (
<PhotoDateTimePicker
initialValue={originalCreationTime}
disabled={loading}
onAccept={saveEdits}
onClose={closeEditMode}
/>
)}
</FlexWrapper>
</>
);
}

View File

@@ -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<FileInfoProps> = ({
}}
/>
<RenderCreationTime
{...{ file, shouldDisableEdits, scheduleUpdate }}
<CreationTime
{...{ enteFile: file, shouldDisableEdits, scheduleUpdate }}
/>
<RenderFileName
@@ -347,6 +356,87 @@ const FileInfoSidebar = styled((props: DialogProps) => (
},
});
interface CreationTimeProps {
enteFile: EnteFile;
shouldDisableEdits: boolean;
scheduleUpdate: () => void;
}
export const CreationTime: React.FC<CreationTimeProps> = ({
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 (
<>
<FlexWrapper>
<InfoItem
icon={<CalendarTodayIcon />}
title={formatDate(originalDate)}
caption={formatTime(originalDate)}
openEditor={openEditMode}
loading={loading}
hideEditOption={shouldDisableEdits || isInEditMode}
/>
{isInEditMode && (
<PhotoDateTimePicker
initialValue={originalDate}
disabled={loading}
onAccept={saveEdits}
onClose={closeEditMode}
/>
)}
</FlexWrapper>
</>
);
};
interface RenderFileNameProps {
file: EnteFile;
shouldDisableEdits: boolean;

View File

@@ -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<PublicMagicMetadata | undefined> => {
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<PublicMagicMetadata>,
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<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,
);
@@ -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);
}
};

View File

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

View File

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

View File

@@ -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;
}
/**

View File

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

View File

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