Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a116776695 |
No files matched your search
@@ -630,7 +630,8 @@ export class Library {
|
|||||||
// in it that decrypts, in the server's order. `collection` is a record
|
// in it that decrypts, in the server's order. `collection` is a record
|
||||||
// `freshCollections()` returned, whose key decrypts the files. It fetches
|
// `freshCollections()` returned, whose key decrypts the files. It fetches
|
||||||
// no other collection's files. Like `freshCollections()`, it stores
|
// no other collection's files. Like `freshCollections()`, it stores
|
||||||
// nothing.
|
// nothing. It returns each file once, as the latest copy the server sent,
|
||||||
|
// where the first copy was.
|
||||||
async freshFiles(collection: Collection): Promise<EnteFile[]> {
|
async freshFiles(collection: Collection): Promise<EnteFile[]> {
|
||||||
if (this.closed) throw new Error("the library is closed");
|
if (this.closed) throw new Error("the library is closed");
|
||||||
const { files } = await this.client.filesSince({
|
const { files } = await this.client.filesSince({
|
||||||
@@ -638,7 +639,9 @@ export class Library {
|
|||||||
collectionKey: collection.key,
|
collectionKey: collection.key,
|
||||||
sinceTime: 0,
|
sinceTime: 0,
|
||||||
});
|
});
|
||||||
return files;
|
const byID = new Map<number, EnteFile>();
|
||||||
|
for (const file of files) byID.set(file.id, file);
|
||||||
|
return [...byID.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back up every in-scope file to `opts.downloadDirectory`, or else the
|
// Back up every in-scope file to `opts.downloadDirectory`, or else the
|
||||||
|
|||||||
+101
-29
@@ -1599,6 +1599,49 @@ describe("an account with many albums", () => {
|
|||||||
progress += `Fetched the files of ${n} of ${ALBUMS} collections\n`;
|
progress += `Fetched the files of ${n} of ${ALBUMS} collections\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The key of each album, by album ID.
|
||||||
|
const albumKeys = new Map<number, Uint8Array>();
|
||||||
|
|
||||||
|
// A photo in album `collectionID` as the server sends it. These tests
|
||||||
|
// fetch no original and no thumbnail.
|
||||||
|
const photoRow = (photo: {
|
||||||
|
id: number;
|
||||||
|
collectionID: number;
|
||||||
|
title: string;
|
||||||
|
updationTime: number;
|
||||||
|
}): Record<string, unknown> => {
|
||||||
|
const photoKey = sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||||
|
const photoKeyBox = encryptSecretbox(
|
||||||
|
photoKey,
|
||||||
|
albumKeys.get(photo.collectionID)!,
|
||||||
|
);
|
||||||
|
const metadata = encryptStreamBlob(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
JSON.stringify({
|
||||||
|
title: photo.title,
|
||||||
|
fileType: 0,
|
||||||
|
creationTime: 1000,
|
||||||
|
modificationTime: 1000,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
photoKey,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: photo.id,
|
||||||
|
collectionID: photo.collectionID,
|
||||||
|
ownerID: USER_ID,
|
||||||
|
encryptedKey: toBase64(photoKeyBox.ciphertext),
|
||||||
|
keyDecryptionNonce: toBase64(photoKeyBox.nonce),
|
||||||
|
metadata: {
|
||||||
|
encryptedData: toBase64(metadata.ciphertext),
|
||||||
|
decryptionHeader: toBase64(metadata.header),
|
||||||
|
},
|
||||||
|
file: { decryptionHeader: "" },
|
||||||
|
thumbnail: { decryptionHeader: "" },
|
||||||
|
updationTime: photo.updationTime,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Album n is named `Album n` and holds one photo, n * 100, titled
|
// Album n is named `Album n` and holds one photo, n * 100, titled
|
||||||
// `photo-n.jpg`.
|
// `photo-n.jpg`.
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -1608,6 +1651,7 @@ describe("an account with many albums", () => {
|
|||||||
const filesByCollection: Record<number, Record<string, unknown>[]> = {};
|
const filesByCollection: Record<number, Record<string, unknown>[]> = {};
|
||||||
for (let id = 1; id <= ALBUMS; id++) {
|
for (let id = 1; id <= ALBUMS; id++) {
|
||||||
const albumKey = sodium.crypto_secretbox_keygen();
|
const albumKey = sodium.crypto_secretbox_keygen();
|
||||||
|
albumKeys.set(id, albumKey);
|
||||||
const albumKeyBox = encryptSecretbox(albumKey, account.masterKey);
|
const albumKeyBox = encryptSecretbox(albumKey, account.masterKey);
|
||||||
const name = encryptSecretbox(
|
const name = encryptSecretbox(
|
||||||
new TextEncoder().encode(`Album ${id}`),
|
new TextEncoder().encode(`Album ${id}`),
|
||||||
@@ -1623,36 +1667,13 @@ describe("an account with many albums", () => {
|
|||||||
type: "album",
|
type: "album",
|
||||||
updationTime: 1,
|
updationTime: 1,
|
||||||
});
|
});
|
||||||
const photoKey =
|
|
||||||
sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
|
||||||
const photoKeyBox = encryptSecretbox(photoKey, albumKey);
|
|
||||||
const metadata = encryptStreamBlob(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify({
|
|
||||||
title: `photo-${id}.jpg`,
|
|
||||||
fileType: 0,
|
|
||||||
creationTime: 1000,
|
|
||||||
modificationTime: 1000,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
photoKey,
|
|
||||||
);
|
|
||||||
filesByCollection[id] = [
|
filesByCollection[id] = [
|
||||||
{
|
photoRow({
|
||||||
id: id * 100,
|
id: id * 100,
|
||||||
collectionID: id,
|
collectionID: id,
|
||||||
ownerID: USER_ID,
|
title: `photo-${id}.jpg`,
|
||||||
encryptedKey: toBase64(photoKeyBox.ciphertext),
|
|
||||||
keyDecryptionNonce: toBase64(photoKeyBox.nonce),
|
|
||||||
metadata: {
|
|
||||||
encryptedData: toBase64(metadata.ciphertext),
|
|
||||||
decryptionHeader: toBase64(metadata.header),
|
|
||||||
},
|
|
||||||
// These tests fetch no original and no thumbnail.
|
|
||||||
file: { decryptionHeader: "" },
|
|
||||||
thumbnail: { decryptionHeader: "" },
|
|
||||||
updationTime: 1,
|
updationTime: 1,
|
||||||
},
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const ente = buildMetaFetch({
|
const ente = buildMetaFetch({
|
||||||
@@ -1688,13 +1709,14 @@ describe("an account with many albums", () => {
|
|||||||
requests.length = 0;
|
requests.length = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// A context whose saved session sends its requests to the fake server.
|
// A context whose saved session sends its requests to `fetch`, by default
|
||||||
const accountContext = (): CliContext => {
|
// the fake server.
|
||||||
|
const accountContext = (fetch = server): CliContext => {
|
||||||
const ctx = context();
|
const ctx = context();
|
||||||
saveSession(ctx.sessionDir, snapshot);
|
saveSession(ctx.sessionDir, snapshot);
|
||||||
return {
|
return {
|
||||||
...ctx,
|
...ctx,
|
||||||
loadSession: (path) => loadSession(path, { fetch: server }),
|
loadSession: (path) => loadSession(path, { fetch }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1727,6 +1749,56 @@ describe("an account with many albums", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Here the server sends album 7's files in two pages. Photo 700 changed
|
||||||
|
// between them, so both pages hold a copy of it.
|
||||||
|
it("files with a collection ID lists only the later copy of a photo sent in two pages", async () => {
|
||||||
|
const firstPage = [
|
||||||
|
photoRow({
|
||||||
|
id: 700,
|
||||||
|
collectionID: 7,
|
||||||
|
title: "photo-7.jpg",
|
||||||
|
updationTime: 1,
|
||||||
|
}),
|
||||||
|
photoRow({
|
||||||
|
id: 701,
|
||||||
|
collectionID: 7,
|
||||||
|
title: "photo-701.jpg",
|
||||||
|
updationTime: 1,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const secondPage = [
|
||||||
|
photoRow({
|
||||||
|
id: 700,
|
||||||
|
collectionID: 7,
|
||||||
|
title: "photo-7-edited.jpg",
|
||||||
|
updationTime: 2,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const paged = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const url = new URL(input instanceof Request ? input.url : input);
|
||||||
|
if (
|
||||||
|
url.pathname !== "/collections/v2/diff" ||
|
||||||
|
url.searchParams.get("collectionID") !== "7"
|
||||||
|
) {
|
||||||
|
return server(input, init);
|
||||||
|
}
|
||||||
|
const first = url.searchParams.get("sinceTime") === "0";
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
diff: first ? firstPage : secondPage,
|
||||||
|
hasMore: first,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}) as typeof globalThis.fetch;
|
||||||
|
|
||||||
|
expect(await filesCommand(accountContext(paged), "7", {})).toBe(0);
|
||||||
|
|
||||||
|
// Photo 700 is listed once, with the later title, before photo 701.
|
||||||
|
expect(stdout.text).toBe(
|
||||||
|
"700\timage\tphoto-7-edited.jpg\n701\timage\tphoto-701.jpg\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// The cache stays empty, so a later `files` without a collection ID still
|
// The cache stays empty, so a later `files` without a collection ID still
|
||||||
// fetches the files of all 40 albums.
|
// fetches the files of all 40 albums.
|
||||||
it("files with a collection ID stores nothing in the cache", async () => {
|
it("files with a collection ID stores nothing in the cache", async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user