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 ) {} async deleteUpload(id: string, deleteToken: string): Promise { 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 { 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): Promise { try { await this.storageService.unlink(upload.storagePath); } catch (error: unknown) { if (!isErrorEnoent(error)) { throw error; } } await uploadRepository.remove(upload); } }