Initial commit with Email Import Wizard and Task Processor updates
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fs from 'fs/promises';
|
||||
import { BarcodeScannerService, type MatchedBarcode } from '../barcode/barcode-scanner.service';
|
||||
import { PageCacheService } from '../barcode/page-cache.service';
|
||||
import {
|
||||
InboxDocument,
|
||||
type InboxSource,
|
||||
} from '../database/entities/inbox-document.entity';
|
||||
|
||||
export interface InboxFile {
|
||||
id: string;
|
||||
name: string;
|
||||
source: InboxSource;
|
||||
pageCount: number;
|
||||
deletedPages: number[];
|
||||
rotations: Record<string, number>;
|
||||
barcodes: MatchedBarcode[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ResolvedDocument {
|
||||
doc: InboxDocument;
|
||||
pdfPath: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InboxService {
|
||||
private readonly logger = new Logger(InboxService.name);
|
||||
|
||||
constructor(
|
||||
private readonly barcodeScanner: BarcodeScannerService,
|
||||
private readonly pageCache: PageCacheService,
|
||||
@InjectRepository(InboxDocument)
|
||||
private readonly documentRepo: Repository<InboxDocument>,
|
||||
) {}
|
||||
|
||||
async listFiles(preferredUsername: string | null): Promise<InboxFile[]> {
|
||||
const where = preferredUsername
|
||||
? [{ Source: 'all' as InboxSource }, { Source: 'user' as InboxSource, OwnerUsername: preferredUsername }]
|
||||
: [{ Source: 'all' as InboxSource }];
|
||||
|
||||
const docs = await this.documentRepo.find({
|
||||
where,
|
||||
order: { CreatedAt: 'DESC' },
|
||||
});
|
||||
|
||||
const files: InboxFile[] = [];
|
||||
for (const doc of docs) {
|
||||
files.push({
|
||||
id: doc.Id,
|
||||
name: doc.OriginalName,
|
||||
source: doc.Source,
|
||||
pageCount: doc.PageCount,
|
||||
deletedPages: [...(doc.DeletedPages ?? [])].sort((a, b) => a - b),
|
||||
rotations: { ...(doc.Rotations ?? {}) },
|
||||
barcodes: await this.barcodeScanner.getMatched(doc),
|
||||
createdAt: doc.CreatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async resolveDocument(id: string, preferredUsername: string | null): Promise<ResolvedDocument> {
|
||||
const doc = await this.documentRepo.findOne({ where: { Id: id } });
|
||||
if (!doc) throw new NotFoundException('Dokument nicht gefunden');
|
||||
if (doc.Source === 'user' && doc.OwnerUsername !== preferredUsername) {
|
||||
throw new NotFoundException('Dokument nicht gefunden');
|
||||
}
|
||||
|
||||
const pdfPath = this.pageCache.documentPdfPath(doc.Id);
|
||||
try {
|
||||
const stat = await fs.stat(pdfPath);
|
||||
if (!stat.isFile()) throw new Error('not a file');
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Datei fehlt trotz DB-Eintrag (${doc.Id}): ${err.message}`);
|
||||
throw new NotFoundException('Dokument nicht gefunden');
|
||||
}
|
||||
|
||||
return { doc, pdfPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert eine Seite virtuell zum Löschen. Die PDF und der Page-Cache
|
||||
* bleiben unverändert; die eigentliche Anwendung passiert später bei
|
||||
* der Weiterverarbeitung.
|
||||
*/
|
||||
async deletePage(
|
||||
id: string,
|
||||
page: number,
|
||||
preferredUsername: string | null,
|
||||
): Promise<void> {
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
|
||||
if (!Number.isInteger(page) || page < 1 || page > doc.PageCount) {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
|
||||
const deleted = new Set<number>(doc.DeletedPages ?? []);
|
||||
if (deleted.has(page)) return; // schon markiert
|
||||
|
||||
const remaining = doc.PageCount - deleted.size;
|
||||
if (remaining <= 1) {
|
||||
throw new ConflictException('Mindestens eine Seite muss übrig bleiben');
|
||||
}
|
||||
|
||||
deleted.add(page);
|
||||
doc.DeletedPages = Array.from(deleted).sort((a, b) => a - b);
|
||||
await this.documentRepo.save(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt eine Seitenrotation virtuell. Wert wird auf 0/90/180/270
|
||||
* normalisiert; 0 entfernt den Eintrag.
|
||||
*/
|
||||
async setPageRotation(
|
||||
id: string,
|
||||
page: number,
|
||||
rotation: number,
|
||||
preferredUsername: string | null,
|
||||
): Promise<void> {
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
if (!Number.isInteger(page) || page < 1 || page > doc.PageCount) {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
const normalized = ((Math.round(rotation / 90) * 90) % 360 + 360) % 360;
|
||||
const next: Record<string, number> = { ...(doc.Rotations ?? {}) };
|
||||
if (normalized === 0) {
|
||||
delete next[String(page)];
|
||||
} else {
|
||||
next[String(page)] = normalized;
|
||||
}
|
||||
doc.Rotations = next;
|
||||
await this.documentRepo.save(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt alle markierten Bearbeitungen (DeletedPages, Rotations) zurück.
|
||||
*/
|
||||
async resetEdits(id: string, preferredUsername: string | null): Promise<void> {
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
let changed = false;
|
||||
if (doc.DeletedPages && doc.DeletedPages.length > 0) {
|
||||
doc.DeletedPages = [];
|
||||
changed = true;
|
||||
}
|
||||
if (doc.Rotations && Object.keys(doc.Rotations).length > 0) {
|
||||
doc.Rotations = {};
|
||||
changed = true;
|
||||
}
|
||||
if (changed) await this.documentRepo.save(doc);
|
||||
}
|
||||
|
||||
async deleteDocument(id: string, preferredUsername: string | null): Promise<void> {
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
const dir = this.pageCache.documentDir(doc.Id);
|
||||
await this.documentRepo.delete(doc.Id);
|
||||
try {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async resolvePageImage(
|
||||
id: string,
|
||||
page: number,
|
||||
variant: 'preview' | 'thumbnail',
|
||||
preferredUsername: string | null,
|
||||
): Promise<string> {
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
if (!Number.isInteger(page) || page < 1 || page > doc.PageCount) {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
const filePath =
|
||||
variant === 'preview'
|
||||
? this.pageCache.previewPath(doc.Id, page)
|
||||
: this.pageCache.thumbnailPath(doc.Id, page);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user