fix: preserve image attachments in live and restored conversations

This commit is contained in:
2026-09-12 23:51:57 +08:00
parent 1d661f7a56
commit 03fc50f7a9
9 changed files with 347 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { createHash, randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
const ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
@@ -22,14 +22,14 @@ export interface CodingAttachmentStoreOptions {
}
export class CodingAttachmentStore {
private readonly createId: () => string;
private readonly createId: (() => string) | undefined;
private readonly maxBytes: number;
constructor(
private readonly rootDir: string,
options: CodingAttachmentStoreOptions = {},
) {
this.createId = options.createId ?? randomUUID;
this.createId = options.createId;
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
}
@@ -39,11 +39,21 @@ export class CodingAttachmentStore {
if (data.byteLength === 0 || data.byteLength > this.maxBytes) {
throw new Error('Attachment size is invalid');
}
const attachmentId = this.createId();
// Live events and session hydration repeat the same large image. Reuse its
// stored bytes, including the original upload, instead of writing a new copy.
const attachmentId = this.createId?.() ?? createHash('sha256')
.update(normalizedMime).update('\0').update(data).digest('hex');
if (!ATTACHMENT_ID_PATTERN.test(attachmentId)) throw new Error('Attachment id is invalid');
await mkdir(this.rootDir, { recursive: true });
const dataPath = path.join(this.rootDir, `${attachmentId}.bin`);
const metadataPath = path.join(this.rootDir, `${attachmentId}.json`);
if (!this.createId) {
const existing = await stat(metadataPath).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
if (existing) return { attachmentId, mime: normalizedMime, byteLength: data.byteLength };
}
const temporaryDataPath = `${dataPath}.${randomUUID()}.tmp`;
const temporaryMetadataPath = `${metadataPath}.${randomUUID()}.tmp`;
const reference: CodingAttachmentRef = {