fix: attachments broken
Some checks failed
Build Android APK / build-android-apk (push) Failing after 3m5s
Queue Release Build / prepare (push) Successful in 32s
Deploy Web Apps / deploy (push) Successful in 9m30s
Queue Release Build / build-windows (push) Successful in 28m7s
Queue Release Build / build-linux (push) Successful in 47m6s
Queue Release Build / finalize (push) Successful in 58s

This commit is contained in:
2026-06-05 08:02:29 +02:00
parent 9a1305f976
commit 35f52b0356
5 changed files with 67 additions and 42 deletions

View File

@@ -5,8 +5,8 @@ import { selectCurrentRoomName } from '../../../../store/rooms/rooms.selectors';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.model';
import { MAX_AUTO_SAVE_SIZE_BYTES } from '../../domain/constants/attachment.constants';
import { LEGACY_ATTACHMENTS_STORAGE_KEY } from '../../domain/constants/attachment-transfer.constants';
import { isBlobObjectUrl, needsBlobObjectUrlForInlineDisplay } from '../../domain/logic/attachment-display-url.rules';
import { mergeAttachmentLocalPaths } from '../../domain/logic/attachment-persistence.rules';
import { AttachmentRuntimeStore } from './attachment-runtime.store';
@@ -113,7 +113,17 @@ export class AttachmentPersistenceService {
}
async tryRestoreAttachmentFromLocal(attachment: Attachment): Promise<boolean> {
if (attachment.available) {
const restored = await this.ensureInlineDisplayObjectUrl(attachment);
if (restored) {
attachment.requestError = undefined;
}
return restored;
}
async ensureInlineDisplayObjectUrl(attachment: Attachment): Promise<boolean> {
if (!needsBlobObjectUrlForInlineDisplay(attachment.objectUrl)) {
return true;
}
@@ -134,19 +144,14 @@ export class AttachmentPersistenceService {
return false;
}
if (await this.restoreMediaAttachmentFromFileUrl(attachment, diskPath)) {
attachment.requestError = undefined;
return true;
}
const base64 = await this.attachmentStorage.readFile(diskPath);
if (!base64) {
return false;
}
this.revokeAttachmentObjectUrl(attachment);
this.restoreAttachmentFromDisk(attachment, base64);
attachment.requestError = undefined;
return true;
}
@@ -291,24 +296,14 @@ export class AttachmentPersistenceService {
);
}
private async restoreMediaAttachmentFromFileUrl(attachment: Attachment, filePath: string): Promise<boolean> {
if (!this.isPlayableMedia(attachment)) {
return false;
private revokeAttachmentObjectUrl(attachment: Attachment): void {
if (!attachment.objectUrl || !isBlobObjectUrl(attachment.objectUrl)) {
return;
}
const fileUrl = await this.attachmentStorage.getFileUrl(filePath);
if (!fileUrl) {
return false;
}
attachment.objectUrl = fileUrl;
attachment.available = true;
return true;
}
private isPlayableMedia(attachment: Pick<Attachment, 'mime'>): boolean {
return attachment.mime.startsWith('video/') || attachment.mime.startsWith('audio/');
try {
URL.revokeObjectURL(attachment.objectUrl);
} catch { /* ignore */ }
}
private async getRetainedSavedPathsForOtherMessages(messageId: string): Promise<Set<string>> {

View File

@@ -148,6 +148,7 @@ export class AttachmentTransferService {
attachment.requestError = isUploader
? UPLOADER_LOCAL_FILE_MISSING_ERROR
: NO_CONNECTED_PEERS_REQUEST_ERROR;
this.runtimeStore.touch();
console.warn('[Attachments] No connected peers to request file from');
return;
@@ -236,8 +237,8 @@ export class AttachmentTransferService {
attachment,
attachment.filePath,
this.attachmentStorage.canCopyFiles()
)) {
const savedPath = await this.persistence.persistUploadCopyFromSourcePath(attachment, attachment.filePath!);
) && attachment.filePath) {
const savedPath = await this.persistence.persistUploadCopyFromSourcePath(attachment, attachment.filePath);
if (savedPath) {
const fileUrl = await this.attachmentStorage.getFileUrl(savedPath);
@@ -659,20 +660,11 @@ export class AttachmentTransferService {
this.runtimeStore.deleteChunkCount(assemblyKey);
if (shouldPersistDownloadedAttachment(attachment)) {
const diskPath = await this.persistence.saveFileToDisk(attachment, blob);
const fileUrl = diskPath && this.isPlayableMedia(attachment)
? await this.attachmentStorage.getFileUrl(diskPath)
: null;
if (fileUrl) {
attachment.objectUrl = fileUrl;
} else {
attachment.objectUrl = URL.createObjectURL(blob);
}
} else {
attachment.objectUrl = URL.createObjectURL(blob);
await this.persistence.saveFileToDisk(attachment, blob);
}
attachment.objectUrl = URL.createObjectURL(blob);
attachment.available = true;
this.runtimeStore.touch();
void this.persistence.persistAttachmentMeta(attachment);
@@ -745,14 +737,14 @@ export class AttachmentTransferService {
return;
}
const fileUrl = await this.attachmentStorage.getFileUrl(assembly.path);
attachment.savedPath = assembly.path;
if (!fileUrl) {
const restoredForDisplay = await this.persistence.ensureInlineDisplayObjectUrl(attachment);
if (!restoredForDisplay) {
throw new Error('Could not open completed media download from disk.');
}
attachment.savedPath = assembly.path;
attachment.objectUrl = fileUrl;
attachment.available = true;
this.diskReceiveAssemblies.delete(assemblyKey);
this.runtimeStore.touch();

View File

@@ -0,0 +1,20 @@
import {
isBlobObjectUrl,
isFileProtocolObjectUrl,
needsBlobObjectUrlForInlineDisplay
} from './attachment-display-url.rules';
describe('attachment display url rules', () => {
it('detects blob and file protocol urls', () => {
expect(isBlobObjectUrl('blob:http://localhost/abc')).toBe(true);
expect(isBlobObjectUrl('file:///tmp/video.mp4')).toBe(false);
expect(isFileProtocolObjectUrl('file:///tmp/video.mp4')).toBe(true);
expect(isFileProtocolObjectUrl('blob:http://localhost/abc')).toBe(false);
});
it('requires blob urls for inline display when missing or file protocol', () => {
expect(needsBlobObjectUrlForInlineDisplay(undefined)).toBe(true);
expect(needsBlobObjectUrlForInlineDisplay('file:///appdata/image.png')).toBe(true);
expect(needsBlobObjectUrlForInlineDisplay('blob:http://localhost/abc')).toBe(false);
});
});

View File

@@ -0,0 +1,11 @@
export function isBlobObjectUrl(url: string): boolean {
return url.startsWith('blob:');
}
export function isFileProtocolObjectUrl(url: string): boolean {
return url.startsWith('file:');
}
export function needsBlobObjectUrlForInlineDisplay(objectUrl?: string): boolean {
return !objectUrl || isFileProtocolObjectUrl(objectUrl);
}