conv 2
This commit is contained in:
@@ -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<Collection> =>
|
||||
createCollection2(collectionName, type, magicMetadataProps);
|
||||
|
||||
|
||||
@@ -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<typeof RemoteCollection>;
|
||||
|
||||
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<CollectionPrivateMagicMetadataData>;
|
||||
pubMagicMetadata: CollectionPublicMagicMetadata;
|
||||
sharedMagicMetadata: CollectionShareeMagicMetadata;
|
||||
sharedMagicMetadata: MagicMetadataCore<CollectionShareeMagicMetadataData>;
|
||||
// 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<CollectionMagicMetadataProps>;
|
||||
|
||||
export interface CollectionShareeMetadataProps {
|
||||
visibility?: number; // ItemVisibility;
|
||||
}
|
||||
export type CollectionShareeMagicMetadata =
|
||||
MagicMetadataCore<CollectionShareeMetadataProps>;
|
||||
|
||||
export interface CollectionPublicMagicMetadataProps {
|
||||
asc?: boolean;
|
||||
coverID?: number;
|
||||
}
|
||||
|
||||
export type CollectionPublicMagicMetadata =
|
||||
MagicMetadataCore<CollectionPublicMagicMetadataProps>;
|
||||
MagicMetadataCore<CollectionPublicMagicMetadataData>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Collection[]> => {
|
||||
|
||||
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<number, Collection>();
|
||||
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<Collection[]> => {
|
||||
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<Collection> => {
|
||||
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, [
|
||||
|
||||
Reference in New Issue
Block a user