mirror of
https://github.com/immich-app/immich.git
synced 2026-06-30 10:07:09 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba3e0560ed | |||
| 42782bf842 |
@@ -399,7 +399,10 @@ export class IntegrityService extends BaseService {
|
|||||||
await this.integrityRepository.deleteByIds(outdatedReports);
|
await this.integrityRepository.deleteByIds(outdatedReports);
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingFiles = results.filter(({ exists }) => !exists);
|
const missingFiles = Object.values(
|
||||||
|
Object.fromEntries(results.filter(({ exists }) => !exists).map((file) => [file.path, file])),
|
||||||
|
);
|
||||||
|
|
||||||
if (missingFiles.length > 0) {
|
if (missingFiles.length > 0) {
|
||||||
await this.integrityRepository.create(
|
await this.integrityRepository.create(
|
||||||
missingFiles.map(({ path, assetId, fileAssetId }) => ({
|
missingFiles.map(({ path, assetId, fileAssetId }) => ({
|
||||||
|
|||||||
@@ -512,6 +512,82 @@ describe(IntegrityService.name, () => {
|
|||||||
nextCursor: undefined,
|
nextCursor: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not fail when the same path is duplicated within a batch', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { id: ownerId },
|
||||||
|
} = await ctx.newUser();
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { id: assetId1 },
|
||||||
|
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate' });
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { id: assetId2 },
|
||||||
|
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate' });
|
||||||
|
|
||||||
|
const fileAssetId1 = randomUUID();
|
||||||
|
await ctx.newAssetFile({
|
||||||
|
id: fileAssetId1,
|
||||||
|
assetId: assetId1,
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/path/to/duplicate-file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileAssetId2 = randomUUID();
|
||||||
|
await ctx.newAssetFile({
|
||||||
|
id: fileAssetId2,
|
||||||
|
assetId: assetId1,
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/path/to/duplicate-file',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { id: assetId3 },
|
||||||
|
} = await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate-cross' });
|
||||||
|
|
||||||
|
const fileAssetId3 = randomUUID();
|
||||||
|
await ctx.newAssetFile({
|
||||||
|
id: fileAssetId3,
|
||||||
|
assetId: assetId3,
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/path/to/duplicate-cross',
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockRejectedValue(new Error('ENOENT'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleMissingFiles({
|
||||||
|
items: [
|
||||||
|
{ path: '/path/to/duplicate', assetId: assetId1, fileAssetId: null, reportId: null },
|
||||||
|
{ path: '/path/to/duplicate', assetId: assetId2, fileAssetId: null, reportId: null },
|
||||||
|
{ path: '/path/to/duplicate-file', assetId: null, fileAssetId: fileAssetId1, reportId: null },
|
||||||
|
{ path: '/path/to/duplicate-file', assetId: null, fileAssetId: fileAssetId2, reportId: null },
|
||||||
|
{ path: '/path/to/duplicate-cross', assetId: assetId3, fileAssetId: null, reportId: null },
|
||||||
|
{ path: '/path/to/duplicate-cross', assetId: null, fileAssetId: fileAssetId3, reportId: null },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.get(IntegrityRepository).getIntegrityReport(
|
||||||
|
{
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
IntegrityReport.MissingFile,
|
||||||
|
),
|
||||||
|
).resolves.toEqual({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ path: '/path/to/duplicate' }),
|
||||||
|
expect.objectContaining({ path: '/path/to/duplicate-file' }),
|
||||||
|
expect.objectContaining({ path: '/path/to/duplicate-cross' }),
|
||||||
|
]),
|
||||||
|
nextCursor: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleMissingRefresh', () => {
|
describe('handleMissingRefresh', () => {
|
||||||
@@ -686,6 +762,40 @@ describe(IntegrityService.name, () => {
|
|||||||
nextCursor: undefined,
|
nextCursor: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not fail when the same path is duplicated across assets', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const job = ctx.getMock(JobRepository);
|
||||||
|
job.queue.mockResolvedValue(void 0);
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { id: ownerId },
|
||||||
|
} = await ctx.newUser();
|
||||||
|
|
||||||
|
await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate', checksum: Buffer.from('mismatch-a') });
|
||||||
|
await ctx.newAsset({ ownerId, originalPath: '/path/to/duplicate', checksum: Buffer.from('mismatch-b') });
|
||||||
|
|
||||||
|
storage.createPlainReadStream.mockImplementation(() => Readable.from('garbage data'));
|
||||||
|
|
||||||
|
await expect(sut.handleChecksumFiles({ refreshOnly: false })).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.get(IntegrityRepository).getIntegrityReport(
|
||||||
|
{
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
IntegrityReport.ChecksumFail,
|
||||||
|
),
|
||||||
|
).resolves.toEqual({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
path: '/path/to/duplicate',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
nextCursor: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleChecksumRefresh', () => {
|
describe('handleChecksumRefresh', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user