Compare commits
2
Commits
4c325edc9b
...
2e885d54e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e885d54e5 | ||
|
|
6757ddea94 |
@@ -449,10 +449,13 @@ quak helper fix-missing-thumbnails [--file ids] generate + upload missing thumbn
|
|||||||
```
|
```
|
||||||
|
|
||||||
Every command runs on the same cache-backed library. The read commands —
|
Every command runs on the same cache-backed library. The read commands —
|
||||||
`collections`, `files`, `get`, and `get-thumb` — force a fresh server round-trip
|
`collections`, `files`, `get`, `get-thumb`, `backup-metadata`,
|
||||||
before they answer, so they report current account state rather than whatever
|
`helper list-missing-thumbnails` and `helper fix-missing-thumbnails` — force a
|
||||||
the cache last held. `--cache-dir` overrides where the cache lives; without it
|
fresh server round-trip before they answer, so they report current account state
|
||||||
each account gets its own directory under the per-user cache path.
|
rather than whatever the cache last held. If that round-trip fails, the command
|
||||||
|
prints the error on one line and exits 1. `--cache-dir` overrides where the
|
||||||
|
cache lives; without it each account gets its own directory under the per-user
|
||||||
|
cache path.
|
||||||
|
|
||||||
`get` and `get-thumb` resolve the file by ID directly, so `--collection` is
|
`get` and `get-thumb` resolve the file by ID directly, so `--collection` is
|
||||||
accepted for backward compatibility but ignored. `backup-metadata --exif` (alias
|
accepted for backward compatibility but ignored. `backup-metadata --exif` (alias
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ Tag v1.0.0.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-09-23: `backup-metadata`, `helper list-missing-thumbnails` and
|
||||||
|
`helper fix-missing-thumbnails` refresh before they answer (issue 100). Each
|
||||||
|
awaits `lib.fresh()` before reading, so a file added since the cache was
|
||||||
|
written is included, and a failed refresh prints one line and exits 1 instead
|
||||||
|
of answering from a stale or empty cache. The README lists them among the
|
||||||
|
commands that refresh first.
|
||||||
|
|
||||||
|
- 2026-09-23: `quak backup` waits for the server refresh and fails when it fails
|
||||||
|
(issue 99). `lib.backup()` joins a refresh already running or starts one, as
|
||||||
|
`fresh()` does, and rejects before touching any file when it fails, leaving
|
||||||
|
`failures.json` as it was, so `quak backup` prints the error as one line and
|
||||||
|
exits 1 instead of backing up the previous run's file list, or nothing, and
|
||||||
|
exiting 0.
|
||||||
|
|
||||||
- 2026-09-23: CLI errors print a message instead of a stack trace (issue 102).
|
- 2026-09-23: CLI errors print a message instead of a stack trace (issue 102).
|
||||||
An error a command throws is printed as one `quak: MESSAGE` line on stderr and
|
An error a command throws is printed as one `quak: MESSAGE` line on stderr and
|
||||||
the CLI exits 1 once output has drained. The wrapper that does this moved from
|
the CLI exits 1 once output has drained. The wrapper that does this moved from
|
||||||
|
|||||||
+5
-4
@@ -1,9 +1,10 @@
|
|||||||
// The backup command, rebuilt on the library API (issue #51).
|
// The backup command, rebuilt on the library API (issue #51).
|
||||||
//
|
//
|
||||||
// `lib.backup()` refreshes the library, then, for every file in scope, gets its
|
// `lib.backup()` waits for a completed refresh of the library (a failed one
|
||||||
// original bytes onto disk under `downloadDirectory` and rebuilds the derived
|
// fails the backup before any file is touched), then, for every file in scope,
|
||||||
// views (per-file sidecars, per-collection symlink trees, per-collection JSON)
|
// gets its original bytes onto disk under `downloadDirectory` and rebuilds the
|
||||||
// from the model. The on-disk layout is the historical one, unchanged:
|
// derived views (per-file sidecars, per-collection symlink trees,
|
||||||
|
// per-collection JSON) from the model. The on-disk layout is the historical one, unchanged:
|
||||||
//
|
//
|
||||||
// <downloadDirectory>/
|
// <downloadDirectory>/
|
||||||
// originals/<fileID>.<ext> the decrypted bytes
|
// originals/<fileID>.<ext> the decrypted bytes
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ export const backupMetadataCommand = async (
|
|||||||
if (!client) return 1;
|
if (!client) return 1;
|
||||||
const lib = await openReadLibrary(ctx, client);
|
const lib = await openReadLibrary(ctx, client);
|
||||||
try {
|
try {
|
||||||
|
// Refresh first so the dump holds current account state, not what the
|
||||||
|
// cache last held; a failed refresh throws.
|
||||||
|
await lib.fresh();
|
||||||
const { failedMLBatches } = await runMetadataBackup(lib, client, dir, {
|
const { failedMLBatches } = await runMetadataBackup(lib, client, dir, {
|
||||||
exif: opts.exif || opts.all,
|
exif: opts.exif || opts.all,
|
||||||
onProgress: (msg) => ctx.stderr.write(msg + "\n"),
|
onProgress: (msg) => ctx.stderr.write(msg + "\n"),
|
||||||
@@ -423,6 +426,9 @@ export const listMissingThumbnailsCommand = async (
|
|||||||
if (!client) return 1;
|
if (!client) return 1;
|
||||||
const lib = await openReadLibrary(ctx, client);
|
const lib = await openReadLibrary(ctx, client);
|
||||||
try {
|
try {
|
||||||
|
// Refresh first so files added since the cache was written are
|
||||||
|
// checked; a failed refresh throws.
|
||||||
|
await lib.fresh();
|
||||||
const missing = await listMissingThumbnails(lib, client, (msg) => {
|
const missing = await listMissingThumbnails(lib, client, (msg) => {
|
||||||
if (!opts.json) ctx.stderr.write(msg + "\n");
|
if (!opts.json) ctx.stderr.write(msg + "\n");
|
||||||
});
|
});
|
||||||
@@ -458,6 +464,9 @@ export const fixMissingThumbnailsCommand = async (
|
|||||||
if (!client) return 1;
|
if (!client) return 1;
|
||||||
const lib = await openReadLibrary(ctx, client);
|
const lib = await openReadLibrary(ctx, client);
|
||||||
try {
|
try {
|
||||||
|
// Refresh first so files added since the cache was written are found;
|
||||||
|
// a failed refresh throws.
|
||||||
|
await lib.fresh();
|
||||||
let fileIDs: number[];
|
let fileIDs: number[];
|
||||||
if (opts.file && opts.file.length > 0) {
|
if (opts.file && opts.file.length > 0) {
|
||||||
fileIDs = opts.file.map(Number).filter(Number.isFinite);
|
fileIDs = opts.file.map(Number).filter(Number.isFinite);
|
||||||
|
|||||||
@@ -538,11 +538,13 @@ export class Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Back up every in-scope file to `downloadDirectory` in the historical
|
// Back up every in-scope file to `downloadDirectory` in the historical
|
||||||
// on-disk layout, with a durable failure ledger (issue #51). Refreshes
|
// on-disk layout, with a durable failure ledger (issue #51). Waits for a
|
||||||
// first, fetches pending originals (and optional thumbnails) through the
|
// completed refresh first, as `fresh()` does, joining one already running,
|
||||||
// content cache and pools, then rebuilds the derived symlink/JSON views
|
// and rejects before touching any file when it fails. Then fetches pending
|
||||||
// from the model. Throws before any network work when no download directory
|
// originals (and optional thumbnails) through the content cache and pools,
|
||||||
// is available or no content cache backs the originals it must fetch.
|
// and rebuilds the derived symlink/JSON views from the model. Throws before
|
||||||
|
// any network work when no download directory is available or no content
|
||||||
|
// cache backs the originals it must fetch.
|
||||||
backup(opts?: BackupOptions): Promise<BackupResult> {
|
backup(opts?: BackupOptions): Promise<BackupResult> {
|
||||||
const downloadDirectory =
|
const downloadDirectory =
|
||||||
opts?.downloadDirectory ?? this.downloadDirectory;
|
opts?.downloadDirectory ?? this.downloadDirectory;
|
||||||
@@ -566,7 +568,7 @@ export class Library {
|
|||||||
const cache = this.cache;
|
const cache = this.cache;
|
||||||
return runBackup(
|
return runBackup(
|
||||||
{
|
{
|
||||||
refresh: () => this.runRefresh(),
|
refresh: () => this.refreshNow(),
|
||||||
listCollections: () => this.store.listCollections(),
|
listCollections: () => this.store.listCollections(),
|
||||||
listFiles: (id) => this.store.listFiles(id),
|
listFiles: (id) => this.store.listFiles(id),
|
||||||
original: (fileID) => cache!.original(fileID),
|
original: (fileID) => cache!.original(fileID),
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ const extractExif = async (
|
|||||||
// Dump every decrypted metadata layer the account holds into a directory tree
|
// Dump every decrypted metadata layer the account holds into a directory tree
|
||||||
// of plain JSON: account, per-collection, and per-file records including the
|
// of plain JSON: account, per-collection, and per-file records including the
|
||||||
// private and public magic metadata and (by default) the ML data. Collections
|
// 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
|
// and files are enumerated from the library's cache, which the caller refreshes
|
||||||
// scan. Returns how many ML data requests failed; their files are still
|
// first. Returns how many ML data requests failed; their files are still
|
||||||
// written, with `mlDataError` in place of `mlData`.
|
// written, with `mlDataError` in place of `mlData`.
|
||||||
export const runMetadataBackup = async (
|
export const runMetadataBackup = async (
|
||||||
lib: Library,
|
lib: Library,
|
||||||
|
|||||||
@@ -625,6 +625,93 @@ describe("lib.backup", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Every refresh fails, as with an expired session or no network.
|
||||||
|
class FailingClient extends MockClient {
|
||||||
|
override async collectionsSince(): Promise<CollectionsPage> {
|
||||||
|
throw new Error("HTTP 401 from server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holds its refresh open until `release()` is called, then reports a third
|
||||||
|
// album, so a backup can be started while that refresh is still running.
|
||||||
|
class HeldClient extends MockClient {
|
||||||
|
release!: () => void;
|
||||||
|
private held = new Promise<void>((resolve) => {
|
||||||
|
this.release = resolve;
|
||||||
|
});
|
||||||
|
override async collectionsSince(): Promise<CollectionsPage> {
|
||||||
|
await this.held;
|
||||||
|
return {
|
||||||
|
collections: [collection(3, "Later")],
|
||||||
|
deleted: [],
|
||||||
|
cursor: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
override async filesSince(args: {
|
||||||
|
collectionID: number;
|
||||||
|
}): Promise<FilesPage> {
|
||||||
|
if (args.collectionID !== 3) return super.filesSince(args);
|
||||||
|
return { files: [file(300, 3, "late.jpg")], deleted: [], cursor: 2 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the library cache on disk, so the next open starts its refresh in the
|
||||||
|
// background instead of waiting for it.
|
||||||
|
const fillCache = async (): Promise<void> => {
|
||||||
|
const lib = await openLibrary(stubSource());
|
||||||
|
await lib.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("the refresh before a backup", () => {
|
||||||
|
it("waits for a refresh already running and backs up what it found", async () => {
|
||||||
|
await fillCache();
|
||||||
|
const client = new HeldClient();
|
||||||
|
const lib = await openLibrary(stubSource(), client);
|
||||||
|
const outDir = join(root, "backup");
|
||||||
|
|
||||||
|
const backup = lib.backup({ downloadDirectory: outDir });
|
||||||
|
client.release();
|
||||||
|
const result = await backup;
|
||||||
|
|
||||||
|
expect(result.totalFiles).toBe(4);
|
||||||
|
expect(existsSync(join(outDir, "originals", "300.jpg"))).toBe(true);
|
||||||
|
await lib.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails before any download when the refresh fails, leaving failures.json as it was", async () => {
|
||||||
|
await fillCache();
|
||||||
|
const outDir = join(root, "backup");
|
||||||
|
seedLedger(outDir, 100, "beach.jpg");
|
||||||
|
const ledgerPath = join(outDir, "failures.json");
|
||||||
|
const ledgerBefore = readFileSync(ledgerPath, "utf-8");
|
||||||
|
const source = stubSource();
|
||||||
|
const lib = await openLibrary(source, new FailingClient());
|
||||||
|
|
||||||
|
await expect(lib.backup({ downloadDirectory: outDir })).rejects.toThrow(
|
||||||
|
"HTTP 401 from server",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(source.originalCalls).toBe(0);
|
||||||
|
expect(readFileSync(ledgerPath, "utf-8")).toBe(ledgerBefore);
|
||||||
|
expect(existsSync(join(outDir, "originals"))).toBe(false);
|
||||||
|
await lib.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when the refresh fails on an empty cache, instead of backing up nothing", async () => {
|
||||||
|
const source = stubSource();
|
||||||
|
const lib = await openLibrary(source, new FailingClient());
|
||||||
|
const outDir = join(root, "backup");
|
||||||
|
|
||||||
|
await expect(lib.backup({ downloadDirectory: outDir })).rejects.toThrow(
|
||||||
|
"HTTP 401 from server",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(source.originalCalls).toBe(0);
|
||||||
|
expect(existsSync(outDir)).toBe(false);
|
||||||
|
await lib.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// The album folders under collections/, driven through `runBackup` with a
|
// The album folders under collections/, driven through `runBackup` with a
|
||||||
// stand-in library whose albums a test changes between runs.
|
// stand-in library whose albums a test changes between runs.
|
||||||
describe("backup album folders", () => {
|
describe("backup album folders", () => {
|
||||||
|
|||||||
+122
-8
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
import { PassThrough } from "node:stream";
|
||||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -32,8 +33,11 @@ import {
|
|||||||
getCommand,
|
getCommand,
|
||||||
getThumbCommand,
|
getThumbCommand,
|
||||||
backupCommand,
|
backupCommand,
|
||||||
|
backupMetadataCommand,
|
||||||
listMissingThumbnailsCommand,
|
listMissingThumbnailsCommand,
|
||||||
|
fixMissingThumbnailsCommand,
|
||||||
} from "../../src/cli-commands.js";
|
} from "../../src/cli-commands.js";
|
||||||
|
import { run } from "../../src/cli-run.js";
|
||||||
import { loadSession } from "../../src/cli-session.js";
|
import { loadSession } from "../../src/cli-session.js";
|
||||||
import type { Client, ClientSnapshot } from "../../src/client.js";
|
import type { Client, ClientSnapshot } from "../../src/client.js";
|
||||||
import type { ContentSource } from "../../src/library/content.js";
|
import type { ContentSource } from "../../src/library/content.js";
|
||||||
@@ -82,8 +86,26 @@ const FILES: Record<number, EnteFile[]> = {
|
|||||||
|
|
||||||
// An original is 7 bytes and a thumbnail 3. `failID` makes that file's
|
// An original is 7 bytes and a thumbnail 3. `failID` makes that file's
|
||||||
// original fail; `emptyThumbID` makes the server report that file's
|
// original fail; `emptyThumbID` makes the server report that file's
|
||||||
// thumbnail as empty.
|
// thumbnail as empty. `withNewFile` adds new.jpg (102) to Vacation, advancing
|
||||||
const fakeClient = (opts: { failID?: number; emptyThumbID?: number } = {}) => {
|
// the collection's updationTime as the server does, and `refreshError` makes
|
||||||
|
// listing collections fail with that message.
|
||||||
|
const fakeClient = (
|
||||||
|
opts: {
|
||||||
|
failID?: number;
|
||||||
|
emptyThumbID?: number;
|
||||||
|
withNewFile?: boolean;
|
||||||
|
refreshError?: string;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const collections = opts.withNewFile
|
||||||
|
? [{ ...COLLECTIONS[0], updationTime: 2 }, COLLECTIONS[1]]
|
||||||
|
: COLLECTIONS;
|
||||||
|
const files = opts.withNewFile
|
||||||
|
? {
|
||||||
|
...FILES,
|
||||||
|
1: [...FILES[1], { ...file(102, 1, "new.jpg"), updationTime: 2 }],
|
||||||
|
}
|
||||||
|
: FILES;
|
||||||
const source: ContentSource = {
|
const source: ContentSource = {
|
||||||
original: async ({ file: f, destination }) => {
|
original: async ({ file: f, destination }) => {
|
||||||
if (f.id === opts.failID) throw new Error("HTTP 500 from server");
|
if (f.id === opts.failID) throw new Error("HTTP 500 from server");
|
||||||
@@ -97,18 +119,19 @@ const fakeClient = (opts: { failID?: number; emptyThumbID?: number } = {}) => {
|
|||||||
};
|
};
|
||||||
const fake = {
|
const fake = {
|
||||||
whoami: () => ({ email: "cli@example.com", userID: USER_ID }),
|
whoami: () => ({ email: "cli@example.com", userID: USER_ID }),
|
||||||
collectionsSince: async () => ({
|
collectionsSince: async () => {
|
||||||
collections: COLLECTIONS,
|
if (opts.refreshError) throw new Error(opts.refreshError);
|
||||||
deleted: [],
|
return { collections, deleted: [], cursor: 1 };
|
||||||
cursor: 1,
|
},
|
||||||
}),
|
|
||||||
filesSince: async (args: { collectionID: number }) => ({
|
filesSince: async (args: { collectionID: number }) => ({
|
||||||
files: FILES[args.collectionID] ?? [],
|
files: files[args.collectionID] ?? [],
|
||||||
deleted: [],
|
deleted: [],
|
||||||
cursor: 1,
|
cursor: 1,
|
||||||
}),
|
}),
|
||||||
contentSource: () => source,
|
contentSource: () => source,
|
||||||
getApiClient: () => ({
|
getApiClient: () => ({
|
||||||
|
// The ML data request of `backup-metadata`: no file has any.
|
||||||
|
postJSON: async () => ({ data: [] }),
|
||||||
getThumbnailStream: async (fileID: number) =>
|
getThumbnailStream: async (fileID: number) =>
|
||||||
new ReadableStream<Uint8Array>({
|
new ReadableStream<Uint8Array>({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
@@ -430,6 +453,34 @@ describe("backup", () => {
|
|||||||
expect(result.errors[0].fileID).toBe(101);
|
expect(result.errors[0].fileID).toBe(101);
|
||||||
expect(stderr.text).toBe("Starting backup...\n");
|
expect(stderr.text).toBe("Starting backup...\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exits 1 with the error on one line when the refresh fails", async () => {
|
||||||
|
const client = {
|
||||||
|
...fakeClient(),
|
||||||
|
collectionsSince: async () => {
|
||||||
|
throw new Error("HTTP 401 from server");
|
||||||
|
},
|
||||||
|
} as unknown as Client;
|
||||||
|
const dir = join(root, "backup");
|
||||||
|
// Through `run`, as `bin/quak.ts` does, which prints a thrown error.
|
||||||
|
const runStderr = new PassThrough();
|
||||||
|
let runText = "";
|
||||||
|
runStderr.on("data", (chunk: Buffer) => {
|
||||||
|
runText += chunk.toString();
|
||||||
|
});
|
||||||
|
const code = await new Promise<number>((resolve) => {
|
||||||
|
void run(
|
||||||
|
backupCommand(context(client), dir, {}),
|
||||||
|
new PassThrough(),
|
||||||
|
runStderr,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(runText).toBe("quak: HTTP 401 from server\n");
|
||||||
|
expect(stderr.text).toBe("Starting backup...\nRefreshing library...\n");
|
||||||
|
expect(existsSync(join(dir, "originals"))).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("helper list-missing-thumbnails", () => {
|
describe("helper list-missing-thumbnails", () => {
|
||||||
@@ -462,3 +513,66 @@ describe("helper list-missing-thumbnails", () => {
|
|||||||
expect(stderr.text).toBe("");
|
expect(stderr.text).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Each test first runs `collections` so the cache holds the account as it was,
|
||||||
|
// then changes the server under it.
|
||||||
|
describe("backup-metadata and the thumbnail helpers refresh first", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
expect(await collectionsCommand(context(), {})).toBe(0);
|
||||||
|
stdout.text = "";
|
||||||
|
stderr.text = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("backup-metadata writes a file added since the cache was written", async () => {
|
||||||
|
const ctx = context(fakeClient({ withNewFile: true }));
|
||||||
|
const dir = join(root, "dump");
|
||||||
|
expect(await backupMetadataCommand(ctx, dir, {})).toBe(0);
|
||||||
|
expect(
|
||||||
|
existsSync(join(dir, "collections", "1-Vacation", "102.json")),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list-missing-thumbnails checks a file added since the cache was written", async () => {
|
||||||
|
const ctx = context(
|
||||||
|
fakeClient({ withNewFile: true, emptyThumbID: 102 }),
|
||||||
|
);
|
||||||
|
expect(await listMissingThumbnailsCommand(ctx, {})).toBe(0);
|
||||||
|
expect(stdout.text).toBe(
|
||||||
|
"102\tnew.jpg\tVacation\tempty thumbnail (0 bytes)\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fix-missing-thumbnails finds a file added since the cache was written", async () => {
|
||||||
|
const ctx = context(fakeClient({ withNewFile: true }));
|
||||||
|
expect(
|
||||||
|
await fixMissingThumbnailsCommand(ctx, {
|
||||||
|
file: ["102"],
|
||||||
|
json: true,
|
||||||
|
}),
|
||||||
|
).toBe(0);
|
||||||
|
// Found, then skipped because the server records no thumbnail size
|
||||||
|
// for it; a file missing from the cache would fail as not found.
|
||||||
|
expect(JSON.parse(stdout.text)).toMatchObject([
|
||||||
|
{ fileID: 102, title: "new.jpg", status: "skipped" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `run` in `cli-run.ts` prints a thrown error as one line and exits 1.
|
||||||
|
it("all three throw when the refresh fails", async () => {
|
||||||
|
const ctx = context(
|
||||||
|
fakeClient({ refreshError: "HTTP 503 from server" }),
|
||||||
|
);
|
||||||
|
const dir = join(root, "dump");
|
||||||
|
await expect(backupMetadataCommand(ctx, dir, {})).rejects.toThrow(
|
||||||
|
"HTTP 503 from server",
|
||||||
|
);
|
||||||
|
expect(existsSync(dir)).toBe(false);
|
||||||
|
await expect(listMissingThumbnailsCommand(ctx, {})).rejects.toThrow(
|
||||||
|
"HTTP 503 from server",
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
fixMissingThumbnailsCommand(ctx, { file: ["100"] }),
|
||||||
|
).rejects.toThrow("HTTP 503 from server");
|
||||||
|
expect(stdout.text).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user