downloadFile streams the encrypted body from the CDN, buffers it to the 4 MiB + 17 encrypted chunk boundary, decrypts each chunk via secretstream pull, and writes the concatenated plaintext to disk. downloadThumbnail does the same for the thumbnail CDN. 4 unit tests (single-chunk, large single-chunk, filename fallback, thumbnail) + live integration test that downloads a real 472 KB JPEG from the dev account and verifies it lands on disk. Uses mkdtempSync for temp directories (not manual timestamp paths).
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { init } from "../../src/crypto/index.js";
|
|
import { ApiClient } from "../../src/api/client.js";
|
|
import { beginLogin } from "../../src/auth/login.js";
|
|
import { unwrapAuth } from "../../src/auth/unwrap.js";
|
|
import { decryptCollection, decryptFile } from "../../src/model/index.js";
|
|
import { downloadFile } from "../../src/download/index.js";
|
|
import type { RawCollection, RawEnteFile } from "../../src/model/index.js";
|
|
|
|
// Dev account — not a secret, throwaway account for integration testing.
|
|
const EMAIL = "entedev2026jp@acidhou.se";
|
|
const PASSWORD = "loldongs";
|
|
const RECOVERY_KEY =
|
|
"deliver have behave collect void chicken boring embrace coast reflect squeeze cotton dish resemble license remain quick dwarf plastic ensure amused cry nasty equip";
|
|
|
|
void RECOVERY_KEY;
|
|
|
|
const main = async () => {
|
|
await init();
|
|
const api = new ApiClient();
|
|
|
|
console.log(`Logging in as ${EMAIL}...`);
|
|
const challenge = await beginLogin(api, EMAIL, PASSWORD);
|
|
if (challenge.kind !== "complete") {
|
|
console.error("Expected complete login, got:", challenge.kind);
|
|
process.exit(1);
|
|
}
|
|
|
|
const { masterKey, token } = await unwrapAuth(challenge.response, PASSWORD);
|
|
api.setAuthToken(token);
|
|
console.log("Logged in, user ID:", challenge.response.id);
|
|
|
|
// Fetch and decrypt collections
|
|
console.log("\nFetching collections...");
|
|
const { collections: rawCollections } = await api.getJSON<{
|
|
collections: RawCollection[];
|
|
}>("/collections/v2", { sinceTime: 0 });
|
|
|
|
const userID = challenge.response.id;
|
|
const collections = rawCollections.map((raw) =>
|
|
decryptCollection(raw, masterKey, userID),
|
|
);
|
|
|
|
console.log(`${collections.length} collection(s):`);
|
|
for (const c of collections) {
|
|
console.log(
|
|
` [${c.type}] "${c.name}" (id=${c.id}, shared=${c.isShared})`,
|
|
);
|
|
}
|
|
|
|
// Fetch and decrypt files from the first non-empty collection
|
|
for (const col of collections) {
|
|
console.log(`\nFetching files from "${col.name}" (id=${col.id})...`);
|
|
const { diff: rawFiles } = await api.getJSON<{
|
|
diff: RawEnteFile[];
|
|
hasMore: boolean;
|
|
}>("/collections/v2/diff", {
|
|
collectionID: col.id,
|
|
sinceTime: 0,
|
|
});
|
|
|
|
if (rawFiles.length === 0) {
|
|
console.log(" (empty)");
|
|
continue;
|
|
}
|
|
|
|
const files = rawFiles
|
|
.filter((f) => !f.isDeleted)
|
|
.map((raw) => decryptFile(raw, col.key));
|
|
|
|
console.log(` ${files.length} file(s):`);
|
|
for (const f of files.slice(0, 10)) {
|
|
console.log(
|
|
` ${f.metadata.title} [${f.metadata.fileType}] id=${f.id}`,
|
|
);
|
|
if (f.metadata.latitude !== undefined) {
|
|
console.log(
|
|
` location: ${f.metadata.latitude}, ${f.metadata.longitude}`,
|
|
);
|
|
}
|
|
}
|
|
if (files.length > 10) {
|
|
console.log(` ... and ${files.length - 10} more`);
|
|
}
|
|
// Download the first file to a temp directory
|
|
if (files.length > 0) {
|
|
const first = files[0]!;
|
|
const { mkdtempSync, statSync } = await import("node:fs");
|
|
const { join } = await import("node:path");
|
|
const { tmpdir } = await import("node:os");
|
|
const outDir = mkdtempSync(join(tmpdir(), "quack-live-test-"));
|
|
const outPath = `${outDir}/${first.metadata.title}`;
|
|
|
|
console.log(`\n Downloading "${first.metadata.title}"...`);
|
|
const result = await downloadFile(api, first, outPath);
|
|
const stat = statSync(result.path);
|
|
console.log(
|
|
` Saved to ${result.path} (${result.bytesWritten} bytes decrypted, ${stat.size} on disk)`,
|
|
);
|
|
|
|
if (result.bytesWritten < 1000) {
|
|
console.error(
|
|
" WARNING: file seems too small, possible decryption issue",
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
console.log("\nLive integration test passed.");
|
|
};
|
|
|
|
main().catch((err) => {
|
|
console.error("FAILED:", err);
|
|
process.exit(1);
|
|
});
|