Initial commit with Email Import Wizard and Task Processor updates
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user