Initial commit with Email Import Wizard and Task Processor updates

This commit is contained in:
2026-05-04 08:02:11 +02:00
commit effdc5d59f
170 changed files with 67739 additions and 0 deletions
@@ -0,0 +1,117 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as path from 'path';
import * as fs from 'fs/promises';
import sharp from 'sharp';
const THUMBNAIL_WIDTH = 180;
@Injectable()
export class PageCacheService {
private readonly logger = new Logger(PageCacheService.name);
private readonly inboxRoot: string;
constructor(configService: ConfigService) {
this.inboxRoot = configService.get<string>('INBOX_DATA_DIR', '/mnt/data/inbox');
}
documentDir(documentId: string): string {
return path.join(this.inboxRoot, documentId);
}
documentPdfPath(documentId: string): string {
return path.join(this.documentDir(documentId), 'document.pdf');
}
previewPath(documentId: string, page: number): string {
return path.join(this.documentDir(documentId), `page-${page}.preview.png`);
}
thumbnailPath(documentId: string, page: number): string {
return path.join(this.documentDir(documentId), `page-${page}.thumb.png`);
}
/**
* Übernimmt die bereits gerenderten 200-dpi-PNGs als preview.png und
* erzeugt parallel eine kleinere thumb.png pro Seite.
*/
async generate(documentId: string, renderedImages: string[]): Promise<void> {
const dir = this.documentDir(documentId);
await fs.mkdir(dir, { recursive: true });
for (let i = 0; i < renderedImages.length; i++) {
const page = i + 1;
const src = renderedImages[i];
const previewDest = this.previewPath(documentId, page);
const thumbDest = this.thumbnailPath(documentId, page);
try {
await fs.copyFile(src, previewDest);
await sharp(src).resize({ width: THUMBNAIL_WIDTH }).png().toFile(thumbDest);
} catch (err: any) {
this.logger.warn(
`Seiten-Cache fehlgeschlagen (${documentId} Seite ${page}): ${err.message}`,
);
}
}
}
/**
* Entfernt alle page-*.png-Dateien eines Dokuments (Original-PDF bleibt).
*/
async clear(documentId: string): Promise<void> {
const dir = this.documentDir(documentId);
let entries: string[];
try {
entries = await fs.readdir(dir);
} catch {
return;
}
for (const name of entries) {
if (!/^page-\d+\.(preview|thumb)\.png$/.test(name)) continue;
await fs.unlink(path.join(dir, name)).catch(() => undefined);
}
}
/**
* Verschiebt Page-Cache-Dateien nach dem Löschen einer Seite:
* page-N.*.png weg, page-(N+1..oldPageCount) rutschen um 1 nach vorne.
*/
async shiftAfterPageDelete(
documentId: string,
deletedPage: number,
oldPageCount: number,
): Promise<void> {
const dir = this.documentDir(documentId);
await fs
.unlink(path.join(dir, `page-${deletedPage}.thumb.png`))
.catch(() => undefined);
await fs
.unlink(path.join(dir, `page-${deletedPage}.preview.png`))
.catch(() => undefined);
for (let n = deletedPage + 1; n <= oldPageCount; n++) {
for (const variant of ['thumb', 'preview'] as const) {
const from = path.join(dir, `page-${n}.${variant}.png`);
const to = path.join(dir, `page-${n - 1}.${variant}.png`);
try {
await fs.rename(from, to);
} catch (err: any) {
this.logger.warn(
`Cache-Shift fehlgeschlagen (${documentId} Seite ${n} ${variant}): ${err.message}`,
);
}
}
}
}
async hasPreview(documentId: string, page: number): Promise<boolean> {
try {
await fs.access(this.previewPath(documentId, page));
return true;
} catch {
return false;
}
}
}