[web] Switch to new EnteFile TypeScript type (internal) (#6323)

This commit is contained in:
Manav Rathi
2025-06-20 20:30:32 +05:30
committed by GitHub
15 changed files with 169 additions and 412 deletions

View File

@@ -7,7 +7,7 @@ import {
decryptRemoteFile,
FileDiffResponse,
RemoteEnteFile,
type EnteFile2,
type EnteFile,
} from "ente-media/file";
import { fileFileName } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
@@ -175,7 +175,7 @@ export const getRemoteCastCollectionFiles = async (
return files;
};
const isFileEligible = (file: EnteFile2) => {
const isFileEligible = (file: EnteFile) => {
if (!isImageOrLivePhoto(file)) return false;
if ((file.info?.fileSize ?? 0) > 100 * 1024 * 1024) return false;
@@ -193,7 +193,7 @@ const isFileEligible = (file: EnteFile2) => {
return true;
};
const isImageOrLivePhoto = (file: EnteFile2) =>
const isImageOrLivePhoto = (file: EnteFile) =>
file.metadata.fileType == FileType.image ||
file.metadata.fileType == FileType.livePhoto;
@@ -204,12 +204,12 @@ const isImageOrLivePhoto = (file: EnteFile2) =>
* Once we're done showing the file, the URL should be revoked using
* {@link URL.revokeObjectURL} to free up browser resources.
*/
const createRenderableURL = async (castToken: string, file: EnteFile2) => {
const createRenderableURL = async (castToken: string, file: EnteFile) => {
const imageBlob = await renderableImageBlob(castToken, file);
return URL.createObjectURL(imageBlob);
};
const renderableImageBlob = async (castToken: string, file: EnteFile2) => {
const renderableImageBlob = async (castToken: string, file: EnteFile) => {
const shouldUseThumbnail = isChromecast();
let blob = await downloadFile(castToken, file, shouldUseThumbnail);
@@ -239,7 +239,7 @@ const renderableImageBlob = async (castToken: string, file: EnteFile2) => {
const downloadFile = async (
castToken: string,
file: EnteFile2,
file: EnteFile,
shouldUseThumbnail: boolean,
) => {
if (!isImageOrLivePhoto(file))

View File

@@ -10,7 +10,7 @@ import { isDevBuild } from "ente-base/env";
import { formattedDateRelative } from "ente-base/i18n-date";
import log from "ente-base/log";
import { downloadManager } from "ente-gallery/services/download";
import { EnteFile, enteFileDeletionDate } from "ente-media/file";
import { EnteFile } from "ente-media/file";
import { fileDurationString } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import {
@@ -26,6 +26,7 @@ import {
} from "ente-new/photos/components/PlaceholderThumbnails";
import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles";
import { PseudoCollectionID } from "ente-new/photos/services/collection-summary";
import { enteFileDeletionDate } from "ente-new/photos/services/files";
import { t } from "i18next";
import memoize from "memoize-one";
import { GalleryContext } from "pages/gallery";
@@ -1283,6 +1284,9 @@ const FileThumbnail: React.FC<FileThumbnailProps> = ({
)}
{activeCollectionID == PseudoCollectionID.trash &&
// TODO(RE):
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
file.isTrashed && (
<TileBottomTextOverlay>
<Typography variant="small">

View File

@@ -1,16 +1,13 @@
import { sharedCryptoWorker } from "ente-base/crypto";
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import { type MagicMetadataCore } from "ente-gallery/services/magic-metadata";
import type {
Collection,
CollectionPublicMagicMetadataData,
} from "ente-media/collection";
import type {
EncryptedEnteFile,
EnteFile,
MagicMetadataCore,
} from "ente-media/file";
import { decryptFile, mergeMetadata } from "ente-media/file";
import type { EnteFile, RemoteEnteFile } from "ente-media/file";
import { decryptRemoteFile, mergeMetadata } from "ente-media/file";
import { savedPublicCollections } from "ente-new/albums/services/public-albums-fdb";
import { sortFiles } from "ente-new/photos/services/files";
import { CustomError, parseSharingErrorCodes } from "ente-shared/error";
@@ -220,7 +217,8 @@ export const syncPublicFiles = async (
files = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, file] of latestVersionFiles) {
if (file.isDeleted) {
// TODO(RE):
if ("isDeleted" in file && file.isDeleted) {
continue;
}
files.push(file);
@@ -275,9 +273,12 @@ const getPublicFiles = async (
decryptedFiles = [
...decryptedFiles,
...(await Promise.all(
resp.data.diff.map(async (file: EncryptedEnteFile) => {
resp.data.diff.map(async (file: RemoteEnteFile) => {
if (!file.isDeleted) {
return await decryptFile(file, collection.key);
return await decryptRemoteFile(
file,
collection.key,
);
} else {
return file;
}
@@ -292,7 +293,9 @@ const getPublicFiles = async (
sortFiles(
mergeMetadata(
[...(files || []), ...decryptedFiles].filter(
(item) => !item.isDeleted,
// TODO(RE):
// (item) => !item.isDeleted,
(file) => !("isDeleted" in file && file.isDeleted),
),
),
sortAsc,

View File

@@ -30,9 +30,9 @@ import UploadService, {
import { processVideoNewUpload } from "ente-gallery/services/video";
import type { Collection } from "ente-media/collection";
import {
decryptFile,
type EncryptedEnteFile,
decryptRemoteFile,
type EnteFile,
type RemoteEnteFile,
} from "ente-media/file";
import type { ParsedMetadata } from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
@@ -544,7 +544,7 @@ class UploadManager {
private async postUploadTask(
uploadableItem: UploadableUploadItem,
uploadResult: UploadResult,
uploadedFile: EncryptedEnteFile | EnteFile | undefined,
uploadedFile: RemoteEnteFile | EnteFile | undefined,
): Promise<FinishedUploadResult> {
log.info(`Upload ${uploadableItem.fileName} | ${uploadResult}`);
const finishedUploadResult =
@@ -565,8 +565,8 @@ class UploadManager {
break;
case "uploaded":
case "uploadedWithStaticThumbnail":
decryptedFile = await decryptFile(
uploadedFile as EncryptedEnteFile,
decryptedFile = await decryptRemoteFile(
uploadedFile as RemoteEnteFile,
uploadableItem.collection.key,
);
break;
@@ -598,11 +598,17 @@ class UploadManager {
}
this.updateExistingFiles(decryptedFile);
}
await this.watchFolderCallback(
uploadResult,
uploadableItem,
uploadedFile as EncryptedEnteFile,
);
if (isDesktop) {
if (watcher.isUploadRunning()) {
await watcher.onFileUpload(
uploadResult,
uploadableItem,
uploadedFile,
);
}
}
return finishedUploadResult;
} catch (e) {
log.error("Post file upload action failed", e);
@@ -610,22 +616,6 @@ class UploadManager {
}
}
private async watchFolderCallback(
fileUploadResult: UploadResult,
fileWithCollection: ClusteredUploadItem,
uploadedFile: EncryptedEnteFile,
) {
if (isDesktop) {
if (watcher.isUploadRunning()) {
await watcher.onFileUpload(
fileUploadResult,
fileWithCollection,
uploadedFile,
);
}
}
}
public cancelRunningUpload() {
log.info("User cancelled upload");
this.uiService.setUploadPhase("cancelling");

View File

@@ -14,7 +14,6 @@ import type {
} from "ente-base/types/ipc";
import { type UploadResult } from "ente-gallery/services/upload";
import type { UploadAsset } from "ente-gallery/services/upload/upload-service";
import { EncryptedEnteFile } from "ente-media/file";
import {
getLocalFiles,
groupFilesByCollectionID,
@@ -23,6 +22,11 @@ import { ensureString } from "ente-utils/ensure";
import { removeFromCollection } from "./collectionService";
import { type UploadItemWithCollection, uploadManager } from "./upload-manager";
interface FolderWatchUploadedFile {
id: number;
collectionID: number;
}
/**
* Watch for file system folders and automatically update the corresponding Ente
* collections.
@@ -46,10 +50,10 @@ class FolderWatcher {
/** `true` if we are temporarily paused to let a user upload go through. */
private isPaused = false;
/**
* A map from file paths to an Ente file for files that were uploaded (or
* symlinked) as part of the most recent upload attempt.
* A map from file paths to the (fileID, collectionID) of the file that was
* uploaded (or symlinked) as part of the most recent upload attempt.
*/
private uploadedFileForPath = new Map<string, EncryptedEnteFile>();
private uploadedFileForPath = new Map<string, FolderWatchUploadedFile>();
/**
* A set of file paths that could not be uploaded in the most recent upload
* attempt. These are the uploads that failed due to a permanent error that
@@ -324,7 +328,7 @@ class FolderWatcher {
async onFileUpload(
fileUploadResult: UploadResult,
item: UploadItemWithCollection,
file: EncryptedEnteFile,
file: FolderWatchUploadedFile,
) {
// Re the usage of ensureString: For desktop watch, the only possibility
// for a UploadItem is for it to be a string (the absolute path to a
@@ -404,7 +408,7 @@ class FolderWatcher {
const syncedFiles: FolderWatch["syncedFiles"] = [];
const ignoredFiles: FolderWatch["ignoredFiles"] = [];
const markSynced = (file: EncryptedEnteFile, path: string) => {
const markSynced = (file: FolderWatchUploadedFile, path: string) => {
syncedFiles.push({
path,
uploadedFileID: file.id,

View File

@@ -3,9 +3,15 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { sharedCryptoWorker } from "ente-base/crypto";
import type { Collection } from "ente-media/collection";
import { type MagicMetadataCore } from "ente-media/file";
import { ItemVisibility } from "ente-media/file-metadata";
export interface MagicMetadataCore<T> {
version: number;
count: number;
header: string;
data: T;
}
export const isArchivedCollection = (item: Collection) => {
if (!item) {
return false;

View File

@@ -7,11 +7,8 @@ import {
type PublicAlbumsCredentials,
} from "ente-base/http";
import { apiURL, uploaderOrigin } from "ente-base/origins";
import {
type EncryptedEnteFile,
type EncryptedMagicMetadata,
type RemoteFileMetadata,
} from "ente-media/file";
import { type RemoteEnteFile, type RemoteFileMetadata } from "ente-media/file";
import type { RemoteMagicMetadata } from "ente-media/magic-metadata";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
@@ -411,7 +408,7 @@ export interface PostEnteFileRequest {
file: UploadedFileObjectAttributes;
thumbnail: UploadedFileObjectAttributes;
metadata: RemoteFileMetadata;
pubMagicMetadata: EncryptedMagicMetadata;
pubMagicMetadata?: RemoteMagicMetadata;
}
/**
@@ -476,7 +473,7 @@ export interface UploadedFileObjectAttributes {
*/
export const postEnteFile = async (
postFileRequest: PostEnteFileRequest,
): Promise<EncryptedEnteFile> => {
): Promise<RemoteEnteFile> => {
const res = await fetch(await apiURL("/files"), {
method: "POST",
headers: await authenticatedRequestHeaders(),
@@ -484,7 +481,7 @@ export const postEnteFile = async (
});
ensureOk(res);
// TODO(RE):
return (await res.json()) as EncryptedEnteFile;
return (await res.json()) as RemoteEnteFile;
};
/**
@@ -494,7 +491,7 @@ export const postPublicAlbumsEnteFile = async (
postFileRequest: PostEnteFileRequest,
credentials: PublicAlbumsCredentials,
): Promise<EncryptedEnteFile> => {
): Promise<RemoteEnteFile> => {
const res = await fetch(await apiURL("/public-collection/file"), {
method: "POST",
headers: authenticatedPublicAlbumsRequestHeaders(credentials),
@@ -502,5 +499,5 @@ export const postPublicAlbumsEnteFile = async (
});
ensureOk(res);
// TODO(RE):
return (await res.json()) as EncryptedEnteFile;
return (await res.json()) as RemoteEnteFile;
};

View File

@@ -19,21 +19,12 @@ import {
determineVideoDuration,
extractVideoMetadata,
} from "ente-gallery/services/ffmpeg";
import {
getNonEmptyMagicMetadataProps,
updateMagicMetadata,
} from "ente-gallery/services/magic-metadata";
import {
detectFileTypeInfoFromChunk,
isFileTypeNotSupportedError,
} from "ente-gallery/utils/detect-type";
import { readStream } from "ente-gallery/utils/native-stream";
import type {
EncryptedEnteFile,
EncryptedMagicMetadata,
EnteFile,
FilePublicMagicMetadata,
} from "ente-media/file";
import type { EnteFile, RemoteEnteFile } from "ente-media/file";
import {
fileFileName,
metadataHash,
@@ -43,6 +34,11 @@ import {
} from "ente-media/file-metadata";
import { FileType, type FileTypeInfo } from "ente-media/file-type";
import { encodeLivePhoto } from "ente-media/live-photo";
import {
createMagicMetadata,
encryptMagicMetadata,
type RemoteMagicMetadata,
} from "ente-media/magic-metadata";
import { addToCollection } from "ente-new/photos/services/collection";
import { mergeUint8Arrays } from "ente-utils/array";
import { ensureInteger, ensureNumber } from "ente-utils/ensure";
@@ -279,9 +275,9 @@ interface ThumbnailedFile {
}
interface FileWithMetadata extends Omit<ThumbnailedFile, "hasStaticThumbnail"> {
metadata: FileMetadata;
localID: number;
pubMagicMetadata: FilePublicMagicMetadata;
metadata: FileMetadata;
publicMagicMetadata: FilePublicMagicMetadataData;
}
interface EncryptedFileStream {
@@ -320,7 +316,7 @@ interface EncryptedFilePieces {
* the decryption header that was used during encryption (base64 string).
*/
metadata: { encryptedData: string; decryptionHeader: string };
pubMagicMetadata: EncryptedMagicMetadata;
pubMagicMetadata: RemoteMagicMetadata | undefined;
localID: number;
}
@@ -641,7 +637,7 @@ interface UploadContext {
interface UploadResponse {
uploadResult: UploadResult;
uploadedFile?: EncryptedEnteFile | EnteFile;
uploadedFile?: RemoteEnteFile | EnteFile;
}
/**
@@ -741,11 +737,6 @@ export const upload = async (
if (hasStaticThumbnail) metadata.hasStaticThumbnail = true;
const pubMagicMetadata = await constructPublicMagicMetadata({
...publicMagicMetadata,
uploaderName,
});
abortIfCancelled();
const fileWithMetadata: FileWithMetadata = {
@@ -753,7 +744,10 @@ export const upload = async (
fileStreamOrData,
thumbnail,
metadata,
pubMagicMetadata,
publicMagicMetadata: {
...publicMagicMetadata,
...(uploaderName && { uploaderName }),
},
};
const { encryptedFilePieces, encryptedFileKey } = await encryptFile(
@@ -1488,20 +1482,6 @@ const augmentWithThumbnail = async (
};
};
const constructPublicMagicMetadata = async (
publicMagicMetadataProps: FilePublicMagicMetadataData,
): Promise<FilePublicMagicMetadata> => {
const nonEmptyPublicMagicMetadataProps = getNonEmptyMagicMetadataProps(
publicMagicMetadataProps,
);
if (Object.values(nonEmptyPublicMagicMetadataProps).length === 0) {
// @ts-ignore
return null;
}
return await updateMagicMetadata(publicMagicMetadataProps);
};
const encryptFile = async (
file: FileWithMetadata,
collectionKey: string,
@@ -1509,8 +1489,13 @@ const encryptFile = async (
) => {
const fileKey = await worker.generateBlobOrStreamKey();
const { fileStreamOrData, thumbnail, metadata, pubMagicMetadata, localID } =
file;
const {
fileStreamOrData,
thumbnail,
metadata,
publicMagicMetadata,
localID,
} = file;
const encryptedFiledata =
fileStreamOrData instanceof Uint8Array
@@ -1532,18 +1517,13 @@ const encryptFile = async (
fileKey,
);
let encryptedPubMagicMetadata: EncryptedMagicMetadata;
// Keep defensive check until the underlying type is audited.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (pubMagicMetadata) {
const { encryptedData, decryptionHeader } =
await worker.encryptMetadataJSON(pubMagicMetadata.data, fileKey);
encryptedPubMagicMetadata = {
version: pubMagicMetadata.version,
count: pubMagicMetadata.count,
data: encryptedData,
header: decryptionHeader,
};
let encryptedPubMagicMetadata: RemoteMagicMetadata | undefined;
const pubMagicMetadata = createMagicMetadata(publicMagicMetadata);
if (pubMagicMetadata.count) {
encryptedPubMagicMetadata = await encryptMagicMetadata(
pubMagicMetadata,
fileKey,
);
}
const encryptedFileKey = await worker.encryptBox(fileKey, collectionKey);
@@ -1553,7 +1533,6 @@ const encryptFile = async (
file: encryptedFiledata,
thumbnail: encryptedThumbnail,
metadata: encryptedMetadata,
// @ts-ignore
pubMagicMetadata: encryptedPubMagicMetadata,
localID: localID,
},

View File

@@ -1,5 +1,5 @@
import { type Location } from "ente-base/types";
import { type EnteFile, type EnteFile2 } from "ente-media/file";
import { type EnteFile } from "ente-media/file";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
import { FileType } from "./file-type";
@@ -456,7 +456,7 @@ export const isArchivedFile = (file: EnteFile) =>
* @returns The provided {@link EnteFile}'s filename, including the extension.
* e.g. "flower.png".
*/
export const fileFileName = (file: EnteFile | EnteFile2) =>
export const fileFileName = (file: EnteFile) =>
file.pubMagicMetadata?.data.editedName ?? file.metadata.title;
/**

View File

@@ -1,9 +1,4 @@
import {
decryptBox,
decryptMetadataJSON,
sharedCryptoWorker,
} from "ente-base/crypto";
import { dateFromEpochMicroseconds } from "ente-base/date";
import { decryptBox, decryptMetadataJSON } from "ente-base/crypto";
import log from "ente-base/log";
import { nullishToBlank, nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
@@ -11,11 +6,15 @@ import { ignore } from "./collection";
import {
fileFileName,
FileMetadata,
type FilePrivateMagicMetadataData,
type FilePublicMagicMetadataData,
FilePrivateMagicMetadataData,
FilePublicMagicMetadataData,
} from "./file-metadata";
import { FileType } from "./file-type";
import { decryptMagicMetadata, RemoteMagicMetadata } from "./magic-metadata";
import {
decryptMagicMetadata,
RemoteMagicMetadata,
type MagicMetadata,
} from "./magic-metadata";
/**
* A File.
@@ -77,7 +76,7 @@ import { decryptMagicMetadata, RemoteMagicMetadata } from "./magic-metadata";
* When a file is permanently deleted, remote will scrub off data from its
* fields. See: [Note: Optionality of remote file fields].
*/
export interface EnteFile2 {
export interface EnteFile {
/**
* The file's globally unique ID.
*
@@ -164,7 +163,7 @@ export interface EnteFile2 {
*
* See: [Note: Metadatum]
*/
magicMetadata?: FileMagicMetadata;
magicMetadata?: MagicMetadata<FilePrivateMagicMetadataData>;
/**
* Public mutable metadata associated with the file that is visible to all
* users with whom the file has been shared.
@@ -175,149 +174,7 @@ export interface EnteFile2 {
*
* See: [Note: Metadatum]
*/
pubMagicMetadata?: FilePublicMagicMetadata;
}
export interface EncryptedEnteFile {
/**
* The file's globally unique ID.
*
* The file's ID is a integer assigned by remote as the identifier for an
* {@link EnteFile} when it is created. It is globally unique across all
* files stored by an Ente instance, and is not scoped to the current user.
*/
id: number;
/**
* The ID of the collection with which this file as associated.
*
* The same file (ID) may be associated with multiple collectionID, each of
* which will come and stay as distinct {@link EnteFile} instances - all of
* which will have the same {@link id} but distinct {@link collectionID}.
*
* So the ({@link id}, {@link collectionID}) pair is a primary key, not the
* {@link id} on its own. See: [Note: Collection file].
*/
collectionID: number;
/**
* The ID of the Ente user who owns the file.
*
* Files uploaded by non users on public links belong to the owner of the
* collection who created the public link (See {@link uploaderName} in
* {@link FilePublicMagicMetadataData}).
*/
ownerID: number;
/**
* Information pertaining to the encrypted S3 object that has the file's
* contents.
*/
file: FileObjectAttributes;
/**
* Information pertaining to the encrypted S3 object that has the contents
* of the file's thumbnail.
*/
thumbnail: FileObjectAttributes;
/**
* Static, remote visible, information associated with a file.
*
* This is information about storage used by the file and its thumbnail.
* Unlike {@link metadata} which is E2EE, the {@link FileInfo} is remote
* visible for bookkeeping purposes.
*
* Files uploaded by very old versions of Ente might not have this field.
*/
info?: FileInfo;
metadata: RemoteFileMetadata;
magicMetadata: EncryptedMagicMetadata;
pubMagicMetadata: EncryptedMagicMetadata;
/**
* The file's encryption key (as a base64 string), encrypted by the key of
* the collection to which it belongs.
*
* (note: This is always present. retaining this note until we remove
* nullability uncertainty from the types).
*/
encryptedKey: string;
/**
* The nonce (as a base64 string) that was used when encrypting the file's
* encryption key.
*
* (note: This is always present. retaining this note until we remove
* nullability uncertainty from the types).
*/
keyDecryptionNonce: string;
isDeleted: boolean;
/**
* The last time the file was updated (epoch microseconds).
*
* (e.g. magic metadata updates).
*/
updationTime: number;
}
export interface EnteFile
extends Omit<
EncryptedEnteFile,
| "metadata"
| "pubMagicMetadata"
| "magicMetadata"
| "encryptedKey"
| "keyDecryptionNonce"
> {
/**
* The file's key.
*
* This is the base64 representation of the decrypted encryption key
* associated with this file. When we get the file from remote (as a
* {@link RemoteEnteFile}), the file key itself would have been encrypted by
* the key of the {@link Collection} to which this file belongs.
*
* This key is used to encrypt both the file's contents, and any associated
* data (e.g., metadatum, thumbnail) for the file.
*/
key: string;
/**
* Public static metadata associated with a file.
*
* This is the immutable metadata that gets associated with a file when it
* is uploaded, and there after cannot be changed.
*
* It is visible to all users with whom the file gets shared.
*
* > {@link pubMagicMetadata} contains fields that override fields present
* > in the metadata. Clients overlay those atop the metadata fields, and
* > thus they can be used to implement edits.
*
* See: [Note: Metadatum].
*/
metadata: FileMetadata;
/**
* Private mutable metadata associated with the file that is only visible to
* the owner of the file.
*
* See: [Note: Metadatum]
*/
magicMetadata?: FileMagicMetadata;
/**
* Public mutable metadata associated with the file that is visible to all
* users with whom the file has been shared.
*
* While in almost all cases, files will have associated public magic
* metadata since newer clients have something or the other they need to add
* to it, its presence is not guaranteed.
*
* See: [Note: Metadatum]
*/
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).
*/
isTrashed?: boolean;
/**
* If {@link isTrashed} is `true`, then {@link deleteBy} contains the epoch
* microseconds when this file will be permanently deleted.
*/
deleteBy?: number;
pubMagicMetadata?: MagicMetadata<FilePublicMagicMetadataData>;
}
/**
@@ -421,7 +278,6 @@ export type RemoteFileMetadata = z.infer<typeof RemoteFileMetadata>;
*
* See: [Note: Use looseObject when parsing JSON that will get persisted]
*/
// TODO(RE): Use me
export const RemoteEnteFile = z.looseObject({
id: z.number(),
collectionID: z.number(),
@@ -452,7 +308,6 @@ export const RemoteEnteFile = z.looseObject({
* - They may have been removed from the collection.
*
* - They have been deleted (either moved to trash, or permanently deleted).
*
*/
isDeleted: z.boolean().nullish().transform(nullToUndefined),
metadata: RemoteFileMetadata,
@@ -484,90 +339,6 @@ export const FileDiffResponse = z.object({
hasMore: z.boolean(),
});
export type FileMagicMetadata = MagicMetadataCore<FilePrivateMagicMetadataData>;
export type FilePrivateMagicMetadata =
MagicMetadataCore<FilePrivateMagicMetadataData>;
export type FilePublicMagicMetadata =
MagicMetadataCore<FilePublicMagicMetadataData>;
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string,
): Promise<EnteFile> {
try {
const worker = await sharedCryptoWorker();
const {
encryptedKey,
keyDecryptionNonce,
metadata,
magicMetadata,
pubMagicMetadata,
...restFileProps
} = file;
const fileKey = await worker.decryptBox(
{ encryptedData: encryptedKey, nonce: keyDecryptionNonce },
collectionKey,
);
const fileMetadata = await worker.decryptMetadataJSON(
// See: [Note: strict mode migration]
//
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
metadata,
fileKey,
);
let fileMagicMetadata: FileMagicMetadata;
let filePubMagicMetadata: FilePublicMagicMetadata;
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
if (magicMetadata?.data) {
fileMagicMetadata = {
...file.magicMetadata,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data: await worker.decryptMetadataJSON(
{
encryptedData: magicMetadata.data,
decryptionHeader: magicMetadata.header,
},
fileKey,
),
};
}
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
if (pubMagicMetadata?.data) {
filePubMagicMetadata = {
...pubMagicMetadata,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data: await worker.decryptMetadataJSON(
{
encryptedData: pubMagicMetadata.data,
decryptionHeader: pubMagicMetadata.header,
},
fileKey,
),
};
}
return {
...restFileProps,
key: fileKey,
// @ts-expect-error TODO: Need to use zod here.
metadata: fileMetadata,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
magicMetadata: fileMagicMetadata,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
pubMagicMetadata: filePubMagicMetadata,
};
} catch (e) {
log.error("file decryption failed", e);
throw e;
}
}
/**
* Decrypt a remote file using the provided {@link collectionKey}.
*
@@ -582,7 +353,7 @@ export async function decryptFile(
export const decryptRemoteFile = async (
remoteFile: RemoteEnteFile,
collectionKey: string,
): Promise<EnteFile2> => {
): Promise<EnteFile> => {
// RemoteEnteFile is a looseObject, and we want to retain that semantic for
// the parsed EnteFile. Mention all fields that we want to explicitly drop
// or transform, passthrough the rest unchanged in the return value.
@@ -618,39 +389,27 @@ export const decryptRemoteFile = async (
transformDecryptedMetadataJSON(id, metadataJSON),
);
let magicMetadata: EnteFile2["magicMetadata"];
let magicMetadata: EnteFile["magicMetadata"];
if (encryptedMagicMetadata) {
const genericMM = await decryptMagicMetadata(
encryptedMagicMetadata,
key,
);
// TODO(RE):
const data = genericMM.data as FilePrivateMagicMetadataData;
// TODO(RE):
magicMetadata = { ...genericMM, header: "", data };
const data = FilePrivateMagicMetadataData.parse(genericMM.data);
magicMetadata = { ...genericMM, data };
}
let pubMagicMetadata: EnteFile2["pubMagicMetadata"];
let pubMagicMetadata: EnteFile["pubMagicMetadata"];
if (encryptedPubMagicMetadata) {
const genericMM = await decryptMagicMetadata(
encryptedPubMagicMetadata,
key,
);
// TODO(RE):
const data = genericMM.data as FilePublicMagicMetadataData;
// TODO(RE):
pubMagicMetadata = { ...genericMM, header: "", data };
const data = FilePublicMagicMetadataData.parse(genericMM.data);
pubMagicMetadata = { ...genericMM, data };
}
return {
...rest,
id,
key,
// TODO(RE):
metadata: metadata as FileMetadata,
magicMetadata,
pubMagicMetadata,
};
return { ...rest, id, key, metadata, magicMetadata, pubMagicMetadata };
};
/**
@@ -752,22 +511,3 @@ export const mergeMetadata = (files: EnteFile[]) =>
*/
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<T> {
version: number;
count: number;
header: string;
data: T;
}
export type EncryptedMagicMetadata = MagicMetadataCore<string>;

View File

@@ -4,9 +4,13 @@ import {
isPinnedCollection,
} from "ente-gallery/services/magic-metadata";
import { collectionTypes, type Collection } from "ente-media/collection";
import type { EnteFile, FilePrivateMagicMetadata } from "ente-media/file";
import type { EnteFile } from "ente-media/file";
import { mergeMetadata } from "ente-media/file";
import { isArchivedFile } from "ente-media/file-metadata";
import {
isArchivedFile,
type FilePrivateMagicMetadataData,
} from "ente-media/file-metadata";
import type { MagicMetadata } from "ente-media/magic-metadata";
import {
createCollectionNameByID,
isHiddenCollection,
@@ -31,6 +35,7 @@ import {
groupFilesByCollectionID,
sortFiles,
uniqueFilesByID,
type TrashedEnteFile,
} from "../../services/files";
import type { PeopleState, Person } from "../../services/ml/people";
import type { SearchSuggestion } from "../../services/search/types";
@@ -315,7 +320,10 @@ export interface GalleryState {
* thereafter the synced files themselves will reflect the latest private
* magic metadata.
*/
unsyncedPrivateMagicMetadataUpdates: Map<number, FilePrivateMagicMetadata>;
unsyncedPrivateMagicMetadataUpdates: Map<
number,
MagicMetadata<FilePrivateMagicMetadataData>
>;
/*--< State that underlies transient UI state >--*/
@@ -418,7 +426,7 @@ export type GalleryAction =
collections: Collection[];
normalFiles: EnteFile[];
hiddenFiles: EnteFile[];
trashedFiles: EnteFile[];
trashedFiles: TrashedEnteFile[];
}
| {
type: "setCollections";
@@ -446,7 +454,7 @@ export type GalleryAction =
| {
type: "unsyncedPrivateMagicMetadataUpdate";
fileID: number;
privateMagicMetadata: FilePrivateMagicMetadata;
privateMagicMetadata: MagicMetadata<FilePrivateMagicMetadataData>;
}
| { type: "clearUnsyncedState" }
| { type: "showAll" }

View File

@@ -6,7 +6,7 @@
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import { type Collection } from "ente-media/collection";
import { decryptFile, type EnteFile } from "ente-media/file";
import { decryptRemoteFile, type EnteFile } from "ente-media/file";
import {
getLocalTrash,
getTrashedFiles,
@@ -220,7 +220,7 @@ export async function syncTrash(
deletedFileIDs.add(trashItem.file.id);
}
if (!trashItem.isDeleted && !trashItem.isRestored) {
const decryptedFile = await decryptFile(
const decryptedFile = await decryptRemoteFile(
trashItem.file,
collectionKey,
);

View File

@@ -1,6 +1,6 @@
import { authenticatedRequestHeaders, ensureOk } from "ente-base/http";
import { apiURL } from "ente-base/origins";
import type { EnteFile, EnteFile2 } from "ente-media/file";
import type { EnteFile } from "ente-media/file";
import type {
FilePrivateMagicMetadataData,
FilePublicMagicMetadataData,
@@ -88,7 +88,7 @@ export const updateFilesVisibility = async (
* See: [Note: Magic metadata data cannot have nullish values]
*/
const updateFilesPrivateMagicMetadata = async (
files: EnteFile2[],
files: EnteFile[],
updates: FilePrivateMagicMetadataData,
) =>
putFilesMagicMetadata({
@@ -165,7 +165,7 @@ const putFilesMagicMetadata = async (
*
* @param newFileName The new file name of the file.
*/
export const updateFileFileName = (file: EnteFile2, newFileName: string) =>
export const updateFileFileName = (file: EnteFile, newFileName: string) =>
updateFilePublicMagicMetadata(file, { editedName: newFileName });
/**
@@ -184,7 +184,7 @@ export const updateFileFileName = (file: EnteFile2, newFileName: string) =>
* Fields in magic metadata cannot be removed after being added, so to reset the
* caption to the default (no value) state pass a blank string.
*/
export const updateFileCaption = (file: EnteFile2, caption: string) =>
export const updateFileCaption = (file: EnteFile, caption: string) =>
updateFilePublicMagicMetadata(file, { caption });
/**
@@ -200,7 +200,7 @@ export const updateFileCaption = (file: EnteFile2, caption: string) =>
* See: [Note: Magic metadata data cannot have nullish values]
*/
export const updateFilePublicMagicMetadata = async (
file: EnteFile2,
file: EnteFile,
updates: FilePublicMagicMetadataData,
) => updateFilesPublicMagicMetadata([file], updates);
@@ -213,7 +213,7 @@ export const updateFilePublicMagicMetadata = async (
* the {@link pubMagicMetadata} of the given files.
*/
const updateFilesPublicMagicMetadata = async (
files: EnteFile2[],
files: EnteFile[],
updates: FilePublicMagicMetadataData,
) =>
putFilesPublicMagicMetadata({

View File

@@ -1,12 +1,13 @@
import { blobCache } from "ente-base/blob-cache";
import { dateFromEpochMicroseconds } from "ente-base/date";
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import type { Collection } from "ente-media/collection";
import {
decryptFile,
decryptRemoteFile,
mergeMetadata,
type EncryptedEnteFile,
type EnteFile,
type RemoteEnteFile,
} from "ente-media/file";
import { metadataHash } from "ente-media/file-metadata";
import { type Trash } from "ente-new/photos/services/trash";
@@ -122,9 +123,9 @@ export const getFiles = async (
const newDecryptedFilesBatch = await Promise.all(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
resp.data.diff.map(async (file: EncryptedEnteFile) => {
resp.data.diff.map(async (file: RemoteEnteFile) => {
if (!file.isDeleted) {
return await decryptFile(file, collection.key);
return await decryptRemoteFile(file, collection.key);
} else {
return file;
}
@@ -230,7 +231,30 @@ export async function getLocalTrashedFiles() {
return getTrashedFiles(await getLocalTrash());
}
export function getTrashedFiles(trash: Trash): EnteFile[] {
export type TrashedEnteFile = EnteFile & {
/**
* `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).
*/
isTrashed?: boolean;
/**
* If {@link isTrashed} is `true`, then {@link deleteBy} contains the epoch
* microseconds when this file will be permanently deleted.
*/
deleteBy?: number;
};
/**
* 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: TrashedEnteFile) =>
dateFromEpochMicroseconds(file.deleteBy);
export function getTrashedFiles(trash: Trash): TrashedEnteFile[] {
return sortTrashFiles(
mergeMetadata(
trash.map((trashedFile) => ({
@@ -250,7 +274,7 @@ export function getTrashedFiles(trash: Trash): EnteFile[] {
export const getLocalTrashFileIDs = () =>
getLocalTrash().then((trash) => new Set(trash.map((f) => f.file.id)));
const sortTrashFiles = (files: EnteFile[]) => {
const sortTrashFiles = (files: TrashedEnteFile[]) => {
return files.sort((a, b) => {
if (a.deleteBy === b.deleteBy) {
if (a.metadata.creationTime === b.metadata.creationTime) {
@@ -354,6 +378,8 @@ export function getLatestVersionFiles(files: EnteFile[]) {
}
});
return Array.from(latestVersionFiles.values()).filter(
(file) => !file.isDeleted,
// TODO(RE):
// (file) => !file.isDeleted,
(file) => !("isDeleted" in file && file.isDeleted),
);
}

View File

@@ -1,11 +1,11 @@
import type { EncryptedEnteFile, EnteFile } from "ente-media/file";
import type { EnteFile, RemoteEnteFile } from "ente-media/file";
export interface TrashItem extends Omit<EncryptedTrashItem, "file"> {
file: EnteFile;
}
export interface EncryptedTrashItem {
file: EncryptedEnteFile;
file: RemoteEnteFile;
/**
* `true` if the file no longer in trash because it was permanently deleted.
*