fix(server): preserve metadata when sidecar writes fail

This commit is contained in:
Anthony Martin 2026-08-03 21:15:53 -04:00
parent db2033a4b0
commit 3e05fff27b
3 changed files with 39 additions and 0 deletions

View file

@ -0,0 +1,19 @@
import { ExifTool } from 'exiftool-vendored';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { automock } from 'test/utils';
describe(MetadataRepository.name, () => {
it('should propagate sidecar write errors', async () => {
const error = new Error('read-only file system');
const logger = automock(LoggingRepository, { strict: false });
const sut = new MetadataRepository(logger);
const write = vitest.fn().mockRejectedValue(error);
sut['exiftool'] = { write } as unknown as ExifTool;
await expect(sut.writeTags('/read-only/asset.jpg.xmp', { Description: 'description' })).rejects.toBe(error);
expect(logger.warn).toHaveBeenCalledWith(
'Error writing exif data (/read-only/asset.jpg.xmp): Error: read-only file system',
);
});
});

View file

@ -132,6 +132,7 @@ export class MetadataRepository {
await this.exiftool.write(path, tagsToWrite);
} catch (error) {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
throw error;
}
}
}

View file

@ -2019,6 +2019,25 @@ describe(MetadataService.name, () => {
]);
});
it('should retain locked properties when writing tags fails', async () => {
const asset = AssetFactory.from()
.file({ type: AssetFileType.Sidecar })
.exif({ description: 'database description' })
.build();
const error = new Error('read-only file system');
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['description']);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
mocks.metadata.writeTags.mockRejectedValue(error);
await expect(sut.handleSidecarWrite({ id: asset.id })).rejects.toBe(error);
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, {
Description: 'database description',
ImageDescription: 'database description',
});
expect(mocks.asset.unlockProperties).not.toHaveBeenCalled();
});
it('should write rating', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
asset.exifInfo.rating = 4;