298 lines
9.7 KiB
TypeScript
298 lines
9.7 KiB
TypeScript
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;
|
|
}
|
|
|
|
async getDocumentIdByChecksum(checksum: string): Promise<number | null> {
|
|
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<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;
|
|
}
|
|
}
|