fix: Bug - Files lose host on reload

Persist large uploads under app data on publish and restore, and re-announce hosted attachments after reload so peers can download again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 22:04:41 +02:00
co-authored by Cursor
parent 95259e8943
commit f0d79aa627
7 changed files with 210 additions and 9 deletions
@@ -12,6 +12,7 @@ import {
signal
} from '@angular/core';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
@@ -51,6 +52,7 @@ describe('AttachmentPersistenceService', () => {
savedPath: '/appdata/photo.png'
}
])),
getAttachmentsForMessage: vi.fn(() => Promise.resolve([])),
getMessageById: vi.fn(() => Promise.resolve(null)),
saveAttachment: vi.fn(() => Promise.resolve()),
deleteAttachmentsForMessage: vi.fn(() => Promise.resolve())
@@ -64,6 +66,9 @@ describe('AttachmentPersistenceService', () => {
getFileSize: vi.fn(() => Promise.resolve(3)),
getFileUrl: vi.fn(() => Promise.resolve(null)),
canReadFileChunks: vi.fn(() => true),
canCopyFiles: vi.fn(() => true),
createWritableFile: vi.fn(async () => '/appdata/server/room/files/setup.exe'),
copyFile: vi.fn(async () => true),
providesInlineObjectUrl: vi.fn(() => false)
};
});
@@ -75,7 +80,7 @@ describe('AttachmentPersistenceService', () => {
AttachmentRuntimeStore,
{ provide: DatabaseService, useValue: database },
{ provide: AttachmentStorageService, useValue: attachmentStorage },
{ provide: Store, useValue: { select: () => ({ pipe: () => ({ subscribe: () => {} }) }) } }
{ provide: Store, useValue: { select: () => of('room-1') } }
]
});
@@ -169,4 +174,36 @@ describe('AttachmentPersistenceService', () => {
expect(attachmentStorage.readFile).not.toHaveBeenCalled();
expect(attachmentStorage.readFileChunk).not.toHaveBeenCalled();
});
it('copies an external upload path into app data and hydrates generic files without loading a blob', async () => {
attachmentStorage.resolveExistingPath
.mockResolvedValueOnce(null)
.mockResolvedValue('/appdata/server/room/files/setup.exe');
const service = createService();
const attachment = {
id: 'att-setup',
messageId: 'msg-1',
filename: 'setup.exe',
size: 628 * 1024 * 1024,
mime: 'application/octet-stream',
isImage: false,
filePath: '/home/ludde/Downloads/setup.exe',
available: false
};
await expect(service.ensurePersistedUploadHost(attachment)).resolves.toBe(true);
expect(attachment.savedPath).toBe('/appdata/server/room/files/setup.exe');
expect(attachment.available).toBe(true);
expect(attachment.objectUrl).toBeUndefined();
expect(attachmentStorage.copyFile).toHaveBeenCalledWith(
'/home/ludde/Downloads/setup.exe',
'/appdata/server/room/files/setup.exe'
);
expect(attachmentStorage.readFile).not.toHaveBeenCalled();
expect(attachmentStorage.readFileChunk).not.toHaveBeenCalled();
expect(database.saveAttachment).toHaveBeenCalled();
});
});