Initial commit with Email Import Wizard and Task Processor updates
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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<InboxDocument>,
|
||||
) {
|
||||
this.legacyRoot = this.configService.get<string>('SCANS_DATA_DIR', '/mnt/data/scans');
|
||||
}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<StoredQrCode[]> {
|
||||
try {
|
||||
const rows = await this.dataSource.query<LegacyScanRow[]>(
|
||||
'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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user