From eb5a0cb1dbb54608138ba83645a28916de9b8a96 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:02:19 +0530 Subject: [PATCH 1/6] Prune --- web/packages/gallery/services/file.ts | 122 ------------------ web/packages/gallery/services/video.ts | 8 +- .../new/photos/services/collection.ts | 3 +- 3 files changed, 4 insertions(+), 129 deletions(-) delete mode 100644 web/packages/gallery/services/file.ts diff --git a/web/packages/gallery/services/file.ts b/web/packages/gallery/services/file.ts deleted file mode 100644 index c83c059cb9..0000000000 --- a/web/packages/gallery/services/file.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* TODO: Audit this file */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import { encryptMetadataJSON } from "ente-base/crypto"; -import { apiURL } from "ente-base/origins"; -import { updateMagicMetadata } from "ente-gallery/services/magic-metadata"; -import type { - EncryptedMagicMetadata, - EnteFile, - FilePublicMagicMetadata, - FilePublicMagicMetadataProps, - FileWithUpdatedPublicMagicMetadata, -} from "ente-media/file"; -import { mergeMetadata } from "ente-media/file"; -import HTTPService from "ente-shared/network/HTTPService"; -import { getToken } from "ente-shared/storage/localStorage/helpers"; - -export interface UpdateMagicMetadataRequest { - id: number; - magicMetadata: EncryptedMagicMetadata; -} - -interface BulkUpdateMagicMetadataRequest { - metadataList: UpdateMagicMetadataRequest[]; -} - -export const updateFilePublicMagicMetadata = async ( - fileWithUpdatedPublicMagicMetadataList: FileWithUpdatedPublicMagicMetadata[], -): Promise => { - const token = getToken(); - if (!token) { - // @ts-ignore - return; - } - const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; - for (const { - file, - updatedPublicMagicMetadata, - } of fileWithUpdatedPublicMagicMetadataList) { - const { encryptedData, decryptionHeader } = await encryptMetadataJSON( - updatedPublicMagicMetadata.data, - file.key, - ); - reqBody.metadataList.push({ - id: file.id, - magicMetadata: { - version: updatedPublicMagicMetadata.version, - count: updatedPublicMagicMetadata.count, - data: encryptedData, - header: decryptionHeader, - }, - }); - } - await HTTPService.put( - await apiURL("/files/public-magic-metadata"), - reqBody, - // @ts-ignore - null, - { "X-Auth-Token": token }, - ); - return fileWithUpdatedPublicMagicMetadataList.map( - ({ file, updatedPublicMagicMetadata }): EnteFile => ({ - ...file, - pubMagicMetadata: { - ...updatedPublicMagicMetadata, - version: updatedPublicMagicMetadata.version + 1, - }, - }), - ); -}; - -export async function changeFileName( - file: EnteFile, - editedName: string, -): Promise { - const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { - editedName, - }; - - const updatedPublicMagicMetadata: FilePublicMagicMetadata = - await updateMagicMetadata( - updatedPublicMagicMetadataProps, - file.pubMagicMetadata, - file.key, - ); - const updateResult = await updateFilePublicMagicMetadata([ - { file, updatedPublicMagicMetadata }, - ]); - // @ts-ignore - return updateResult[0]; -} - -export async function changeCaption( - file: EnteFile, - caption: string, -): Promise { - const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { - caption, - }; - - const updatedPublicMagicMetadata: FilePublicMagicMetadata = - await updateMagicMetadata( - updatedPublicMagicMetadataProps, - file.pubMagicMetadata, - file.key, - ); - const updateResult = await updateFilePublicMagicMetadata([ - { file, updatedPublicMagicMetadata }, - ]); - // @ts-ignore - return updateResult[0]; -} - -export function updateExistingFilePubMetadata( - existingFile: EnteFile, - updatedFile: EnteFile, -) { - // @ts-ignore - existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; - // @ts-ignore - existingFile.metadata = mergeMetadata([existingFile])[0].metadata; -} diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index 65ee371e9f..ebdfb362f2 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -10,11 +10,9 @@ 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, - updateRemotePublicMagicMetadata, -} from "ente-media/file-metadata"; +import { filePublicMagicMetadata } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; +import { updateFilePublicMagicMetadata } from "ente-new/photos/services/file"; import { getAllLocalFiles, getLocalTrashFileIDs, @@ -1040,7 +1038,7 @@ const processQueueItem = async ({ if (!res) { log.info(`Generate HLS for ${fileLogID(file)} | not-required`); // See: [Note: Marking files which do not need video processing] - await updateRemotePublicMagicMetadata(file, { sv: 1 }); + await updateFilePublicMagicMetadata(file, { sv: 1 }); return; } diff --git a/web/packages/new/photos/services/collection.ts b/web/packages/new/photos/services/collection.ts index a78c78f4f0..b185f0d671 100644 --- a/web/packages/new/photos/services/collection.ts +++ b/web/packages/new/photos/services/collection.ts @@ -9,7 +9,6 @@ import { import { authenticatedRequestHeaders, ensureOk } from "ente-base/http"; import { apiURL } from "ente-base/origins"; import { ensureMasterKeyFromSession } from "ente-base/session"; -import type { UpdateMagicMetadataRequest } from "ente-gallery/services/file"; import { CollectionSubType, decryptRemoteCollection, @@ -32,7 +31,7 @@ import { } from "ente-media/magic-metadata"; import { batch } from "ente-utils/array"; import { z } from "zod/v4"; -import { requestBatchSize } from "./file"; +import { requestBatchSize, type UpdateMagicMetadataRequest } from "./file"; import { ensureUserKeyPair, getPublicKey } from "./user"; const uncategorizedCollectionName = "Uncategorized"; From 9e80aeb0618d8704bea5eac8d86718c50200ea56 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:25:43 +0530 Subject: [PATCH 2/6] Cleanup --- .../photos/src/components/FixCreationTime.tsx | 6 +- web/packages/gallery/components/FileInfo.tsx | 11 +- .../gallery/components/viewer/FileViewer.tsx | 8 +- .../gallery/components/viewer/data-source.ts | 6 +- .../gallery/services/upload/upload-service.ts | 9 +- web/packages/gallery/services/video.ts | 5 +- web/packages/media/file-metadata.ts | 217 +++--------------- web/packages/media/file.ts | 55 +---- web/packages/new/photos/services/dedup.ts | 7 +- web/packages/new/photos/services/file.ts | 6 +- .../new/photos/services/search/worker.ts | 3 +- 11 files changed, 53 insertions(+), 280 deletions(-) diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx index 986f0a106b..bee5e78621 100644 --- a/web/apps/photos/src/components/FixCreationTime.tsx +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -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, { diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index 1d891e0b81..5d272738bb 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -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 = ({ onSelectPerson?.(personID); }; - const uploaderName = filePublicMagicMetadata(file)?.uploaderName; + const uploaderName = file.pubMagicMetadata?.data.uploaderName; return ( @@ -582,7 +580,7 @@ const Caption: React.FC = ({ }) => { 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 = ({ 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); diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index baa10f24d5..90323618fc 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -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 = ({ file && activeAnnotatedFile.annotation.showArchive ) { - switch (fileVisibility(file)) { + switch (file.magicMetadata?.data.visibility) { case undefined: case ItemVisibility.visible: isArchived = false; diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts index 1611c9e41f..f13df294b4 100644 --- a/web/packages/gallery/components/viewer/data-source.ts +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -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, 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, 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; diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 6a0cb9ba31..b6c89e9a58 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -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 => { const nonEmptyPublicMagicMetadataProps = getNonEmptyMagicMetadataProps( publicMagicMetadataProps, diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index ebdfb362f2..02a0b93c36 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -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 => { 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, ), ); diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index e75e6be8e2..73540f7ea8 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -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 => { - 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, ): Promise => { - 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, -) => { - 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 => { // 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. diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index e07c2645ea..11938fc429 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -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; /** * `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; export type FilePrivateMagicMetadata = MagicMetadataCore; -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; + MagicMetadataCore; export interface TrashItem extends Omit { 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 }; } diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index d86c9abaf7..0965c184b6 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -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) diff --git a/web/packages/new/photos/services/file.ts b/web/packages/new/photos/services/file.ts index 4716054e5c..e187d5c709 100644 --- a/web/packages/new/photos/services/file.ts +++ b/web/packages/new/photos/services/file.ts @@ -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( diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 5223e0b547..b088eef50f 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -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": { From c23cd8d2f0120c7e6972a403c1714ecc0adca172 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:37:00 +0530 Subject: [PATCH 3/6] Cleanup --- web/packages/media/file.ts | 79 ++++++------------- .../new/photos/services/collections.ts | 11 ++- web/packages/new/photos/services/files.ts | 2 +- web/packages/new/photos/services/trash.ts | 31 ++++++++ 4 files changed, 59 insertions(+), 64 deletions(-) create mode 100644 web/packages/new/photos/services/trash.ts diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index 11938fc429..4ad4947b04 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -484,11 +484,6 @@ export const FileDiffResponse = z.object({ hasMore: z.boolean(), }); -export interface FileWithUpdatedPublicMagicMetadata { - file: EnteFile; - updatedPublicMagicMetadata: FilePublicMagicMetadata; -} - export type FileMagicMetadata = MagicMetadataCore; export type FilePrivateMagicMetadata = MagicMetadataCore; @@ -496,58 +491,6 @@ export type FilePrivateMagicMetadata = export type FilePublicMagicMetadata = MagicMetadataCore; -export interface TrashItem extends Omit { - file: EnteFile; -} - -export interface EncryptedTrashItem { - file: EncryptedEnteFile; - /** - * `true` if the file no longer in trash because it was permanently deleted. - * - * This field is relevant when we obtain a trash item as part of the trash - * diff. It indicates that the file which was previously in trash is no - * longer in the trash because it was permanently deleted. - */ - isDeleted: boolean; - /** - * `true` if the file no longer in trash because it was restored to some - * collection. - * - * This field is relevant when we obtain a trash item as part of the trash - * diff. It indicates that the file which was previously in trash is no - * longer in the trash because it was restored to a collection. - */ - isRestored: boolean; - deleteBy: number; - createdAt: number; - updatedAt: number; -} - -export type Trash = TrashItem[]; - -/** - * A short identifier for a file in log messages. - * - * e.g. "file flower.png (827233681)" - * - * @returns a string to use as an identifier when logging information about the - * given {@link file}. The returned string contains the file name (for ease of - * debugging) and the file ID (for exactness). - */ -export const fileLogID = (file: EnteFile) => - `file ${fileFileName(file)} (${file.id})`; - -/** - * Return the date when the file will be deleted permanently. Only valid for - * files that are in the user's trash. - * - * This is a convenience wrapper over the {@link deleteBy} property of a file, - * converting that epoch microsecond value into a JavaScript date. - */ -export const enteFileDeletionDate = (file: EnteFile) => - dateFromEpochMicroseconds(file.deleteBy); - export async function decryptFile( file: EncryptedEnteFile, collectionKey: string, @@ -798,6 +741,28 @@ export const mergeMetadata1 = (file: EnteFile): EnteFile => { export const mergeMetadata = (files: EnteFile[]) => files.map((file) => mergeMetadata1(file)); +/** + * A short identifier for a file in log messages. + * + * e.g. "file flower.png (827233681)" + * + * @returns a string to use as an identifier when logging information about the + * given {@link file}. The returned string contains the file name (for ease of + * debugging) and the file ID (for exactness). + */ +export const fileLogID = (file: EnteFile) => + `file ${fileFileName(file)} (${file.id})`; + +/** + * Return the date when the file will be deleted permanently. Only valid for + * files that are in the user's trash. + * + * This is a convenience wrapper over the {@link deleteBy} property of a file, + * converting that epoch microsecond value into a JavaScript date. + */ +export const enteFileDeletionDate = (file: EnteFile) => + dateFromEpochMicroseconds(file.deleteBy); + export interface MagicMetadataCore { version: number; count: number; diff --git a/web/packages/new/photos/services/collections.ts b/web/packages/new/photos/services/collections.ts index f04d9320c8..904780279c 100644 --- a/web/packages/new/photos/services/collections.ts +++ b/web/packages/new/photos/services/collections.ts @@ -6,17 +6,16 @@ import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { type Collection } from "ente-media/collection"; -import { - decryptFile, - type EncryptedTrashItem, - type EnteFile, - type Trash, -} from "ente-media/file"; +import { decryptFile, type EnteFile } from "ente-media/file"; import { getLocalTrash, getTrashedFiles, TRASH, } from "ente-new/photos/services/files"; +import { + type EncryptedTrashItem, + type Trash, +} from "ente-new/photos/services/trash"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; import { getToken } from "ente-shared/storage/localStorage/helpers"; diff --git a/web/packages/new/photos/services/files.ts b/web/packages/new/photos/services/files.ts index fadbb67c9a..293b9749cb 100644 --- a/web/packages/new/photos/services/files.ts +++ b/web/packages/new/photos/services/files.ts @@ -7,9 +7,9 @@ import { mergeMetadata, type EncryptedEnteFile, type EnteFile, - type Trash, } from "ente-media/file"; import { metadataHash } from "ente-media/file-metadata"; +import { type Trash } from "ente-new/photos/services/trash"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; import { getToken } from "ente-shared/storage/localStorage/helpers"; diff --git a/web/packages/new/photos/services/trash.ts b/web/packages/new/photos/services/trash.ts new file mode 100644 index 0000000000..0635e3a37b --- /dev/null +++ b/web/packages/new/photos/services/trash.ts @@ -0,0 +1,31 @@ +import type { EncryptedEnteFile, EnteFile } from "ente-media/file"; + +export interface TrashItem extends Omit { + file: EnteFile; +} + +export interface EncryptedTrashItem { + file: EncryptedEnteFile; + /** + * `true` if the file no longer in trash because it was permanently deleted. + * + * This field is relevant when we obtain a trash item as part of the trash + * diff. It indicates that the file which was previously in trash is no + * longer in the trash because it was permanently deleted. + */ + isDeleted: boolean; + /** + * `true` if the file no longer in trash because it was restored to some + * collection. + * + * This field is relevant when we obtain a trash item as part of the trash + * diff. It indicates that the file which was previously in trash is no + * longer in the trash because it was restored to a collection. + */ + isRestored: boolean; + deleteBy: number; + createdAt: number; + updatedAt: number; +} + +export type Trash = TrashItem[]; From 012a3bef0ad2460910fbfd76d1eac3762c140e88 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:40:54 +0530 Subject: [PATCH 4/6] Replace --- web/apps/photos/src/pages/gallery.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index e0e25e98c6..62071ed114 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -38,10 +38,7 @@ import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload"; import { type Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; -import { - updateRemotePrivateMagicMetadata, - type ItemVisibility, -} from "ente-media/file-metadata"; +import { type ItemVisibility } from "ente-media/file-metadata"; import { CollectionSelector, type CollectionSelectorAttributes, @@ -78,6 +75,7 @@ import { } from "ente-new/photos/services/collection-summary"; import { getAllLocalCollections } from "ente-new/photos/services/collections"; import exportService from "ente-new/photos/services/export"; +import { updateFilesVisibility } from "ente-new/photos/services/file"; import { getLocalFiles, getLocalTrashedFiles, @@ -847,14 +845,16 @@ const Page: React.FC = () => { const fileID = file.id; dispatch({ type: "addPendingVisibilityUpdate", fileID }); try { - const privateMagicMetadata = - await updateRemotePrivateMagicMetadata(file, { - visibility, - }); + await updateFilesVisibility([file], visibility); + // TODO: Replace with file fetch? dispatch({ type: "unsyncedPrivateMagicMetadataUpdate", fileID, - privateMagicMetadata, + privateMagicMetadata: { + ...file.magicMetadata, + version: (file.magicMetadata?.version ?? 0) + 1, + data: { ...file.magicMetadata?.data, visibility }, + }, }); } finally { dispatch({ type: "removePendingVisibilityUpdate", fileID }); From d9fa30dbef663d1b3f60d3bb642895e4262433ef Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:44:26 +0530 Subject: [PATCH 5/6] Unused --- web/apps/photos/src/pages/gallery.tsx | 12 +- web/packages/media/file-metadata.ts | 160 +------------------------- 2 files changed, 12 insertions(+), 160 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 62071ed114..5edfb77f6c 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -846,7 +846,17 @@ const Page: React.FC = () => { dispatch({ type: "addPendingVisibilityUpdate", fileID }); try { await updateFilesVisibility([file], visibility); - // TODO: Replace with file fetch? + // [Note: Interactive updates to file metadata] + // + // 1. Update the remote metadata. + // + // 2. Construct a fake a metadata object with the updates + // reflected in it. + // + // 3. The caller (eventually) triggers a remote sync in the + // background, but meanwhile uses this updated metadata. + // + // TODO(RE): Replace with file fetch? dispatch({ type: "unsyncedPrivateMagicMetadataUpdate", fileID, diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 73540f7ea8..17d568ea7c 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -1,17 +1,8 @@ -import { encryptMetadataJSON } from "ente-base/crypto"; -import { authenticatedRequestHeaders, ensureOk } from "ente-base/http"; -import { apiURL } from "ente-base/origins"; import { type Location } from "ente-base/types"; -import { - type EnteFile, - type EnteFile2, - type FileMagicMetadata, - type FilePrivateMagicMetadata, -} from "ente-media/file"; +import { type EnteFile, type EnteFile2 } from "ente-media/file"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; import { FileType } from "./file-type"; -import type { RemoteMagicMetadata } from "./magic-metadata"; /** * Information about the file that never changes post upload. @@ -489,155 +480,6 @@ export const fileCreationPhotoDate = (file: EnteFile) => file.metadata.creationTime, ); -/** - * Update the private magic metadata associated with a file on remote. - * - * @param file The {@link EnteFile} whose public magic metadata we want to - * update. - * - * @param metadataUpdates A subset of {@link FilePrivateMagicMetadataData} containing - * the fields that we want to add or update. - * - * @returns An updated {@link FilePrivateMagicMetadataData} object containing the - * (decrypted) metadata updates we just made. This is effectively what we would - * get if we to ask the remote for the latest file for this ID, except we don't - * do an actual sync and instead reconstruct it piecemeal. - * - * [Note: Interactive updates to file metadata] - * - * This function updates the magic metadata on remote, and returns a magic - * metadata object with the updated (and decrypted) values, but it does not - * update the state of the file objects in our databases. - * - * The caller needs to ensure that we subsequently sync with remote to fetch the - * updates as part of the diff and update the {@link EnteFile} that is persisted - * in our local db. - * - * This partial update approach is used because a full sync requires multiple - * API calls, which can cause a slow experience for interactive operations (e.g. - * archiving a file). So this function does not immediately perform the sync, - * but instead expects the caller to arrange for an eventual delayed sync in the - * background without waiting for it to complete. - * - * Returning a modified in-memory object is essential because in addition to the - * updated metadata itself, the metadatum (See: [Note: Metadatum]) contain a - * version field that is incremented for each change. So if we were not to - * update the version, and if the user were to perform another operation on that - * file before the asynchronous remote sync completes, the client will send a - * stale version of the metadata, and remote will reject the update. - * - * The overall sequence is thus: - * - * 1. This function modifies the remote metadata. - * - * 2. It returns a metadata object with the updates reflected in it. - * - * 3. The caller (eventually) triggers a remote sync in the background, but - * meanwhile uses this updated metadata. - */ -export const updateRemotePrivateMagicMetadata = async ( - file: EnteFile, - metadataUpdates: Partial, -): Promise => { - const existingMetadata = file.magicMetadata?.data; - - const updatedMetadata = { ...(existingMetadata ?? {}), ...metadataUpdates }; - - const metadataVersion = file.magicMetadata?.version ?? 1; - - const updateRequest = await updateMagicMetadataRequest( - file, - updatedMetadata, - metadataVersion, - ); - - const updatedEnvelope = updateRequest.metadataList[0]!.magicMetadata; - - await putFilesPrivateMagicMetadata(updateRequest); - - // See: [Note: Interactive updates to file metadata] - - // Use the updated envelope we sent as a starting point for the metadata we - // will use for the updated file. - const updatedMagicMetadata = updatedEnvelope as FileMagicMetadata; - // The correct version will come in the updated EnteFile we get in the - // response of the /diff. Temporarily bump it to reflect our latest edit. - updatedMagicMetadata.version = metadataVersion + 1; - // Set the contents (data) to the updated metadata contents we just PUT. - updatedMagicMetadata.data = updatedMetadata; - - return updatedMagicMetadata; -}; - -/** - * The shape of the JSON body payload expected by the APIs that update the - * public and private magic metadata fields associated with a file. - */ -interface UpdateMagicMetadataRequest { - /** The list of (file id, new magic metadata) pairs to update */ - metadataList: { - /** File ID */ - id: number; - /** The new metadata to use */ - magicMetadata: RemoteMagicMetadata; - }[]; -} - -/** - * Construct an remote update request payload from the public or private magic - * metadata JSON object for a {@link file}, using the provided - * {@link encryptMetadataF} function to encrypt the JSON. - */ -const updateMagicMetadataRequest = async ( - file: EnteFile, - metadata: FilePrivateMagicMetadataData | FilePublicMagicMetadataData, - metadataVersion: number, -): Promise => { - // Drop all null or undefined values to obtain the syncable entries. - // See: [Note: Optional magic metadata keys]. - const validEntries = Object.entries(metadata).filter( - ([, v]) => v !== null && v !== undefined, - ); - - const { encryptedData, decryptionHeader } = await encryptMetadataJSON( - Object.fromEntries(validEntries), - file.key, - ); - - return { - metadataList: [ - { - id: file.id, - magicMetadata: { - version: metadataVersion, - count: validEntries.length, - data: encryptedData, - header: decryptionHeader, - }, - }, - ], - }; -}; - -/** - * Update the (private) magic metadata for a list of files. - * - * See: [Note: Private magic metadata is called magic metadata on remote] - * - * @param request The list of file ids and the updated encrypted magic metadata - * associated with each of them. - */ -const putFilesPrivateMagicMetadata = async ( - request: UpdateMagicMetadataRequest, -) => - ensureOk( - await fetch(await apiURL("/files/magic-metadata"), { - method: "PUT", - headers: await authenticatedRequestHeaders(), - body: JSON.stringify(request), - }), - ); - /** * Return the GPS coordinates (if any) present in the given {@link EnteFile}. */ From 63fc0663010d3f4386c920526bb3d34b58fca30b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Jun 2025 17:53:49 +0530 Subject: [PATCH 6/6] Schema --- web/packages/media/file-metadata.ts | 31 +++++++++++------------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 17d568ea7c..89c3ef7491 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -278,8 +278,6 @@ export type ItemVisibility = * And never like: * * foo: T | undefined - * - * Also see: [Note: Zod doesn't work with `exactOptionalPropertyTypes` yet]. */ export interface FilePublicMagicMetadataData { /** @@ -392,10 +390,6 @@ export interface FilePublicMagicMetadataData { /** * Zod schema for the {@link PublicMagicMetadata} type. * - * See: [Note: Duplicated Zod schema and TypeScript type] - * - * --- - * * [Note: Use looseObject for metadata Zod schemas] * * It is important to (recursively) use the {@link looseObject} option when @@ -404,19 +398,18 @@ export interface FilePublicMagicMetadataData { * 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. */ -// 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 - // 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(), +export const FilePublicMagicMetadataData = z.looseObject({ + dateTime: z.string().nullish().transform(nullToUndefined), + offsetTime: z.string().nullish().transform(nullToUndefined), + editedTime: z.number().nullish().transform(nullToUndefined), + editedName: z.string().nullish().transform(nullToUndefined), + w: z.number().nullish().transform(nullToUndefined), + h: z.number().nullish().transform(nullToUndefined), + caption: z.string().nullish().transform(nullToUndefined), + uploaderName: z.string().nullish().transform(nullToUndefined), + lat: z.number().nullish().transform(nullToUndefined), + long: z.number().nullish().transform(nullToUndefined), + sv: z.number().nullish().transform(nullToUndefined), }); /**