import { Injectable, Logger } from "@nestjs/common"; import { Cron } from "@nestjs/schedule"; import { PrismaService } from 'src/prisma/prisma.service'; import * as path from 'node:path'; import { promises as fsp } from 'node:fs'; import { resolveAttachmentsRoot } from "src/config/attachment.config"; @Injectable() export class GarbargeCollectorService { private readonly logger = new Logger(GarbargeCollectorService.name); //.env refs private readonly batch_size = Number(process.env.GC_BATCH_SIZE || 500); private readonly cron_expression = process.env.GC_CRON || '15 4 * * *'; // everyday at 04:15 AM //fetchs root of storage private readonly root = resolveAttachmentsRoot(); constructor(private readonly prisma: PrismaService) { } //planif for the Cronjob @Cron(function (this: GarbargeCollectorService) { return this.cron_expression; } as any) async runScheduled() { await this.collect(); } //Manage Garbage collecting by batch of elements until a batch != full async collect() { let total = 0, round = 0; //infinit loop (;;) with break for (; ;) { round++; const num = await this.collectBatch(); total += num; this.logger.log(`Garbage Collector round #${round} removed ${num}`); if (num < this.batch_size) break; //breaks if not a full batch } this.logger.log(`Garbage Collecting done: total removed ${total}`); return { removed: total }; } //Manage a single lot of orphan blobs private async collectBatch(): Promise { const blobs = await this.prisma.blobs.findMany({ where: { refcount: { lte: 0 } }, select: { sha256: true, storage_path: true }, take: this.batch_size, }); if (blobs.length === 0) return 0; // delete original file and all its variants in the same file await Promise.all( blobs.map(async (blob) => { const absolute_path = path.join(this.root, blob.storage_path); await this.deleteFileIfExists(absolute_path); //tries to delete original file if found const dir = path.dirname(absolute_path); const base = path.basename(absolute_path); try { const entries = await fsp.readdir(dir, { withFileTypes: true }); const targets = entries.filter(entry => entry.isFile() && entry.name.startsWith(base + '.')) .map(entry => path.join(dir, entry.name)); //deletes all variants await Promise.all(targets.map(target => this.deleteFileIfExists(target))); } catch { } }) ); //deletes blobs lignes if file is deleted const hashes = blobs.map(blob => blob.sha256); await this.prisma.blobs.deleteMany({ where: { sha256: { in: hashes } } }); return blobs.length; } //helper: deletes path if exists and ignore errors private async deleteFileIfExists(path: string) { try { await fsp.unlink(path); } catch { } } }