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,287 @@
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<string>('PAPERLESS_URL', 'http://localhost:8000');
const token = this.configService.get<string>('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<string | number, string>;
},
): Promise<string> {
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<any[]> {
const response = await this.client.get('/users/');
return response.data?.results ?? [];
}
async getDocuments(params?: Record<string, any>): Promise<any> {
const response = await this.client.get('/documents/', { params });
return response.data;
}
async getDocument(id: number): Promise<any> {
const response = await this.client.get(`/documents/${id}/`);
return response.data;
}
async getInboxDocuments(): Promise<any[]> {
// 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<any[]> {
const errorTag = this.configService.get<number>('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<string, any>): Promise<any> {
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<any[]> {
const response = await this.client.get('/document_types/', {
params: { page_size: 9999 }
});
return response.data.results;
}
async getTags(): Promise<any[]> {
const response = await this.client.get('/tags/', {
params: { page_size: 9999 }
});
return response.data.results;
}
async getCorrespondents(params?: Record<string, any>): Promise<any> {
const response = await this.client.get('/correspondents/', { params });
return response.data;
}
async getCorrespondent(id: number): Promise<any> {
const response = await this.client.get(`/correspondents/${id}/`);
return response.data;
}
async getCustomFields(): Promise<any[]> {
const response = await this.client.get('/custom_fields/', {
params: { page_size: 9999 }
});
return response.data.results;
}
async getTask(taskId: string): Promise<any> {
const response = await this.client.get('/tasks/', {
params: { task_id: taskId },
});
return response.data;
}
async getCorrespondentByName(name: string): Promise<any> {
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<any> {
const response = await this.client.post('/correspondents/', data);
return response.data;
}
async downloadDocument(id: number, type: 'original' | 'archive' = 'archive'): Promise<Buffer> {
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<any> {
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<any> {
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<any> {
const response = await this.client.get(`/documents/${id}/metadata/`);
return response.data;
}
async addNote(id: number, note: string): Promise<any> {
const response = await this.client.post(`/documents/${id}/notes/`, { note });
return response.data;
}
async checksumExists(checksum: string): Promise<boolean> {
const response = await this.client.get('/documents/', {
params: { checksum__iexact: checksum },
});
return response.data.count > 0;
}
/**
* Prüft, ob eine ASN bereits vergeben ist und wirft einen Fehler, falls ja.
*/
async validateAsnNotExists(interneBelegnummer: string): Promise<void> {
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<number | null> {
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<number | null> {
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;
}
}