From a58ab7cd1630aa85a2f41ebfc63dfc2a2cae7328 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 16 Jun 2025 18:55:47 +0530 Subject: [PATCH] conv 2 --- .../photos/src/services/collectionService.ts | 4 +- web/packages/media/collection.ts | 54 +--- .../new/photos/services/collection.ts | 4 +- .../new/photos/services/collections.ts | 240 +++--------------- 4 files changed, 48 insertions(+), 254 deletions(-) diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index e68c6ed007..48e629958e 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -4,7 +4,7 @@ import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { Collection, - CollectionMagicMetadataProps, + CollectionPrivateMagicMetadataData, CollectionSubType, type CollectionType, RemoveFromCollectionRequest, @@ -48,7 +48,7 @@ export const createAlbum = (albumName: string) => const createCollection = async ( collectionName: string, type: CollectionType, - magicMetadataProps?: CollectionMagicMetadataProps, + magicMetadataProps?: CollectionPrivateMagicMetadataData, ): Promise => createCollection2(collectionName, type, magicMetadataProps); diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index 22e8f2970f..9f72a6cebe 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -1,8 +1,5 @@ import { decryptBoxBytes } from "ente-base/crypto"; -import { - type EncryptedMagicMetadata, - type MagicMetadataCore, -} from "ente-media/file"; +import { type MagicMetadataCore } from "ente-media/file"; import { nullishToEmpty, nullishToFalse, @@ -434,41 +431,20 @@ export const RemoteCollection = z.looseObject({ export type RemoteCollection = z.infer; -export interface EncryptedCollection { +export interface Collection { id: number; owner: CollectionUser; - encryptedKey: string; - keyDecryptionNonce: string; - name?: string; - encryptedName: string; - nameDecryptionNonce: string; type: CollectionType; attributes: unknown; sharees: CollectionUser[]; publicURLs?: PublicURL[]; updationTime: number; isDeleted: boolean; - magicMetadata?: EncryptedMagicMetadata; - pubMagicMetadata?: EncryptedMagicMetadata; - sharedMagicMetadata?: EncryptedMagicMetadata; -} - -export interface Collection - extends Omit< - EncryptedCollection, - | "encryptedKey" - | "keyDecryptionNonce" - | "encryptedName" - | "nameDecryptionNonce" - | "magicMetadata" - | "pubMagicMetadata" - | "sharedMagicMetadata" - > { key: string; name: string; - magicMetadata: CollectionMagicMetadata; + magicMetadata: MagicMetadataCore; pubMagicMetadata: CollectionPublicMagicMetadata; - sharedMagicMetadata: CollectionShareeMagicMetadata; + sharedMagicMetadata: MagicMetadataCore; // TODO(C2): Gradual conversion to new structure. c2?: Collection2; } @@ -787,25 +763,5 @@ export interface RemoveFromCollectionRequest { fileIDs: number[]; } -export interface CollectionMagicMetadataProps { - visibility?: number; // ItemVisibility; - subType?: number; // CollectionSubType; - order?: number; -} - -export type CollectionMagicMetadata = - MagicMetadataCore; - -export interface CollectionShareeMetadataProps { - visibility?: number; // ItemVisibility; -} -export type CollectionShareeMagicMetadata = - MagicMetadataCore; - -export interface CollectionPublicMagicMetadataProps { - asc?: boolean; - coverID?: number; -} - export type CollectionPublicMagicMetadata = - MagicMetadataCore; + MagicMetadataCore; diff --git a/web/packages/new/photos/services/collection.ts b/web/packages/new/photos/services/collection.ts index fdc5115830..bf642f7c6e 100644 --- a/web/packages/new/photos/services/collection.ts +++ b/web/packages/new/photos/services/collection.ts @@ -284,7 +284,9 @@ export interface CollectionChange { * fetched in a previous set of changes. This allows us to resume fetching from * that point. Pass 0 to fetch from the beginning. * - * @returns An array of {@link CollectionChange}s. + * @returns An array of {@link CollectionChange}s. It is guaranteed that there + * will be at most one entry for a given collection in the result array. See: + * [Note: Diff response will have at most one entry for an id] */ export const getCollectionChanges = async ( sinceTime: number, diff --git a/web/packages/new/photos/services/collections.ts b/web/packages/new/photos/services/collections.ts index ca4c29a0ca..072a23c7b9 100644 --- a/web/packages/new/photos/services/collections.ts +++ b/web/packages/new/photos/services/collections.ts @@ -1,21 +1,12 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ // TODO: Audit this file +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { sharedCryptoWorker } from "ente-base/crypto"; import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; -import { ensureMasterKeyFromSession } from "ente-base/session"; -import { - type Collection, - type CollectionMagicMetadata, - type CollectionPublicMagicMetadata, - type CollectionShareeMagicMetadata, - type EncryptedCollection, -} from "ente-media/collection"; +import { type Collection } from "ente-media/collection"; import { decryptFile, type EncryptedTrashItem, @@ -29,10 +20,12 @@ import { } from "ente-new/photos/services/files"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; -import { getData } from "ente-shared/storage/localStorage"; import { getToken } from "ente-shared/storage/localStorage/helpers"; -import { getCollectionByID, isHiddenCollection } from "./collection"; -import { ensureUserKeyPair } from "./user"; +import { + getCollectionByID, + getCollectionChanges, + isHiddenCollection, +} from "./collection"; const COLLECTION_TABLE = "collections"; const HIDDEN_COLLECTION_IDS = "hidden-collection-ids"; @@ -86,212 +79,51 @@ export const getAllLatestCollections = async (): Promise => { export const syncCollections = async () => { const localCollections = await getAllLocalCollections(); - let lastCollectionUpdationTime = await getCollectionUpdationTime(); + let sinceTime = await getCollectionUpdationTime(); + + const changes = await getCollectionChanges(sinceTime); + if (!changes.length) return localCollections; + const hiddenCollectionIDs = await getHiddenCollectionIDs(); - const token = getToken(); - const masterKey = await ensureMasterKeyFromSession(); - const updatedCollections = - (await getCollections(token, lastCollectionUpdationTime, masterKey)) ?? - []; - if (updatedCollections.length === 0) { - return localCollections; - } - const allCollectionsInstances = [ - ...localCollections, - ...updatedCollections, - ]; - const latestCollectionsInstances = new Map(); - allCollectionsInstances.forEach((collection) => { - if ( - !latestCollectionsInstances.has(collection.id) || - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - latestCollectionsInstances.get(collection.id).updationTime < - collection.updationTime - ) { - latestCollectionsInstances.set(collection.id, collection); - } - }); - const collections: Collection[] = []; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, collection] of latestCollectionsInstances) { - const isDeletedCollection = collection.isDeleted; - const isNewlyHiddenCollection = - isHiddenCollection(collection) && - !hiddenCollectionIDs.includes(collection.id); - const isNewlyUnHiddenCollection = - !isHiddenCollection(collection) && - hiddenCollectionIDs.includes(collection.id); - if ( - isDeletedCollection || - isNewlyHiddenCollection || - isNewlyUnHiddenCollection - ) { - await removeCollectionIDLastSyncTime(collection.id); + const collectionsByID = new Map(localCollections.map((c) => [c.id, c])); + for (const { id, updationTime, collection } of changes) { + sinceTime = Math.max(sinceTime, updationTime); + let removeSyncTime = false; + if (collection) { + collectionsByID.set(id, collection); + + const wasHidden = hiddenCollectionIDs.includes(collection.id); + const isHidden = isHiddenCollection(collection); + // If hidden state changes. + removeSyncTime = wasHidden != isHidden; + } else { + // Collection was deleted on remote. + collectionsByID.delete(id); + + removeSyncTime = true; } - if (isDeletedCollection) { - continue; + + if (removeSyncTime) { + await removeCollectionIDLastSyncTime(id); } - collections.push(collection); - lastCollectionUpdationTime = Math.max( - lastCollectionUpdationTime, - collection.updationTime, - ); } + const collections = [...collectionsByID.values()]; const updatedHiddenCollectionIDs = collections .filter((collection) => isHiddenCollection(collection)) .map((collection) => collection.id); await localForage.setItem(COLLECTION_TABLE, collections); - await localForage.setItem( - COLLECTION_UPDATION_TIME, - lastCollectionUpdationTime, - ); + await localForage.setItem(COLLECTION_UPDATION_TIME, sinceTime); await localForage.setItem( HIDDEN_COLLECTION_IDS, updatedHiddenCollectionIDs, ); + return collections; }; -const getCollections = async ( - token: string, - sinceTime: number, - key: string, -): Promise => { - try { - const resp = await HTTPService.get( - await apiURL("/collections/v2"), - { sinceTime }, - { "X-Auth-Token": token }, - ); - const decryptedCollections: Collection[] = await Promise.all( - resp.data.collections.map( - async (collection: EncryptedCollection) => { - if (collection.isDeleted) { - return collection; - } - try { - return await getCollectionWithSecrets(collection, key); - } catch (e) { - log.error( - `decryption failed for collection with ID ${collection.id}`, - e, - ); - return collection; - } - }, - ), - ); - // only allow deleted or collection with key, filtering out collection whose decryption failed - const collections = decryptedCollections.filter( - (collection) => collection.isDeleted || collection.key, - ); - return collections; - } catch (e) { - log.error("getCollections failed", e); - throw e; - } -}; - -export const getCollectionWithSecrets = async ( - collection: EncryptedCollection, - masterKey: string, -): Promise => { - const cryptoWorker = await sharedCryptoWorker(); - const userID = getData("user").id; - let collectionKey: string; - if (collection.owner.id === userID) { - collectionKey = await cryptoWorker.decryptBox( - { - encryptedData: collection.encryptedKey, - nonce: collection.keyDecryptionNonce, - }, - masterKey, - ); - } else { - collectionKey = await cryptoWorker.boxSealOpen( - collection.encryptedKey, - await ensureUserKeyPair(), - ); - } - const collectionName = - collection.name || - new TextDecoder().decode( - await cryptoWorker.decryptBoxBytes( - { - encryptedData: collection.encryptedName, - nonce: collection.nameDecryptionNonce, - }, - collectionKey, - ), - ); - - let collectionMagicMetadata: CollectionMagicMetadata; - if (collection.magicMetadata?.data) { - collectionMagicMetadata = { - ...collection.magicMetadata, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - data: await cryptoWorker.decryptMetadataJSON( - { - encryptedData: collection.magicMetadata.data, - decryptionHeader: collection.magicMetadata.header, - }, - collectionKey, - ), - }; - } - let collectionPublicMagicMetadata: CollectionPublicMagicMetadata; - if (collection.pubMagicMetadata?.data) { - collectionPublicMagicMetadata = { - ...collection.pubMagicMetadata, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - data: await cryptoWorker.decryptMetadataJSON( - { - encryptedData: collection.pubMagicMetadata.data, - decryptionHeader: collection.pubMagicMetadata.header, - }, - collectionKey, - ), - }; - } - - let collectionShareeMagicMetadata: CollectionShareeMagicMetadata; - if (collection.sharedMagicMetadata?.data) { - collectionShareeMagicMetadata = { - ...collection.sharedMagicMetadata, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - data: await cryptoWorker.decryptMetadataJSON( - { - encryptedData: collection.sharedMagicMetadata.data, - decryptionHeader: collection.sharedMagicMetadata.header, - }, - collectionKey, - ), - }; - } - - return { - ...collection, - name: collectionName, - key: collectionKey, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - magicMetadata: collectionMagicMetadata, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - pubMagicMetadata: collectionPublicMagicMetadata, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - sharedMagicMetadata: collectionShareeMagicMetadata, - }; -}; - const TRASH_TIME = "trash-time"; const DELETED_COLLECTION = "deleted-collection"; @@ -374,6 +206,10 @@ export async function syncTrash( const collectionID = trashItem.file.collectionID; let collection = collectionByID.get(collectionID); if (!collection) { + // We might not have the collection locally since it + // might've been deleted. We still need the collection since + // the trash item will be encrypted using the (erstwhile) + // collection's key. collection = await getCollectionByID(collectionID); collectionByID.set(collectionID, collection); await localForage.setItem(DELETED_COLLECTION, [