Add quak helper list-missing-thumbnails and fix-missing-thumbnails
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:
2026-05-13 21:00:35 -07:00
parent fbd5099d49
commit e9a56d5c8d
8 changed files with 551 additions and 2 deletions

View File

@@ -162,6 +162,52 @@ export class ApiClient {
return resp.body;
}
async getUploadURL(
contentLength: number,
contentMD5: string,
): Promise<{ objectKey: string; url: string }> {
return this.postJSON("/files/upload-url", {
contentLength,
contentMD5,
});
}
async putFile(presignedURL: string, data: Uint8Array): Promise<void> {
const resp = await this._fetch(presignedURL, {
method: "PUT",
headers: {
"Content-Type": "application/octet-stream",
"Content-Length": String(data.length),
},
body: data,
});
if (!resp.ok) {
throw new Error(`PUT to presigned URL failed: HTTP ${resp.status}`);
}
}
async putJSON<T>(path: string, body: unknown): Promise<T> {
const url = `${this.apiOrigin}${path}`;
const resp = await this._fetch(url, {
method: "PUT",
headers: this.headers({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
await this.throwIfError(resp);
return (await resp.json()) as T;
}
async updateThumbnail(
fileID: number,
objectKey: string,
decryptionHeader: string,
): Promise<void> {
await this.putJSON("/files/thumbnail", {
fileID,
thumbnail: { objectKey, decryptionHeader },
});
}
async getThumbnailStream(
fileID: number,
): Promise<ReadableStream<Uint8Array>> {