Decrypt and persist all file metadata layers
Extends RawEnteFile and EnteFile with optional magicMetadata and pubMagicMetadata fields. Both are secretstream blobs under the file key, decrypted to arbitrary JSON (Record<string, unknown>). pubMagicMetadata carries ML-derived data from the Ente clients: camera make/model, image dimensions, datetime with timezone offset, and (when present) captions, editedName, face labels, keywords. magicMetadata carries private mutable fields like visibility. Backup now writes per-file JSON at originals/<fileID>.json containing all three metadata layers (basic + magic + pubMagic). Live-tested: all 11 files in the dev account have pubMagicMetadata with SONY DSC-RX1RM3 camera info and 3000x2000 dimensions.
This commit is contained in:
@@ -116,6 +116,24 @@ export const runBackup = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write per-file metadata JSON alongside the original
|
||||||
|
const metaJsonPath = join(originalsDir, `${file.id}.json`);
|
||||||
|
if (!existsSync(metaJsonPath)) {
|
||||||
|
const fileMeta: Record<string, unknown> = {
|
||||||
|
id: file.id,
|
||||||
|
collectionID: file.collectionID,
|
||||||
|
ownerID: file.ownerID,
|
||||||
|
metadata: file.metadata,
|
||||||
|
};
|
||||||
|
if (file.magicMetadata) {
|
||||||
|
fileMeta.magicMetadata = file.magicMetadata;
|
||||||
|
}
|
||||||
|
if (file.pubMagicMetadata) {
|
||||||
|
fileMeta.pubMagicMetadata = file.pubMagicMetadata;
|
||||||
|
}
|
||||||
|
writeFileSync(metaJsonPath, JSON.stringify(fileMeta, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (!existsSync(linkPath) && existsSync(origPath)) {
|
if (!existsSync(linkPath) && existsSync(origPath)) {
|
||||||
const target = relative(colDir, origPath);
|
const target = relative(colDir, origPath);
|
||||||
symlinkSync(target, linkPath);
|
symlinkSync(target, linkPath);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
FileType,
|
FileType,
|
||||||
RawCollection,
|
RawCollection,
|
||||||
RawEnteFile,
|
RawEnteFile,
|
||||||
|
RawMagicMetadata,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
const KNOWN_COLLECTION_TYPES = new Set([
|
const KNOWN_COLLECTION_TYPES = new Set([
|
||||||
@@ -88,14 +89,32 @@ export const decryptFile = (
|
|||||||
hash: metadataJSON.hash,
|
hash: metadataJSON.hash,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const magicMetadata = decryptMagicMetadata(raw.magicMetadata, key);
|
||||||
|
const pubMagicMetadata = decryptMagicMetadata(raw.pubMagicMetadata, key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: raw.id,
|
id: raw.id,
|
||||||
collectionID: raw.collectionID,
|
collectionID: raw.collectionID,
|
||||||
ownerID: raw.ownerID,
|
ownerID: raw.ownerID,
|
||||||
key,
|
key,
|
||||||
metadata,
|
metadata,
|
||||||
|
magicMetadata,
|
||||||
|
pubMagicMetadata,
|
||||||
file: { decryptionHeader: raw.file.decryptionHeader },
|
file: { decryptionHeader: raw.file.decryptionHeader },
|
||||||
thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader },
|
thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader },
|
||||||
updationTime: raw.updationTime,
|
updationTime: raw.updationTime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const decryptMagicMetadata = (
|
||||||
|
raw: RawMagicMetadata | undefined,
|
||||||
|
key: Uint8Array,
|
||||||
|
): Record<string, unknown> | undefined => {
|
||||||
|
if (!raw?.data || !raw?.header) return undefined;
|
||||||
|
const bytes = decryptBlob(
|
||||||
|
fromBase64(raw.data),
|
||||||
|
fromBase64(raw.header),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
return JSON.parse(new TextDecoder().decode(bytes));
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export type {
|
|||||||
Microseconds,
|
Microseconds,
|
||||||
RawCollection,
|
RawCollection,
|
||||||
RawEnteFile,
|
RawEnteFile,
|
||||||
|
RawMagicMetadata,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export interface EnteFile {
|
|||||||
ownerID: number;
|
ownerID: number;
|
||||||
key: Uint8Array;
|
key: Uint8Array;
|
||||||
metadata: FileMetadata;
|
metadata: FileMetadata;
|
||||||
|
magicMetadata?: Record<string, unknown>;
|
||||||
|
pubMagicMetadata?: Record<string, unknown>;
|
||||||
file: FileBlob;
|
file: FileBlob;
|
||||||
thumbnail: FileBlob;
|
thumbnail: FileBlob;
|
||||||
updationTime: Microseconds;
|
updationTime: Microseconds;
|
||||||
@@ -59,6 +61,13 @@ export interface RawCollection {
|
|||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawMagicMetadata {
|
||||||
|
version: number;
|
||||||
|
count: number;
|
||||||
|
data: string;
|
||||||
|
header: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RawEnteFile {
|
export interface RawEnteFile {
|
||||||
id: number;
|
id: number;
|
||||||
collectionID: number;
|
collectionID: number;
|
||||||
@@ -66,6 +75,8 @@ export interface RawEnteFile {
|
|||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
keyDecryptionNonce: string;
|
keyDecryptionNonce: string;
|
||||||
metadata: { encryptedData: string; decryptionHeader: string };
|
metadata: { encryptedData: string; decryptionHeader: string };
|
||||||
|
magicMetadata?: RawMagicMetadata;
|
||||||
|
pubMagicMetadata?: RawMagicMetadata;
|
||||||
info?: { fileSize?: number; thumbSize?: number };
|
info?: { fileSize?: number; thumbSize?: number };
|
||||||
file: { decryptionHeader: string };
|
file: { decryptionHeader: string };
|
||||||
thumbnail: { decryptionHeader: string };
|
thumbnail: { decryptionHeader: string };
|
||||||
|
|||||||
Reference in New Issue
Block a user