Ports the CLI to the library API. collections/files/get/get-thumb use the fresh read variants (Library.fresh(), current server state); backup runs lib.backup; backup-metadata and the missing-thumbnail helpers enumerate via the library. files/get output is byte-identical to the pre-port CLI — raw metadata.title, microsecond creationTime, pre-port row order — exit codes unchanged. Adds --cache-dir; fixes the helper JPEG-only assumption (closes #17). Model: opus-4-8
This commit was merged in pull request #74.
This commit is contained in:
+32
-25
@@ -1,15 +1,9 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as jpeg from "jpeg-js";
|
||||
import exifReader from "exif-reader";
|
||||
import type { Client } from "./client.js";
|
||||
import type { Library, Photo } from "./library/index.js";
|
||||
import { fetchMLData } from "./mldata-fetch.js";
|
||||
import type { EnteFile } from "./model/types.js";
|
||||
|
||||
@@ -104,24 +98,29 @@ const extractImageMetadata = (
|
||||
}
|
||||
};
|
||||
|
||||
// Read a file's original bytes through the library's content cache and extract
|
||||
// its embedded image metadata. The bytes come from `photo.original()` — the
|
||||
// same on-disk cache the rest of the library fills — rather than a fresh
|
||||
// per-call download to a throwaway temp file.
|
||||
const extractExif = async (
|
||||
client: Client,
|
||||
file: EnteFile,
|
||||
photo: Photo,
|
||||
): Promise<Record<string, unknown> | undefined> => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "quak-exif-"));
|
||||
try {
|
||||
const origPath = join(tmpDir, "original");
|
||||
await client.downloadFile(file, origPath);
|
||||
const fileBytes = new Uint8Array(readFileSync(origPath));
|
||||
const { path } = await photo.original();
|
||||
const fileBytes = new Uint8Array(readFileSync(path));
|
||||
return extractImageMetadata(fileBytes);
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Dump every decrypted metadata layer the account holds into a directory tree
|
||||
// of plain JSON: account, per-collection, and per-file records including the
|
||||
// private and public magic metadata and (by default) the ML data. Collections
|
||||
// and files are enumerated from the library's cache rather than a fresh server
|
||||
// scan; the ML fetch and EXIF extraction are unchanged.
|
||||
export const runMetadataBackup = async (
|
||||
lib: Library,
|
||||
client: Client,
|
||||
outDir: string,
|
||||
opts?: MetadataBackupOptions,
|
||||
@@ -139,13 +138,19 @@ export const runMetadataBackup = async (
|
||||
);
|
||||
|
||||
log("Fetching collections...");
|
||||
const collections = await client.listCollections();
|
||||
|
||||
const allFiles: { file: EnteFile; colDirName: string }[] = [];
|
||||
// Enumerate through the library's read surface. Each album carries its
|
||||
// photos, but the full decrypted `Collection`/`EnteFile` records (with the
|
||||
// magic-metadata layers this dump exists to preserve) come from the
|
||||
// library's by-id accessors.
|
||||
const allFiles: { file: EnteFile; photo: Photo; colDirName: string }[] = [];
|
||||
const fileKeys = new Map<number, Uint8Array>();
|
||||
const seenFileIDs = new Set<number>();
|
||||
|
||||
for (const col of collections) {
|
||||
for (const album of lib.albums.list()) {
|
||||
const col = lib.getCollection(album.collectionID);
|
||||
if (!col) continue;
|
||||
|
||||
const dirName = `${col.id}-${sanitizePath(col.name || "unnamed")}`;
|
||||
const colDir = join(outDir, "collections", dirName);
|
||||
mkdirSync(colDir, { recursive: true });
|
||||
@@ -170,11 +175,13 @@ export const runMetadataBackup = async (
|
||||
);
|
||||
|
||||
log(`[${col.name}] Fetching files...`);
|
||||
const files = await client.listFiles(col.id, col.key);
|
||||
log(`[${col.name}] ${files.length} file(s)`);
|
||||
const photos = album.photos.list();
|
||||
log(`[${col.name}] ${photos.length} file(s)`);
|
||||
|
||||
for (const file of files) {
|
||||
allFiles.push({ file, colDirName: dirName });
|
||||
for (const photo of photos) {
|
||||
const file = lib.getFile(col.id, photo.fileID);
|
||||
if (!file) continue;
|
||||
allFiles.push({ file, photo, colDirName: dirName });
|
||||
if (!seenFileIDs.has(file.id)) {
|
||||
fileKeys.set(file.id, file.key);
|
||||
seenFileIDs.add(file.id);
|
||||
@@ -191,7 +198,7 @@ export const runMetadataBackup = async (
|
||||
log(`Got ML data for ${mlDataMap.size} file(s)`);
|
||||
|
||||
const writtenFileIDs = new Set<number>();
|
||||
for (const { file, colDirName } of allFiles) {
|
||||
for (const { file, photo, colDirName } of allFiles) {
|
||||
const colDir = join(outDir, "collections", colDirName);
|
||||
|
||||
const fileMeta: Record<string, unknown> = {
|
||||
@@ -210,7 +217,7 @@ export const runMetadataBackup = async (
|
||||
|
||||
if (wantExif && !writtenFileIDs.has(file.id)) {
|
||||
log(`[${file.metadata.title}] Extracting EXIF...`);
|
||||
const exifData = await extractExif(client, file);
|
||||
const exifData = await extractExif(photo);
|
||||
if (exifData) fileMeta.imageMetadata = exifData;
|
||||
}
|
||||
writtenFileIDs.add(file.id);
|
||||
|
||||
Reference in New Issue
Block a user