Compare commits

...

1 Commits

Author SHA1 Message Date
Daniel Dietzler ec40c14abd fix: integrity checks checksum pagination 2026-07-03 15:44:06 +02:00
4 changed files with 47 additions and 31 deletions
+2 -3
View File
@@ -130,10 +130,9 @@ from
where where
"asset"."deletedAt" is null "asset"."deletedAt" is null
and "asset"."isExternal" = false and "asset"."isExternal" = false
and "integrity_report"."createdAt" >= $2 and "asset"."createdAt" >= $2
and "integrity_report"."createdAt" <= $3
order by order by
"integrity_report"."createdAt" asc "asset"."createdAt" asc
-- IntegrityRepository.streamIntegrityReports -- IntegrityRepository.streamIntegrityReports
select select
@@ -160,8 +160,8 @@ export class IntegrityRepository {
>; >;
} }
@GenerateSql({ params: [DummyValue.DATE, DummyValue.DATE], stream: true }) @GenerateSql({ params: [DummyValue.DATE], stream: true })
streamAssetChecksums(startMarker?: Date, endMarker?: Date) { streamAssetChecksums(startMarker?: Date) {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
@@ -178,9 +178,8 @@ export class IntegrityRepository {
'integrity_report.id as reportId', 'integrity_report.id as reportId',
]) ])
.where('asset.isExternal', '=', sql.lit(false)) .where('asset.isExternal', '=', sql.lit(false))
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!)) .$if(startMarker !== undefined, (qb) => qb.where('asset.createdAt', '>=', startMarker!))
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!)) .orderBy('asset.createdAt', 'asc')
.orderBy('integrity_report.createdAt', 'asc')
.stream(); .stream();
} }
+15 -22
View File
@@ -532,8 +532,7 @@ export class IntegrityService extends BaseService {
const { count } = await this.integrityRepository.getAssetCount(); const { count } = await this.integrityRepository.getAssetCount();
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint); const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
let startMarker: Date | undefined = checkpoint?.date ? new Date(checkpoint.date) : undefined; const startMarker = checkpoint?.date ? new Date(checkpoint.date) : undefined;
let endMarker: Date | undefined;
const printStats = () => { const printStats = () => {
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2); const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
@@ -546,31 +545,25 @@ export class IntegrityService extends BaseService {
let lastCreatedAt: Date | undefined; let lastCreatedAt: Date | undefined;
finishEarly: do { this.logger.log(`Processing assets from ${startMarker?.toISOString() ?? 'beginning'}`);
this.logger.log(
`Processing assets in range [${startMarker?.toISOString() ?? 'beginning'}, ${endMarker?.toISOString() ?? 'end'}]`,
);
const assets = this.integrityRepository.streamAssetChecksums(startMarker, endMarker); const assets = this.integrityRepository.streamAssetChecksums(startMarker);
endMarker = startMarker;
startMarker = undefined;
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) { for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
await this.checkAssetChecksum(originalPath, checksum, assetId, reportId); await this.checkAssetChecksum(originalPath, checksum, assetId, reportId);
processed++; processed++;
if (processed % 100 === 0) { if (processed % 100 === 0) {
printStats(); printStats();
}
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
this.logger.log('Reached stop criteria.');
lastCreatedAt = createdAt;
break finishEarly;
}
} }
} while (endMarker);
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
this.logger.log('Reached stop criteria.');
lastCreatedAt = createdAt;
break;
}
}
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
date: lastCreatedAt?.toISOString(), date: lastCreatedAt?.toISOString(),
@@ -1,9 +1,10 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DateTime } from 'luxon';
import { createHash, randomUUID } from 'node:crypto'; import { createHash, randomUUID } from 'node:crypto';
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import { text } from 'node:stream/consumers'; import { text } from 'node:stream/consumers';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { AssetFileType, IntegrityReport, JobName, JobStatus } from 'src/enum'; import { AssetFileType, IntegrityReport, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { AssetRepository } from 'src/repositories/asset.repository'; import { AssetRepository } from 'src/repositories/asset.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository'; import { EventRepository } from 'src/repositories/event.repository';
@@ -702,6 +703,30 @@ describe(IntegrityService.name, () => {
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail), ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
).resolves.toEqual({ items: [], nextCursor: undefined }); ).resolves.toEqual({ items: [], nextCursor: undefined });
}); });
it('should continue from checkpoint', async () => {
const { sut, ctx } = setup();
const job = ctx.getMock(JobRepository);
job.queue.mockResolvedValue();
const { user } = await ctx.newUser();
await ctx.newAsset({
ownerId: user.id,
createdAt: DateTime.now().minus({ days: 1 }).toISO(),
originalPath: '/foo/bar',
});
await ctx.newAsset({ ownerId: user.id, originalPath: '/foo/baz' });
await ctx
.get(SystemMetadataRepository)
.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { date: DateTime.now().minus({ minutes: 5 }).toISO() });
await sut.handleChecksumFiles({ refreshOnly: false });
expect(ctx.getMock(StorageRepository).createPlainReadStream).not.toHaveBeenCalledWith('/foo/bar');
expect(ctx.getMock(StorageRepository).createPlainReadStream).toHaveBeenCalledWith('/foo/baz');
});
}); });
describe('handleChecksumRefresh', () => { describe('handleChecksumRefresh', () => {