diff --git a/server/src/repositories/metadata.repository.spec.ts b/server/src/repositories/metadata.repository.spec.ts new file mode 100644 index 0000000000..f4335d94f1 --- /dev/null +++ b/server/src/repositories/metadata.repository.spec.ts @@ -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', + ); + }); +}); diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 1d504f6c71..946a55bc33 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -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; } } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 57c029961e..6eace707c8 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -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;