[web] Switch to new EnteFile TypeScript type (internal) (#6323)
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user