Add quak helper list-missing-thumbnails and fix-missing-thumbnails
Some checks failed
check / check (push) Failing after 8s
Some checks failed
check / check (push) Failing after 8s
list-missing-thumbnails: iterates all files across all collections, fetches each thumbnail from the CDN, reports any that are missing or empty. Deduplicates by file ID across collections. fix-missing-thumbnails: for each missing thumbnail, downloads the original file, generates a 720px JPEG thumbnail via sharp, encrypts it with secretstream push (encryptBlob), uploads to a presigned URL, and registers the new thumbnail via PUT /files/thumbnail. New crypto: encryptBlob (secretstream push, single chunk TAG_FINAL). New ApiClient methods: getUploadURL, putFile, putJSON, updateThumbnail. New Client method: getApiClient() for modules that need raw API access. Deps: sharp 0.34.5 (image processing), @types/sharp 0.32.0.
This commit is contained in:
90
bin/quak.ts
90
bin/quak.ts
@@ -9,6 +9,10 @@ import envPaths from "env-paths";
|
||||
import { Client, type ClientSnapshot } from "../src/client.js";
|
||||
import { init } from "../src/crypto/index.js";
|
||||
import { runBackup } from "../src/backup.js";
|
||||
import {
|
||||
listMissingThumbnails,
|
||||
fixMissingThumbnails,
|
||||
} from "../src/thumbnails.js";
|
||||
|
||||
const paths = envPaths("quak", { suffix: "" });
|
||||
const sessionPath = join(paths.data, "session.json");
|
||||
@@ -366,4 +370,90 @@ program
|
||||
process.exit(result.failed > 0 ? 1 : 0);
|
||||
});
|
||||
|
||||
const helper = program
|
||||
.command("helper")
|
||||
.description("Maintenance and repair utilities");
|
||||
|
||||
helper
|
||||
.command("list-missing-thumbnails")
|
||||
.description("List files whose thumbnails are missing or empty")
|
||||
.option("--json", "Output as JSON array")
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
await init();
|
||||
const client = requireSession();
|
||||
const missing = await listMissingThumbnails(client, (msg) => {
|
||||
if (!opts.json) stderr.write(msg + "\n");
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
stdout.write(JSON.stringify(missing, null, 2) + "\n");
|
||||
} else {
|
||||
if (missing.length === 0) {
|
||||
stderr.write("No missing thumbnails found.\n");
|
||||
} else {
|
||||
stderr.write(
|
||||
`\n${missing.length} file(s) with missing thumbnails:\n`,
|
||||
);
|
||||
for (const m of missing) {
|
||||
stdout.write(
|
||||
`${m.fileID}\t${m.title}\t${m.collection}\t${m.reason}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
helper
|
||||
.command("fix-missing-thumbnails")
|
||||
.description(
|
||||
"Generate and upload thumbnails for files that are missing them",
|
||||
)
|
||||
.option(
|
||||
"--file <ids...>",
|
||||
"Specific file IDs to fix (default: fix all missing)",
|
||||
)
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (opts: { file?: string[]; json?: boolean }) => {
|
||||
await init();
|
||||
const client = requireSession();
|
||||
|
||||
let fileIDs: number[];
|
||||
if (opts.file && opts.file.length > 0) {
|
||||
fileIDs = opts.file.map(Number).filter(Number.isFinite);
|
||||
} else {
|
||||
stderr.write("Scanning for missing thumbnails...\n");
|
||||
const missing = await listMissingThumbnails(client, (msg) => {
|
||||
if (!opts.json) stderr.write(msg + "\n");
|
||||
});
|
||||
fileIDs = missing.map((m) => m.fileID);
|
||||
if (fileIDs.length === 0) {
|
||||
stderr.write("No missing thumbnails found.\n");
|
||||
return;
|
||||
}
|
||||
stderr.write(`Found ${fileIDs.length} file(s) to fix.\n`);
|
||||
}
|
||||
|
||||
const results = await fixMissingThumbnails(client, fileIDs, (msg) => {
|
||||
if (!opts.json) stderr.write(msg + "\n");
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
stdout.write(JSON.stringify(results, null, 2) + "\n");
|
||||
} else {
|
||||
const ok = results.filter((r) => r.success).length;
|
||||
const fail = results.filter((r) => !r.success).length;
|
||||
stderr.write(`\n--- Done ---\n`);
|
||||
stderr.write(` Fixed: ${ok}\n`);
|
||||
stderr.write(` Failed: ${fail}\n`);
|
||||
if (fail > 0) {
|
||||
stderr.write("\nFailed files:\n");
|
||||
for (const r of results.filter((r) => !r.success)) {
|
||||
stderr.write(` ${r.fileID}\t${r.title}\t${r.error}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(results.some((r) => !r.success) ? 1 : 0);
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
||||
Reference in New Issue
Block a user