import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios, { type AxiosInstance } from 'axios'; import * as fs from 'fs'; import * as path from 'path'; import FormData = require('form-data'); @Injectable() export class PaperlessService { private readonly logger = new Logger(PaperlessService.name); private readonly client: AxiosInstance; constructor(private readonly configService: ConfigService) { const baseURL = this.configService.get('PAPERLESS_URL', 'http://localhost:8000'); const token = this.configService.get('PAPERLESS_TOKEN', ''); this.client = axios.create({ baseURL: `${baseURL}/api`, headers: { Authorization: `Token ${token}`, }, timeout: 30000, }); } async uploadDocument( filePath: string, options?: { title?: string; filename?: string; created?: string; documentType?: number; correspondent?: number; storagePath?: number; tags?: number[]; owner?: number; archiveSerialNumber?: number; customFields?: Record; }, ): Promise { const form = new FormData(); const uploadFilename = options?.filename ? `${options.filename}.pdf` : path.basename(filePath); form.append('document', fs.createReadStream(filePath), { filename: uploadFilename, contentType: 'application/pdf', }); if (options?.title) form.append('title', options.title); if (options?.created) form.append('created', options.created); if (options?.documentType) form.append('document_type', String(options.documentType)); if (options?.correspondent) form.append('correspondent', String(options.correspondent)); if (options?.storagePath) form.append('storage_path', String(options.storagePath)); if (options?.owner !== undefined && options.owner !== null) { form.append('owner', String(options.owner)); } if (options?.tags) { options.tags.forEach((tag) => form.append('tags', String(tag))); } if (options?.archiveSerialNumber !== undefined && !Number.isNaN(options.archiveSerialNumber)) { form.append('archive_serial_number', String(options.archiveSerialNumber)); } if (options?.customFields && Object.keys(options.customFields).length > 0) { form.append('custom_fields', JSON.stringify(options.customFields)); } const response = await this.client.post('/documents/post_document/', form, { headers: form.getHeaders(), timeout: 120000, }); this.logger.log(`Dokument hochgeladen: ${response.data}`); return response.data; } async getUsers(): Promise { const response = await this.client.get('/users/'); return response.data?.results ?? []; } async getDocuments(params?: Record): Promise { const response = await this.client.get('/documents/', { params }); return response.data; } async getDocument(id: number): Promise { const response = await this.client.get(`/documents/${id}/`); return response.data; } async getInboxDocuments(): Promise { // API pagination to get large amount of inbox documents (assuming max 9999 like C# app) const response = await this.client.get('/documents/', { params: { page: 1, page_size: 9999, ordering: '-added', truncate_content: true, tags__id__all: 1 } }); return response.data.results; } async getManuellDocuments(): Promise { const errorTag = this.configService.get('MANUELL_BEARBEITEN_TAG', 6); const response = await this.client.get('/documents/', { params: { page: 1, page_size: 9999, ordering: '-added', truncate_content: true, tags__id__all: errorTag } }); return response.data.results; } async updateDocument(id: number, data: Record): Promise { try { const response = await this.client.patch(`/documents/${id}/`, data); return response.data; } catch (err: any) { const body = err?.response?.data; if (body) { this.logger.error(`Paperless updateDocument(${id}) Fehlerdetails: ${JSON.stringify(body)}`); } throw err; } } async getDocumentTypes(): Promise { const response = await this.client.get('/document_types/', { params: { page_size: 9999 } }); return response.data.results; } async getTags(): Promise { const response = await this.client.get('/tags/', { params: { page_size: 9999 } }); return response.data.results; } async getCorrespondents(params?: Record): Promise { const response = await this.client.get('/correspondents/', { params }); return response.data; } async getCorrespondent(id: number): Promise { const response = await this.client.get(`/correspondents/${id}/`); return response.data; } async getCustomFields(): Promise { const response = await this.client.get('/custom_fields/', { params: { page_size: 9999 } }); return response.data.results; } async getTask(taskId: string): Promise { const response = await this.client.get('/tasks/', { params: { task_id: taskId }, }); return response.data; } async getCorrespondentByName(name: string): Promise { const response = await this.client.get('/correspondents/', { params: { name__icontains: name }, }); return response.data.results.find((c: any) => c.name === name); } async addCorrespondent(data: any): Promise { const response = await this.client.post('/correspondents/', data); return response.data; } async downloadDocument(id: number, type: 'original' | 'archive' = 'archive'): Promise { const endpoint = type === 'original' ? `/documents/${id}/download/` : `/documents/${id}/download/`; const response = await this.client.get(endpoint, { responseType: 'arraybuffer', params: type === 'original' ? { original: true } : {}, timeout: 120000, }); return Buffer.from(response.data); } async getDocumentPreviewStream(id: number): Promise { const response = await this.client.get(`/documents/${id}/thumb/`, { responseType: 'stream', timeout: 30000, }); return response.data; } async getDocumentPdfStream(id: number, type: 'original' | 'archive' = 'archive'): Promise { const endpoint = type === 'original' ? `/documents/${id}/download/` : `/documents/${id}/download/`; const response = await this.client.get(endpoint, { responseType: 'stream', params: type === 'original' ? { original: true } : {}, timeout: 120000, }); return response.data; } async getDocumentMetadata(id: number): Promise { const response = await this.client.get(`/documents/${id}/metadata/`); return response.data; } async addNote(id: number, note: string): Promise { const response = await this.client.post(`/documents/${id}/notes/`, { note }); return response.data; } async checksumExists(checksum: string): Promise { const response = await this.client.get('/documents/', { params: { checksum__iexact: checksum }, }); return response.data.count > 0; } async getDocumentIdByChecksum(checksum: string): Promise { const response = await this.client.get('/documents/', { params: { checksum__iexact: checksum }, }); if (response.data.count > 0 && response.data.results?.length > 0) { return response.data.results[0].id as number; } return null; } /** * Prüft, ob eine ASN bereits vergeben ist und wirft einen Fehler, falls ja. */ async validateAsnNotExists(interneBelegnummer: string): Promise { if (!interneBelegnummer) return; // Logic like in PaperlessTaskProcessorService const asnNum = parseInt(interneBelegnummer.replace(/-/g, ''), 10); if (isNaN(asnNum)) return; const existingDocId = await this.findDocumentIdByAsn(asnNum); if (existingDocId) { throw new HttpException( `Die ASN ${asnNum} (aus Belegnummer ${interneBelegnummer}) existiert bereits in Paperless (Dokument ID: ${existingDocId}).`, HttpStatus.CONFLICT, ); } } /** * Liefert die Paperless-Doc-ID des passenden Dokuments oder null. */ async findDocumentIdByAsn(asn: number): Promise { if (!Number.isFinite(asn)) return null; const response = await this.client.get('/documents/', { params: { archive_serial_number: asn, page_size: 5 }, }); if ((response.data.count ?? 0) === 0) return null; const match = (response.data.results as any[] ?? []).find( (doc: any) => Number(doc.archive_serial_number) === asn, ); return match ? Number(match.id) : null; } /** * Liefert die Paperless-Doc-ID des passenden Dokuments oder null. * Paperless kann den custom_fields-Filter ignorieren — daher manuell verifizieren. */ async findDocumentIdByCustomField(fieldId: number, value: string): Promise { const response = await this.client.get('/documents/', { params: { [`custom_fields__${fieldId}__value__iexact`]: value, page_size: 25, truncate_content: true, }, }); if ((response.data.count ?? 0) === 0) return null; const valueLower = value.toLowerCase(); const match = (response.data.results as any[] ?? []).find((doc: any) => (Array.isArray(doc.custom_fields) ? doc.custom_fields as any[] : []).some( (cf: any) => cf.field === fieldId && String(cf.value ?? '').toLowerCase() === valueLower, ), ); return match ? Number(match.id) : null; } }