This commit is contained in:
Manav Rathi
2025-06-20 17:25:43 +05:30
parent eb5a0cb1db
commit 9e80aeb061
11 changed files with 53 additions and 280 deletions

View File

@@ -18,7 +18,6 @@ import { downloadManager } from "ente-gallery/services/download";
import { extractExifDates } from "ente-gallery/services/exif";
import { fileLogID, type EnteFile } from "ente-media/file";
import {
decryptPublicMagicMetadata,
fileCreationPhotoDate,
fileFileName,
type ParsedMetadataDate,
@@ -343,10 +342,7 @@ const updateFileDate = async (
if (!newDate) return;
const existingDate = fileCreationPhotoDate(
file,
await decryptPublicMagicMetadata(file),
);
const existingDate = fileCreationPhotoDate(file);
if (newDate.timestamp == existingDate.getTime()) return;
await updateFilePublicMagicMetadata(file, {

View File

@@ -58,11 +58,9 @@ import { tagNumericValue, type RawExifTags } from "ente-gallery/services/exif";
import { formattedByteSize } from "ente-gallery/utils/units";
import { type EnteFile } from "ente-media/file";
import {
fileCaption,
fileCreationPhotoDate,
fileFileName,
fileLocation,
filePublicMagicMetadata,
type ParsedMetadata,
type ParsedMetadataDate,
} from "ente-media/file-metadata";
@@ -260,7 +258,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
onSelectPerson?.(personID);
};
const uploaderName = filePublicMagicMetadata(file)?.uploaderName;
const uploaderName = file.pubMagicMetadata?.data.uploaderName;
return (
<FileInfoSidebar {...{ open, onClose }}>
@@ -582,7 +580,7 @@ const Caption: React.FC<CaptionProps> = ({
}) => {
const [isSaving, setIsSaving] = useState(false);
const caption = fileCaption(file) ?? "";
const caption = file.pubMagicMetadata?.data.caption ?? "";
const formik = useFormik<{ caption: string }>({
initialValues: { caption },
@@ -675,10 +673,7 @@ const CreationTime: React.FC<CreationTimeProps> = ({
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const originalDate = fileCreationPhotoDate(
file,
filePublicMagicMetadata(file),
);
const originalDate = fileCreationPhotoDate(file);
const saveEdits = async (pickedTime: ParsedMetadataDate) => {
setIsEditing(false);

View File

@@ -37,11 +37,7 @@ import {
type FileInfoProps,
} from "ente-gallery/components/FileInfo";
import type { Collection } from "ente-media/collection";
import {
fileFileName,
fileVisibility,
ItemVisibility,
} from "ente-media/file-metadata";
import { fileFileName, ItemVisibility } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import type { EnteFile } from "ente-media/file.js";
import { isHEICExtension, needsJPEGConversion } from "ente-media/formats";
@@ -670,7 +666,7 @@ export const FileViewer: React.FC<FileViewerProps> = ({
file &&
activeAnnotatedFile.annotation.showArchive
) {
switch (fileVisibility(file)) {
switch (file.magicMetadata?.data.visibility) {
case undefined:
case ItemVisibility.visible:
isArchived = false;

View File

@@ -7,7 +7,6 @@ import {
type HLSPlaylistDataForFile,
} from "ente-gallery/services/video";
import type { EnteFile } from "ente-media/file";
import { fileCaption, filePublicMagicMetadata } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import { ensureString } from "ente-utils/ensure";
@@ -439,7 +438,7 @@ const enqueueUpdates = async (
const update = (itemData: Partial<ItemData>, validTill?: Date) => {
// Use the file's caption as its alt text (in addition to using it as
// the visible caption).
const alt = fileCaption(file);
const alt = file.pubMagicMetadata?.data.caption;
_state.itemDataByFileID.set(file.id, {
...itemData,
@@ -647,8 +646,7 @@ const thumbnailDimensions = (
{ width: thumbnailWidth, height: thumbnailHeight }: Partial<ItemData>,
file: EnteFile,
) => {
const { w: imageWidth, h: imageHeight } =
filePublicMagicMetadata(file) ?? {};
const { w: imageWidth, h: imageHeight } = file.pubMagicMetadata?.data ?? {};
if (thumbnailWidth && thumbnailHeight && imageWidth && imageHeight) {
const arThumb = thumbnailWidth / thumbnailHeight;
const arImage = imageWidth / imageHeight;

View File

@@ -33,14 +33,13 @@ import type {
EncryptedMagicMetadata,
EnteFile,
FilePublicMagicMetadata,
FilePublicMagicMetadataProps,
} from "ente-media/file";
import {
fileFileName,
metadataHash,
type FileMetadata,
type FilePublicMagicMetadataData,
type ParsedMetadata,
type PublicMagicMetadata,
} from "ente-media/file-metadata";
import { FileType, type FileTypeInfo } from "ente-media/file-type";
import { encodeLivePhoto } from "ente-media/live-photo";
@@ -1047,7 +1046,7 @@ const readEntireStream = async (stream: ReadableStream) =>
interface ExtractAssetMetadataResult {
metadata: FileMetadata;
publicMagicMetadata: FilePublicMagicMetadataProps;
publicMagicMetadata: FilePublicMagicMetadataData;
}
/**
@@ -1171,7 +1170,7 @@ const extractImageOrVideoMetadata = async (
parsedMetadataJSONMap,
);
const publicMagicMetadata: PublicMagicMetadata = {};
const publicMagicMetadata: FilePublicMagicMetadataData = {};
const modificationTime =
parsedMetadataJSON?.modificationTime ?? lastModifiedMs * 1000;
@@ -1490,7 +1489,7 @@ const augmentWithThumbnail = async (
};
const constructPublicMagicMetadata = async (
publicMagicMetadataProps: FilePublicMagicMetadataProps,
publicMagicMetadataProps: FilePublicMagicMetadataData,
): Promise<FilePublicMagicMetadata> => {
const nonEmptyPublicMagicMetadataProps = getNonEmptyMagicMetadataProps(
publicMagicMetadataProps,

View File

@@ -10,7 +10,6 @@ import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import { ensureAuthToken } from "ente-base/token";
import { fileLogID, type EnteFile } from "ente-media/file";
import { filePublicMagicMetadata } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import { updateFilePublicMagicMetadata } from "ente-new/photos/services/file";
import {
@@ -336,7 +335,7 @@ export const hlsPlaylistDataForFile = async (
): Promise<HLSPlaylistDataForFile> => {
ensurePrecondition(file.metadata.fileType == FileType.video);
if (filePublicMagicMetadata(file)?.sv == 1) {
if (file.pubMagicMetadata?.data.sv == 1) {
return "skip";
}
@@ -901,7 +900,7 @@ const backfillQueue = async (
// Not in trash.
!localTrashFileIDs.has(f.id) &&
// See: [Note: Marking files which do not need video processing]
filePublicMagicMetadata(f)?.sv != 1,
f.pubMagicMetadata?.data.sv != 1,
),
);

View File

@@ -1,18 +1,15 @@
import { decryptMetadataJSON, encryptMetadataJSON } from "ente-base/crypto";
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 {
fileLogID,
type EnteFile,
type EnteFile2,
type FileMagicMetadata,
type FilePrivateMagicMetadata,
type FilePublicMagicMetadata,
} from "ente-media/file";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
import { mergeMetadata1 } from "./file";
import { FileType } from "./file-type";
import type { RemoteMagicMetadata } from "./magic-metadata";
@@ -293,7 +290,7 @@ export type ItemVisibility =
*
* Also see: [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet].
*/
export interface PublicMagicMetadata {
export interface FilePublicMagicMetadataData {
/**
* A ISO 8601 date time string without a timezone, indicating the local time
* where the photo (or video) was taken.
@@ -358,6 +355,20 @@ export interface PublicMagicMetadata {
* (The owner of such files will be the owner of the collection)
*/
uploaderName?: string;
/**
* Edited latitude of the file
*
* If the user edits the location (latitude and longitude) of a file within
* Ente, then the edits will be stored as the {@link lat} and {@link long}
* properties in the file's public magic metadata.
*/
lat?: number;
/**
* Edited longitude of the file.
*
* See {@link long}.
*/
long?: number;
/**
* An arbitrary integer set to indicate that this file should be skipped for
* the purpose of HLS generation.
@@ -402,7 +413,8 @@ export interface PublicMagicMetadata {
* 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.looseObject({
// TODO(RE): Use me
export const PublicMagicMetadata = z.looseObject({
// [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet]
//
// Using `optional` is not accurate here. The key is optional, but the
@@ -416,46 +428,6 @@ const PublicMagicMetadata = z.looseObject({
editedTime: z.number().optional(),
});
/**
* Return the private magic metadata for an {@link EnteFile}.
*
* We are not expected to be in a scenario where the file gets to the UI without
* having its private magic metadata decrypted, so this function is a sanity
* check and should be a no-op in usually. It'll throw if it finds its
* assumptions broken. Once the types have been refactored this entire
* check/cast shouldn't be needed, and this should become a trivial accessor.
*/
export const filePrivateMagicMetadata = (file: EnteFile) => {
if (!file.magicMetadata) return undefined;
if (typeof file.magicMetadata.data == "string") {
throw new Error(
`Private magic metadata for ${fileLogID(file)} had not been decrypted even when the file reached the UI layer`,
);
}
return file.magicMetadata.data;
};
/**
* Return the public magic metadata for an {@link EnteFile}.
*
* 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. It'll throw if it finds its
* assumptions broken. Once the types have been refactored this entire
* check/cast shouldn't be needed, and this should become a trivial accessor.
*/
export const filePublicMagicMetadata = (file: EnteFile) => {
if (!file.pubMagicMetadata) return undefined;
if (typeof file.pubMagicMetadata.data == "string") {
throw new Error(
`Public magic metadata for ${fileLogID(file)} had not been decrypted even when the file reached the UI layer`,
);
}
// This cast is unavoidable in the current setup. We need to refactor the
// types so that this cast in not needed.
return file.pubMagicMetadata.data as PublicMagicMetadata;
};
/**
* Return the hash of the file by reading it from its metadata.
*
@@ -483,59 +455,11 @@ export const metadataHash = (metadata: FileMetadata) => {
};
/**
* Return the public magic metadata for the given {@link file}.
*
* 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`.
* Return `true` if the {@link ItemVisibility} of the given {@link file} is
* archived.
*/
export const decryptPublicMagicMetadata = async (
file: EnteFile,
): Promise<PublicMagicMetadata | undefined> => {
const envelope = file.pubMagicMetadata;
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 decryptMetadataJSON(
{
encryptedData: envelope.data,
decryptionHeader: envelope.header,
},
file.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
// @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
// @ts-ignore
return result;
};
const withoutNullAndUndefinedValues = (o: object) =>
Object.fromEntries(
Object.entries(o).filter(([, v]) => v !== null && v !== undefined),
);
export const isArchivedFile = (file: EnteFile) =>
file.magicMetadata?.data.visibility == ItemVisibility.archived;
/**
* Return the file name of the file (including both the name and the extension).
@@ -555,15 +479,13 @@ export const fileFileName = (file: EnteFile | EnteFile2) =>
* Return the file's creation date as a Date in the hypothetical "timezone of
* the photo".
*
* For all the details and nuance, see {@link createPhotoDate}.
* This function handles files with edited dates. For all the details and
* nuance, see {@link createPhotoDate}.
*/
export const fileCreationPhotoDate = (
file: EnteFile,
publicMagicMetadata: PublicMagicMetadata | undefined,
) =>
export const fileCreationPhotoDate = (file: EnteFile) =>
createPhotoDate(
publicMagicMetadata?.dateTime ??
publicMagicMetadata?.editedTime ??
file.pubMagicMetadata?.data.dateTime ??
file.pubMagicMetadata?.data.editedTime ??
file.metadata.creationTime,
);
@@ -617,7 +539,7 @@ export const updateRemotePrivateMagicMetadata = async (
file: EnteFile,
metadataUpdates: Partial<FilePrivateMagicMetadataData>,
): Promise<FilePrivateMagicMetadata> => {
const existingMetadata = filePrivateMagicMetadata(file);
const existingMetadata = file.magicMetadata?.data;
const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates };
@@ -647,50 +569,6 @@ export const updateRemotePrivateMagicMetadata = async (
return updatedMagicMetadata;
};
/**
* Update the public magic metadata associated with a file on remote.
*
* See: [Note: Interactive updates to file metadata]
*
* @param file 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.
*/
export const updateRemotePublicMagicMetadata = async (
file: EnteFile,
metadataUpdates: Partial<PublicMagicMetadata>,
) => {
const existingMetadata = await decryptPublicMagicMetadata(file);
const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates };
const metadataVersion = file.pubMagicMetadata?.version ?? 1;
const updateRequest = await updateMagicMetadataRequest(
file,
updatedMetadata,
metadataVersion,
);
const updatedEnvelope = 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.
file.pubMagicMetadata = updatedEnvelope as FilePublicMagicMetadata;
// 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.
file.pubMagicMetadata.version = file.pubMagicMetadata.version + 1;
// Re-read the data.
await decryptPublicMagicMetadata(file);
// Re-jig the other bits of EnteFile that depend on its public magic
// metadata.
mergeMetadata1(file);
};
/**
* The shape of the JSON body payload expected by the APIs that update the
* public and private magic metadata fields associated with a file.
@@ -712,7 +590,7 @@ interface UpdateMagicMetadataRequest {
*/
const updateMagicMetadataRequest = async (
file: EnteFile,
metadata: FilePrivateMagicMetadataData | PublicMagicMetadata,
metadata: FilePrivateMagicMetadataData | FilePublicMagicMetadataData,
metadataVersion: number,
): Promise<UpdateMagicMetadataRequest> => {
// Drop all null or undefined values to obtain the syncable entries.
@@ -760,36 +638,6 @@ const putFilesPrivateMagicMetadata = async (
}),
);
/**
* Update the public magic metadata for a list of files.
*
* @param request The list of file ids and the updated encrypted magic metadata
* associated with each of them.
*/
const putFilesPublicMagicMetadata = async (
request: UpdateMagicMetadataRequest,
) =>
ensureOk(
await fetch(await apiURL("/files/public-magic-metadata"), {
method: "PUT",
headers: await authenticatedRequestHeaders(),
body: JSON.stringify(request),
}),
);
/**
* Return the {@link ItemVisibility} for the given {@link file}.
*/
export const fileVisibility = (file: EnteFile) =>
filePrivateMagicMetadata(file)?.visibility;
/**
* Return `true` if the {@link ItemVisibility} of the given {@link file} is
* archived.
*/
export const isArchivedFile = (item: EnteFile) =>
fileVisibility(item) === ItemVisibility.archived;
/**
* Return the GPS coordinates (if any) present in the given {@link EnteFile}.
*/
@@ -848,13 +696,6 @@ export const fileDurationString = (file: EnteFile): string | undefined => {
}
};
/**
* Return the caption, aka "description", (if any) attached to the given
* {@link EnteFile}.
*/
export const fileCaption = (file: EnteFile): string | undefined =>
filePublicMagicMetadata(file)?.caption;
/**
* Metadata about a file extracted from various sources (like Exif) when
* uploading it into Ente.

View File

@@ -12,6 +12,7 @@ import {
fileFileName,
FileMetadata,
type FilePrivateMagicMetadataData,
type FilePublicMagicMetadataData,
} from "./file-metadata";
import { FileType } from "./file-type";
import { decryptMagicMetadata, RemoteMagicMetadata } from "./magic-metadata";
@@ -306,7 +307,7 @@ export interface EnteFile
*
* See: [Note: Metadatum]
*/
pubMagicMetadata?: FilePublicMagicMetadata;
pubMagicMetadata?: MagicMetadataCore<FilePublicMagicMetadataData>;
/**
* `true` if this file is in trash (i.e. it has been deleted by the user,
* and will be permanently deleted after 30 days of being moved to trash).
@@ -492,56 +493,8 @@ export type FileMagicMetadata = MagicMetadataCore<FilePrivateMagicMetadataData>;
export type FilePrivateMagicMetadata =
MagicMetadataCore<FilePrivateMagicMetadataData>;
export interface FilePublicMagicMetadataProps {
/**
* Modified value of the date time associated with an {@link EnteFile}.
*
* Epoch microseconds.
*/
editedTime?: number;
/** See {@link PublicMagicMetadata} in file-metadata.ts */
dateTime?: string;
/** See {@link PublicMagicMetadata} in file-metadata.ts */
offsetTime?: string;
/**
* Edited name of the {@link EnteFile}.
*
* If the user edits the name of the file within Ente, then the edits are
* saved in this field.
*/
editedName?: string;
/**
* A arbitrary textual caption / description that the user has attached to
* the {@link EnteFile}.
*/
caption?: string;
uploaderName?: string;
/**
* Width of the image / video, in pixels.
*/
w?: number;
/**
* Height of the image / video, in pixels.
*/
h?: number;
/**
* Edited latitude for the {@link EnteFile}.
*
* If the user edits the location (latitude and longitude) of a file within
* Ente, then the edits will be stored as the {@link lat} and {@link long}
* properties in the file's public magic metadata.
*/
lat?: number;
/**
* Edited longitude for the {@link EnteFile}.
*
* See {@link long}.
*/
long?: number;
}
export type FilePublicMagicMetadata =
MagicMetadataCore<FilePublicMagicMetadataProps>;
MagicMetadataCore<FilePublicMagicMetadataData>;
export interface TrashItem extends Omit<EncryptedTrashItem, "file"> {
file: EnteFile;
@@ -741,7 +694,7 @@ export const decryptRemoteFile = async (
key,
);
// TODO(RE):
const data = genericMM.data as FilePublicMagicMetadataProps;
const data = genericMM.data as FilePublicMagicMetadataData;
// TODO(RE):
pubMagicMetadata = { ...genericMM, header: "", data };
}

View File

@@ -2,10 +2,7 @@ import { ensureLocalUser } from "ente-accounts/services/user";
import { assertionFailed } from "ente-base/assert";
import { newID } from "ente-base/id";
import type { EnteFile } from "ente-media/file";
import {
filePublicMagicMetadata,
metadataHash,
} from "ente-media/file-metadata";
import { metadataHash } from "ente-media/file-metadata";
import {
addToCollection,
createCollectionNameByID,
@@ -312,7 +309,7 @@ const duplicateGroupItemToRetain = (duplicateGroup: DuplicateGroup) => {
const itemsWithCaption: DuplicateGroup["items"] = [];
const itemsWithOtherEdits: DuplicateGroup["items"] = [];
for (const item of duplicateGroup.items) {
const pubMM = filePublicMagicMetadata(item.file);
const pubMM = item.file.pubMagicMetadata?.data;
if (!pubMM) continue;
if (pubMM.caption) itemsWithCaption.push(item);
if (pubMM.editedName ?? pubMM.editedTime)

View File

@@ -3,8 +3,8 @@ import { apiURL } from "ente-base/origins";
import type { EnteFile, EnteFile2 } from "ente-media/file";
import type {
FilePrivateMagicMetadataData,
FilePublicMagicMetadataData,
ItemVisibility,
PublicMagicMetadata,
} from "ente-media/file-metadata";
import {
createMagicMetadata,
@@ -201,7 +201,7 @@ export const updateFileCaption = (file: EnteFile2, caption: string) =>
*/
export const updateFilePublicMagicMetadata = async (
file: EnteFile2,
updates: PublicMagicMetadata,
updates: FilePublicMagicMetadataData,
) => updateFilesPublicMagicMetadata([file], updates);
/**
@@ -214,7 +214,7 @@ export const updateFilePublicMagicMetadata = async (
*/
const updateFilesPublicMagicMetadata = async (
files: EnteFile2[],
updates: PublicMagicMetadata,
updates: FilePublicMagicMetadataData,
) =>
putFilesPublicMagicMetadata({
metadataList: await Promise.all(

View File

@@ -10,7 +10,6 @@ import {
fileCreationPhotoDate,
fileFileName,
fileLocation,
filePublicMagicMetadata,
} from "ente-media/file-metadata";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
@@ -385,7 +384,7 @@ const isMatchingFile = (file: EnteFile, suggestion: SearchSuggestion) => {
case "date":
return isDateComponentsMatch(
suggestion.dateComponents,
fileCreationPhotoDate(file, filePublicMagicMetadata(file)),
fileCreationPhotoDate(file),
);
case "location": {