diff --git a/bin/quak.ts b/bin/quak.ts
index f5fcee1..4ac077a 100644
--- a/bin/quak.ts
+++ b/bin/quak.ts
@@ -337,13 +337,25 @@ program
program
.command("backup-metadata")
.description(
- "Dump all decrypted account metadata (no file content) to a directory",
+ "Dump all decrypted account metadata to a directory of JSON files",
)
.argument("
", "Output directory")
- .action(async (dir: string) => {
+ .option(
+ "--ml",
+ "Include ML data (face detections, CLIP embeddings) from the Ente server",
+ )
+ .option(
+ "--exif",
+ "Download each file and extract full EXIF/IPTC/XMP metadata (slow)",
+ )
+ .action(async (dir: string, opts: { ml?: boolean; exif?: boolean }) => {
await init();
const client = requireSession();
- await runMetadataBackup(client, dir, (msg) => stderr.write(msg + "\n"));
+ await runMetadataBackup(client, dir, {
+ mlData: opts.ml,
+ exif: opts.exif,
+ onProgress: (msg) => stderr.write(msg + "\n"),
+ });
});
program
diff --git a/package.json b/package.json
index 836f04d..7d2b90d 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"dependencies": {
"commander": "14.0.3",
"env-paths": "4.0.0",
+ "exif-reader": "2.0.3",
"fast-srp-hap": "2.0.4",
"libsodium-wrappers-sumo": "0.8.4",
"sharp": "0.34.5"
diff --git a/src/metadata-backup.ts b/src/metadata-backup.ts
index 2b622ca..26034cd 100644
--- a/src/metadata-backup.ts
+++ b/src/metadata-backup.ts
@@ -1,18 +1,127 @@
-import { mkdirSync, writeFileSync } from "node:fs";
+import { gunzipSync } from "node:zlib";
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
+import { tmpdir } from "node:os";
+import sharp from "sharp";
+import exifReader from "exif-reader";
import type { Client } from "./client.js";
+import { decryptBlob, fromBase64 } from "./crypto/index.js";
+import type { EnteFile } from "./model/types.js";
export type ProgressCallback = (message: string) => void;
+export interface MetadataBackupOptions {
+ mlData?: boolean;
+ exif?: boolean;
+ onProgress?: ProgressCallback;
+}
+
const sanitizePath = (name: string): string =>
name.replace(/[/\\:*?"<>|]/g, "_").replace(/^\.+/, "_");
+interface RawRemoteFileData {
+ fileID: number;
+ encryptedData: string;
+ decryptionHeader: string;
+ updatedAt?: number;
+}
+
+const fetchMLDataForFiles = async (
+ client: Client,
+ fileIDs: number[],
+ fileKeys: Map,
+): Promise