Merge: decrypt magic metadata + per-file JSON in backups
All three metadata layers (basic, magicMetadata, pubMagicMetadata) are now decrypted from secretstream blobs and exposed on EnteFile. Backup writes originals/<fileID>.json with the full decrypted metadata including camera make/model, dimensions, datetime, and any face/keyword data the Ente clients have added.
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)) {
|
||||
const target = relative(colDir, origPath);
|
||||
symlinkSync(target, linkPath);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
FileType,
|
||||
RawCollection,
|
||||
RawEnteFile,
|
||||
RawMagicMetadata,
|
||||
} from "./types.js";
|
||||
|
||||
const KNOWN_COLLECTION_TYPES = new Set([
|
||||
@@ -88,14 +89,32 @@ export const decryptFile = (
|
||||
hash: metadataJSON.hash,
|
||||
};
|
||||
|
||||
const magicMetadata = decryptMagicMetadata(raw.magicMetadata, key);
|
||||
const pubMagicMetadata = decryptMagicMetadata(raw.pubMagicMetadata, key);
|
||||
|
||||
return {
|
||||
id: raw.id,
|
||||
collectionID: raw.collectionID,
|
||||
ownerID: raw.ownerID,
|
||||
key,
|
||||
metadata,
|
||||
magicMetadata,
|
||||
pubMagicMetadata,
|
||||
file: { decryptionHeader: raw.file.decryptionHeader },
|
||||
thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader },
|
||||
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,
|
||||
RawCollection,
|
||||
RawEnteFile,
|
||||
RawMagicMetadata,
|
||||
} from "./types.js";
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface EnteFile {
|
||||
ownerID: number;
|
||||
key: Uint8Array;
|
||||
metadata: FileMetadata;
|
||||
magicMetadata?: Record<string, unknown>;
|
||||
pubMagicMetadata?: Record<string, unknown>;
|
||||
file: FileBlob;
|
||||
thumbnail: FileBlob;
|
||||
updationTime: Microseconds;
|
||||
@@ -59,6 +61,13 @@ export interface RawCollection {
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface RawMagicMetadata {
|
||||
version: number;
|
||||
count: number;
|
||||
data: string;
|
||||
header: string;
|
||||
}
|
||||
|
||||
export interface RawEnteFile {
|
||||
id: number;
|
||||
collectionID: number;
|
||||
@@ -66,6 +75,8 @@ export interface RawEnteFile {
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
metadata: { encryptedData: string; decryptionHeader: string };
|
||||
magicMetadata?: RawMagicMetadata;
|
||||
pubMagicMetadata?: RawMagicMetadata;
|
||||
info?: { fileSize?: number; thumbSize?: number };
|
||||
file: { decryptionHeader: string };
|
||||
thumbnail: { decryptionHeader: string };
|
||||
|
||||
Reference in New Issue
Block a user