Add UploadCleanupService for cron purge and delete uploads.

Move token delete and storage purge off UploadService; enforce expiry and max views on GET without purging until the scheduled cleanup job runs.
This commit is contained in:
2026-10-27 21:07:01 +01:00
parent 00ef845277
commit 29407814cc
7 changed files with 286 additions and 121 deletions
@@ -0,0 +1,64 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Cron, CronExpression } from '@nestjs/schedule';
import { DataSource, Repository } from 'typeorm';
import { hashDeleteToken } from '../../../utils/hashDeleteToken';
import { isErrorEnoent } from '../../../utils/isErrorEnoent';
import { StorageService } from '../../storage/services/StorageService';
import { Upload } from '../entities/Upload';
@Injectable()
export class UploadCleanupService {
constructor(
@InjectDataSource()
private readonly dataSource: DataSource,
private readonly storageService: StorageService,
@InjectRepository(Upload)
private readonly uploadRepository: Repository<Upload>
) {}
async deleteUpload(id: string, deleteToken: string): Promise<void> {
await this.dataSource.transaction(async manager => {
const uploadRepository = manager.getRepository(Upload);
const upload = await uploadRepository.findOne({
where: { id, deleteTokenHash: hashDeleteToken(deleteToken) }
});
if (!upload) {
throw new NotFoundException();
}
await this.purgeUpload(upload, uploadRepository);
});
}
@Cron(CronExpression.EVERY_MINUTE)
async purgeUploads(): Promise<void> {
const purgeBatchSize = 100;
const now = new Date();
const uploads = await this.uploadRepository
.createQueryBuilder('upload')
.where('upload.expiresAt IS NOT NULL AND upload.expiresAt < :now', { now })
.orWhere('upload.maxViews IS NOT NULL AND upload.viewCount >= upload.maxViews')
.take(purgeBatchSize)
.getMany();
for (const upload of uploads) {
await this.purgeUpload(upload, this.uploadRepository);
}
}
async purgeUpload(upload: Upload, uploadRepository: Repository<Upload>): Promise<void> {
try {
await this.storageService.unlink(upload.storagePath);
} catch (error: unknown) {
if (!isErrorEnoent(error)) {
throw error;
}
}
await uploadRepository.remove(upload);
}
}