import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { randomUUID } from 'crypto'; import * as path from 'path'; import * as fs from 'fs/promises'; import { InboxDocument, type InboxSource, type StoredQrCode, } from '../database/entities/inbox-document.entity'; import { PageCacheService } from '../barcode/page-cache.service'; interface LegacyScanRow { QrCodes: string | StoredQrCode[]; } @Injectable() export class InboxMigrationService implements OnApplicationBootstrap { private readonly logger = new Logger(InboxMigrationService.name); private readonly legacyRoot: string; constructor( private readonly configService: ConfigService, private readonly pageCache: PageCacheService, private readonly dataSource: DataSource, @InjectRepository(InboxDocument) private readonly documentRepo: Repository, ) { this.legacyRoot = this.configService.get( 'SCANS_DATA_DIR', '/mnt/data/scans', ); } async onApplicationBootstrap(): Promise { let subdirs: string[]; try { const entries = await fs.readdir(this.legacyRoot, { withFileTypes: true, }); subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); } catch (err: any) { if (err.code !== 'ENOENT') { this.logger.warn( `Migration: ${this.legacyRoot} nicht lesbar: ${err.message}`, ); } return; } let migrated = 0; for (const subdir of subdirs) { const dir = path.join(this.legacyRoot, subdir); let files: string[]; try { files = await fs.readdir(dir); } catch (err: any) { this.logger.warn(`Migration: ${dir} nicht lesbar: ${err.message}`); continue; } for (const name of files) { if (path.extname(name).toLowerCase() !== '.pdf') continue; const src = path.join(dir, name); try { await this.migrateFile(src, subdir, name); migrated += 1; } catch (err: any) { this.logger.error( `Migration fehlgeschlagen (${src}): ${err.message}`, ); } } await fs.rmdir(dir).catch(() => undefined); } if (migrated > 0) { this.logger.log(`Migration: ${migrated} Datei(en) übernommen`); } else { this.logger.log('Migration: keine Altdaten gefunden'); } } private async migrateFile( src: string, subdir: string, name: string, ): Promise { const id = randomUUID(); const source: InboxSource = subdir === 'all' ? 'all' : 'user'; const owner = source === 'all' ? null : subdir; const targetDir = this.pageCache.documentDir(id); const targetPdf = this.pageCache.documentPdfPath(id); await fs.mkdir(targetDir, { recursive: true }); await this.move(src, targetPdf); const qrCodes = await this.loadLegacyQrCodes(src); const doc = this.documentRepo.create({ Id: id, OriginalName: name, Source: source, OwnerUsername: owner, PageCount: 0, QrCodes: qrCodes, }); await this.documentRepo.save(doc); } private async move(src: string, dest: string): Promise { try { await fs.rename(src, dest); return; } catch (err: any) { if (err.code !== 'EXDEV') throw err; } await fs.copyFile(src, dest); try { await fs.unlink(src); } catch (err) { await fs.unlink(dest).catch(() => undefined); throw err; } } private async loadLegacyQrCodes( oldFilePath: string, ): Promise { try { const rows = await this.dataSource.query( 'SELECT QrCodes FROM barcode_scans WHERE FilePath = ? LIMIT 1', [oldFilePath], ); if (!rows || rows.length === 0) return []; const raw = rows[0].QrCodes; if (typeof raw === 'string') { try { return JSON.parse(raw) as StoredQrCode[]; } catch { return []; } } return Array.isArray(raw) ? raw : []; } catch { // Tabelle fehlt oder anderes DB-Problem: Backfill scannt später. return []; } } }