Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
005612ef76 |
+18
-27
@@ -15,12 +15,6 @@ import envPaths from "env-paths";
|
|||||||
import { Client, type ClientSnapshot } from "../src/client.js";
|
import { Client, type ClientSnapshot } from "../src/client.js";
|
||||||
import { init } from "../src/crypto/index.js";
|
import { init } from "../src/crypto/index.js";
|
||||||
import { Library, type LibraryClient } from "../src/library/index.js";
|
import { Library, type LibraryClient } from "../src/library/index.js";
|
||||||
import {
|
|
||||||
fileListRow,
|
|
||||||
fileListLine,
|
|
||||||
originalName,
|
|
||||||
thumbnailName,
|
|
||||||
} from "../src/cli-output.js";
|
|
||||||
import { runMetadataBackup } from "../src/metadata-backup.js";
|
import { runMetadataBackup } from "../src/metadata-backup.js";
|
||||||
import {
|
import {
|
||||||
listMissingThumbnails,
|
listMissingThumbnails,
|
||||||
@@ -235,21 +229,24 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present each file from its own decrypted metadata, not the
|
const photos = album.photos.list();
|
||||||
// PhotoRecord projection, so the raw title and the microsecond
|
|
||||||
// creationTime print as the pre-library CLI did (issue #52). The
|
|
||||||
// album's photo order is kept; only the field source changes.
|
|
||||||
const files = album.photos.list().flatMap((p) => {
|
|
||||||
const file = lib.getFile(collectionID, p.fileID);
|
|
||||||
return file ? [file] : [];
|
|
||||||
});
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
stdout.write(
|
stdout.write(
|
||||||
JSON.stringify(files.map(fileListRow), null, 2) + "\n",
|
JSON.stringify(
|
||||||
|
photos.map((p) => ({
|
||||||
|
id: p.fileID,
|
||||||
|
title: p.title,
|
||||||
|
fileType: p.fileType,
|
||||||
|
creationTime: p.takenAt,
|
||||||
|
collectionID,
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) + "\n",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for (const file of files) {
|
for (const p of photos) {
|
||||||
stdout.write(fileListLine(file) + "\n");
|
stdout.write(`${p.fileID}\t${p.fileType}\t${p.title}\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finish(lib, 0);
|
finish(lib, 0);
|
||||||
@@ -272,17 +269,14 @@ program
|
|||||||
|
|
||||||
const lib = await openReadLibrary(client);
|
const lib = await openReadLibrary(client);
|
||||||
const photo = lib.photos.byID({ fileID });
|
const photo = lib.photos.byID({ fileID });
|
||||||
const file = lib.getFileByID(fileID);
|
if (!photo) {
|
||||||
if (!photo || !file) {
|
|
||||||
stderr.write(`File ${fileID} not found\n`);
|
stderr.write(`File ${fileID} not found\n`);
|
||||||
finish(lib, 1);
|
finish(lib, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await photo.original();
|
const result = await photo.original();
|
||||||
// Default name is the file's own title, as the pre-library CLI used
|
const outPath = opts.out ?? photo.title;
|
||||||
// (not the editedName-preferring projection title) (issue #52).
|
|
||||||
const outPath = opts.out ?? originalName(file);
|
|
||||||
copyFileSync(result.path, outPath);
|
copyFileSync(result.path, outPath);
|
||||||
stderr.write(`${result.bytes} bytes -> ${outPath}\n`);
|
stderr.write(`${result.bytes} bytes -> ${outPath}\n`);
|
||||||
finish(lib, 0);
|
finish(lib, 0);
|
||||||
@@ -305,17 +299,14 @@ program
|
|||||||
|
|
||||||
const lib = await openReadLibrary(client);
|
const lib = await openReadLibrary(client);
|
||||||
const photo = lib.photos.byID({ fileID });
|
const photo = lib.photos.byID({ fileID });
|
||||||
const file = lib.getFileByID(fileID);
|
if (!photo) {
|
||||||
if (!photo || !file) {
|
|
||||||
stderr.write(`File ${fileID} not found\n`);
|
stderr.write(`File ${fileID} not found\n`);
|
||||||
finish(lib, 1);
|
finish(lib, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await photo.thumbnail();
|
const result = await photo.thumbnail();
|
||||||
// Default name is thumb_<file's own title>, as the pre-library CLI
|
const outPath = opts.out ?? `thumb_${photo.title}`;
|
||||||
// used (not the projection title) (issue #52).
|
|
||||||
const outPath = opts.out ?? thumbnailName(file);
|
|
||||||
copyFileSync(result.path, outPath);
|
copyFileSync(result.path, outPath);
|
||||||
stderr.write(`${result.bytes} bytes -> ${outPath}\n`);
|
stderr.write(`${result.bytes} bytes -> ${outPath}\n`);
|
||||||
finish(lib, 0);
|
finish(lib, 0);
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
// How the CLI presents a file's identity in `files`, `get`, and `get-thumb`.
|
|
||||||
//
|
|
||||||
// These read the file's own decrypted metadata — the raw title and the
|
|
||||||
// creationTime in microseconds — rather than the `PhotoRecord` projection the
|
|
||||||
// rest of the library exposes. The projection prefers `editedName`/`editedTime`
|
|
||||||
// and reports time in milliseconds, which is right for a photo browser but
|
|
||||||
// would change the CLI's externally-visible output. The pre-library CLI printed
|
|
||||||
// `metadata.title` and `metadata.creationTime` and named downloads after
|
|
||||||
// `metadata.title`, and issue #52 requires that output stay byte-identical, so
|
|
||||||
// the commands shape their output from the raw `EnteFile` through here.
|
|
||||||
|
|
||||||
import type { EnteFile, FileType, Microseconds } from "./model/types.js";
|
|
||||||
|
|
||||||
// One row of `quak files --json`.
|
|
||||||
export interface FileListRow {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
fileType: FileType;
|
|
||||||
creationTime: Microseconds;
|
|
||||||
collectionID: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fileListRow = (file: EnteFile): FileListRow => ({
|
|
||||||
id: file.id,
|
|
||||||
title: file.metadata.title,
|
|
||||||
fileType: file.metadata.fileType,
|
|
||||||
creationTime: file.metadata.creationTime,
|
|
||||||
collectionID: file.collectionID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// One line of `quak files` in its human, tab-separated form.
|
|
||||||
export const fileListLine = (file: EnteFile): string =>
|
|
||||||
`${file.id}\t${file.metadata.fileType}\t${file.metadata.title}`;
|
|
||||||
|
|
||||||
// Default output path for `quak get` when `--out` is not given.
|
|
||||||
export const originalName = (file: EnteFile): string => file.metadata.title;
|
|
||||||
|
|
||||||
// Default output path for `quak get-thumb` when `--out` is not given.
|
|
||||||
export const thumbnailName = (file: EnteFile): string =>
|
|
||||||
`thumb_${file.metadata.title}`;
|
|
||||||
@@ -433,13 +433,6 @@ export class Library {
|
|||||||
return this.store.getFile(collectionID, fileID);
|
return this.store.getFile(collectionID, fileID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any membership of a file, addressed by file id alone. A file's own
|
|
||||||
// metadata (title, creationTime) is identical across the collections it
|
|
||||||
// belongs to, so this serves the point commands that hold only a fileID.
|
|
||||||
getFileByID(fileID: number): EnteFile | undefined {
|
|
||||||
return this.store.getFileByID(fileID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A synchronous, RAM-only projection of the whole library into plain
|
// A synchronous, RAM-only projection of the whole library into plain
|
||||||
// records (no keys), the surface the GUI reads across IPC. Photos are
|
// records (no keys), the surface the GUI reads across IPC. Photos are
|
||||||
// deduplicated to one record per file and ordered newest first.
|
// deduplicated to one record per file and ordered newest first.
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
// The CLI presents a file by its own decrypted metadata, not the PhotoRecord
|
|
||||||
// projection (issue #52). For a renamed file the two disagree: the projection
|
|
||||||
// prefers `editedName` and reports `editedTime` in milliseconds, while the CLI
|
|
||||||
// must print the raw `metadata.title` and `metadata.creationTime` (microseconds)
|
|
||||||
// and name downloads after the raw title, byte-identical to the pre-library CLI.
|
|
||||||
//
|
|
||||||
// This locks in that contrast: the shared output helpers emit the raw values,
|
|
||||||
// and the projection of the same file emits the edited ones — so a regression
|
|
||||||
// that re-sourced the CLI from the projection would fail here.
|
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
|
|
||||||
import {
|
|
||||||
fileListRow,
|
|
||||||
fileListLine,
|
|
||||||
originalName,
|
|
||||||
thumbnailName,
|
|
||||||
} from "../../src/cli-output.js";
|
|
||||||
import { deriveRecords } from "../../src/library/records.js";
|
|
||||||
import type { EnteFile } from "../../src/model/types.js";
|
|
||||||
|
|
||||||
// Microseconds, as Ente stores times.
|
|
||||||
const RAW_CREATION = 1700000000000000;
|
|
||||||
const EDITED_TIME = 1710000000000000;
|
|
||||||
const RAW_TITLE = "IMG_0001.HEIC";
|
|
||||||
const EDITED_NAME = "Sunset.heic";
|
|
||||||
|
|
||||||
// A file the user has renamed and re-dated: basic metadata holds the original
|
|
||||||
// title and capture time; public magic metadata holds the edits.
|
|
||||||
const renamedFile: EnteFile = {
|
|
||||||
id: 100,
|
|
||||||
collectionID: 10,
|
|
||||||
ownerID: 42,
|
|
||||||
key: new Uint8Array(),
|
|
||||||
metadata: {
|
|
||||||
title: RAW_TITLE,
|
|
||||||
fileType: "image",
|
|
||||||
creationTime: RAW_CREATION,
|
|
||||||
modificationTime: RAW_CREATION,
|
|
||||||
},
|
|
||||||
pubMagicMetadata: { editedName: EDITED_NAME, editedTime: EDITED_TIME },
|
|
||||||
file: { decryptionHeader: "" },
|
|
||||||
thumbnail: { decryptionHeader: "" },
|
|
||||||
updationTime: RAW_CREATION,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("CLI file output (issue #52)", () => {
|
|
||||||
it("emits the raw title and microsecond creationTime for --json", () => {
|
|
||||||
expect(fileListRow(renamedFile)).toEqual({
|
|
||||||
id: 100,
|
|
||||||
title: RAW_TITLE,
|
|
||||||
fileType: "image",
|
|
||||||
creationTime: RAW_CREATION,
|
|
||||||
collectionID: 10,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits the raw title in the human column", () => {
|
|
||||||
expect(fileListLine(renamedFile)).toBe(`100\timage\t${RAW_TITLE}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("names downloads after the raw title", () => {
|
|
||||||
expect(originalName(renamedFile)).toBe(RAW_TITLE);
|
|
||||||
expect(thumbnailName(renamedFile)).toBe(`thumb_${RAW_TITLE}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not use the editedName/editedTime projection", () => {
|
|
||||||
const record = deriveRecords([], [renamedFile]).photos.get(100);
|
|
||||||
// The projection prefers the edits and reports milliseconds; the CLI
|
|
||||||
// helpers above deliberately do not.
|
|
||||||
expect(record?.title).toBe(EDITED_NAME);
|
|
||||||
expect(record?.takenAt).toBe(Math.floor(EDITED_TIME / 1000));
|
|
||||||
expect(fileListRow(renamedFile).title).not.toBe(record?.title);
|
|
||||||
expect(fileListRow(renamedFile).creationTime).not.toBe(record?.takenAt);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user