n8n-nodes-sotoros-gotenberg 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nodes/Gotenberg/Gotenberg.node.js +260 -156
- package/package.json +1 -1
|
@@ -72,12 +72,21 @@ class Gotenberg {
|
|
|
72
72
|
default: 'office',
|
|
73
73
|
required: true,
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
displayName: 'List Property',
|
|
77
|
+
name: 'listPropertyName',
|
|
78
|
+
type: 'string',
|
|
79
|
+
default: 'items',
|
|
80
|
+
description: 'Name of the JSON property that contains the array/list of items from the aggregation node (e.g., "items", "data"). ' +
|
|
81
|
+
'Leave empty if the aggregated item JSON itself is an array.',
|
|
82
|
+
},
|
|
75
83
|
{
|
|
76
84
|
displayName: 'Binary Property',
|
|
77
85
|
name: 'binaryPropertyName',
|
|
78
86
|
type: 'string',
|
|
79
|
-
default: '',
|
|
80
|
-
description: 'Name of the binary property that contains the file(s) to convert
|
|
87
|
+
default: 'data',
|
|
88
|
+
description: 'Name of the binary property in each item of the list that contains the file(s) to convert',
|
|
89
|
+
required: true,
|
|
81
90
|
},
|
|
82
91
|
{
|
|
83
92
|
displayName: 'Output Binary Property',
|
|
@@ -138,13 +147,44 @@ class Gotenberg {
|
|
|
138
147
|
};
|
|
139
148
|
}
|
|
140
149
|
async execute() {
|
|
150
|
+
// Работаем с одним элементом, который содержит список (результат узла агрегации)
|
|
141
151
|
const items = this.getInputData();
|
|
142
152
|
const returnData = [];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
153
|
+
if (items.length === 0) {
|
|
154
|
+
return [returnData];
|
|
155
|
+
}
|
|
156
|
+
// Берем первый элемент (результат узла агрегации)
|
|
157
|
+
const aggregatedItem = items[0];
|
|
158
|
+
let gotenbergUrl;
|
|
159
|
+
let operation;
|
|
160
|
+
let listPropertyName;
|
|
161
|
+
let binaryPropertyName;
|
|
162
|
+
let outputBinaryPropertyName;
|
|
163
|
+
let options;
|
|
164
|
+
try {
|
|
165
|
+
gotenbergUrl = this.getNodeParameter('gotenbergUrl', 0);
|
|
166
|
+
operation = this.getNodeParameter('operation', 0);
|
|
167
|
+
listPropertyName = this.getNodeParameter('listPropertyName', 0);
|
|
168
|
+
binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
|
|
169
|
+
outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', 0);
|
|
170
|
+
options = this.getNodeParameter('options', 0, {});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (this.continueOnFail()) {
|
|
174
|
+
returnData.push({
|
|
175
|
+
json: {
|
|
176
|
+
...aggregatedItem.json,
|
|
177
|
+
error: error instanceof Error ? error.message : String(error),
|
|
178
|
+
success: false,
|
|
179
|
+
},
|
|
180
|
+
binary: aggregatedItem.binary,
|
|
181
|
+
});
|
|
182
|
+
return [returnData];
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
148
188
|
// Вспомогательная функция для получения расширения файла из MIME типа
|
|
149
189
|
const getFileExtensionFromMimeType = (mimeType) => {
|
|
150
190
|
const mimeToExt = {
|
|
@@ -160,174 +200,238 @@ class Gotenberg {
|
|
|
160
200
|
};
|
|
161
201
|
return mimeToExt[mimeType] || 'bin';
|
|
162
202
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
203
|
+
try {
|
|
204
|
+
// Извлекаем список из JSON свойства агрегированного элемента
|
|
205
|
+
let listData;
|
|
206
|
+
// Если listPropertyName пустой или не указан, проверяем, является ли сам json массивом
|
|
207
|
+
if (!listPropertyName || listPropertyName.trim() === '') {
|
|
208
|
+
if (Array.isArray(aggregatedItem.json)) {
|
|
209
|
+
listData = aggregatedItem.json;
|
|
169
210
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
else {
|
|
212
|
+
// Показываем доступные свойства для помощи пользователю
|
|
213
|
+
const availableProperties = Object.keys(aggregatedItem.json || {}).join(', ');
|
|
214
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `List Property is empty, but aggregated item JSON is not an array. ` +
|
|
215
|
+
`Available properties: ${availableProperties || 'none'}. ` +
|
|
216
|
+
`Please specify the property name that contains the array of items.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
listData = aggregatedItem.json[listPropertyName];
|
|
221
|
+
if (!listData) {
|
|
222
|
+
// Показываем доступные свойства для помощи пользователю
|
|
223
|
+
const availableProperties = Object.keys(aggregatedItem.json || {}).join(', ');
|
|
224
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Property "${listPropertyName}" not found in aggregated item. ` +
|
|
225
|
+
`Available properties: ${availableProperties || 'none'}. ` +
|
|
226
|
+
`Please check the property name.`);
|
|
227
|
+
}
|
|
228
|
+
if (!Array.isArray(listData)) {
|
|
229
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Property "${listPropertyName}" is not an array. ` +
|
|
230
|
+
`Current type: ${typeof listData}. ` +
|
|
231
|
+
`Expected an array of items from aggregation node.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (listData.length === 0) {
|
|
235
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `The list ${listPropertyName ? `in property "${listPropertyName}"` : ''} is empty. No items to process.`);
|
|
236
|
+
}
|
|
237
|
+
// Создаем FormData для отправки в Gotenberg - один запрос для всех элементов списка
|
|
238
|
+
const formData = new form_data_1.default();
|
|
239
|
+
let totalFilesCount = 0;
|
|
240
|
+
// Собираем все binary данные из всех элементов списка
|
|
241
|
+
for (let listIndex = 0; listIndex < listData.length; listIndex++) {
|
|
242
|
+
const listItem = listData[listIndex];
|
|
243
|
+
if (!listItem || typeof listItem !== 'object') {
|
|
244
|
+
continue; // Пропускаем некорректные элементы
|
|
245
|
+
}
|
|
246
|
+
// Элемент списка может быть INodeExecutionData (с полями json и binary)
|
|
247
|
+
// или обычным объектом с binary данными в разных местах
|
|
248
|
+
let itemBinary;
|
|
249
|
+
// Вариант 1: элемент имеет структуру INodeExecutionData (json и binary)
|
|
250
|
+
if ('binary' in listItem && listItem.binary && typeof listItem.binary === 'object') {
|
|
251
|
+
itemBinary = listItem.binary;
|
|
252
|
+
}
|
|
253
|
+
// Вариант 2: binary данные находятся в свойстве элемента напрямую
|
|
254
|
+
else if (binaryPropertyName in listItem) {
|
|
255
|
+
const binaryProp = listItem[binaryPropertyName];
|
|
256
|
+
if (binaryProp && typeof binaryProp === 'object') {
|
|
257
|
+
// Может быть одиночным объектом или массивом
|
|
258
|
+
if (binaryProp.data) {
|
|
259
|
+
// Это одиночный binary объект
|
|
260
|
+
itemBinary = { [binaryPropertyName]: binaryProp };
|
|
202
261
|
}
|
|
203
|
-
else {
|
|
204
|
-
//
|
|
205
|
-
|
|
262
|
+
else if (Array.isArray(binaryProp)) {
|
|
263
|
+
// Это массив binary объектов
|
|
264
|
+
itemBinary = { [binaryPropertyName]: binaryProp };
|
|
206
265
|
}
|
|
207
|
-
const fileName = binaryItem.fileName ||
|
|
208
|
-
`${propName}_${i}.${getFileExtensionFromMimeType(binaryItem.mimeType || '')}`;
|
|
209
|
-
formData.append('files', dataBuffer, {
|
|
210
|
-
filename: fileName,
|
|
211
|
-
contentType: binaryItem.mimeType || 'application/octet-stream',
|
|
212
|
-
});
|
|
213
|
-
totalFilesCount++;
|
|
214
266
|
}
|
|
215
267
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
268
|
+
if (!itemBinary || Object.keys(itemBinary).length === 0) {
|
|
269
|
+
continue; // Пропускаем элементы без binary данных
|
|
218
270
|
}
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
formData.append('waitTimeout', options.waitTimeout);
|
|
232
|
-
}
|
|
233
|
-
if (options.waitDelay) {
|
|
234
|
-
formData.append('waitDelay', options.waitDelay);
|
|
271
|
+
// Получаем binary свойство
|
|
272
|
+
const binaryProperty = itemBinary[binaryPropertyName];
|
|
273
|
+
if (!binaryProperty) {
|
|
274
|
+
continue; // Пропускаем элементы без указанного binary свойства
|
|
275
|
+
}
|
|
276
|
+
// Обрабатываем как одиночное значение, так и массив
|
|
277
|
+
const binaryItems = Array.isArray(binaryProperty) ? binaryProperty : [binaryProperty];
|
|
278
|
+
// Добавляем все файлы из этого элемента списка
|
|
279
|
+
for (let i = 0; i < binaryItems.length; i++) {
|
|
280
|
+
const binaryItem = binaryItems[i];
|
|
281
|
+
if (!binaryItem || !binaryItem.data) {
|
|
282
|
+
continue; // Пропускаем файлы без данных
|
|
235
283
|
}
|
|
284
|
+
// Получаем buffer из binary данных (всегда base64 в n8n)
|
|
285
|
+
const dataBuffer = Buffer.from(binaryItem.data, 'base64');
|
|
286
|
+
const fileName = binaryItem.fileName ||
|
|
287
|
+
`${binaryPropertyName}_${listIndex}_${i}.${getFileExtensionFromMimeType(binaryItem.mimeType || '')}`;
|
|
288
|
+
formData.append('files', dataBuffer, {
|
|
289
|
+
filename: fileName,
|
|
290
|
+
contentType: binaryItem.mimeType || 'application/octet-stream',
|
|
291
|
+
});
|
|
292
|
+
totalFilesCount++;
|
|
236
293
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
break;
|
|
246
|
-
case 'markdown':
|
|
247
|
-
endpoint = '/convert/markdown';
|
|
248
|
-
break;
|
|
249
|
-
case 'url':
|
|
250
|
-
endpoint = '/convert/url';
|
|
251
|
-
// Для URL операции нужен параметр url
|
|
252
|
-
if (item.json.url) {
|
|
253
|
-
formData.append('url', item.json.url);
|
|
254
|
-
}
|
|
255
|
-
break;
|
|
256
|
-
case 'merge':
|
|
257
|
-
endpoint = '/forms/pdfengines/merge';
|
|
258
|
-
break;
|
|
259
|
-
default:
|
|
260
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
|
|
294
|
+
}
|
|
295
|
+
if (totalFilesCount === 0) {
|
|
296
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid binary files found in any items. Please make sure binary data exists.');
|
|
297
|
+
}
|
|
298
|
+
// Добавляем опции в зависимости от операции
|
|
299
|
+
if (operation === 'office' || operation === 'html' || operation === 'markdown') {
|
|
300
|
+
if (options.landscape) {
|
|
301
|
+
formData.append('landscape', 'true');
|
|
261
302
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const response = await this.helpers.httpRequest({
|
|
265
|
-
method: 'POST',
|
|
266
|
-
url,
|
|
267
|
-
body: formData,
|
|
268
|
-
returnFullResponse: true,
|
|
269
|
-
headers: formData.getHeaders(),
|
|
270
|
-
});
|
|
271
|
-
// Проверяем статус ответа
|
|
272
|
-
if (response.statusCode && response.statusCode >= 400) {
|
|
273
|
-
let errorText;
|
|
274
|
-
if (Buffer.isBuffer(response.body)) {
|
|
275
|
-
errorText = response.body.toString('utf-8');
|
|
276
|
-
}
|
|
277
|
-
else if (typeof response.body === 'string') {
|
|
278
|
-
errorText = response.body;
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
errorText = JSON.stringify(response.body);
|
|
282
|
-
}
|
|
283
|
-
throw new n8n_workflow_2.NodeApiError(this.getNode(), {
|
|
284
|
-
message: `Gotenberg API error: ${response.statusCode}`,
|
|
285
|
-
description: errorText,
|
|
286
|
-
}, { itemIndex });
|
|
303
|
+
if (options.pageRanges) {
|
|
304
|
+
formData.append('pageRanges', options.pageRanges);
|
|
287
305
|
}
|
|
288
|
-
|
|
289
|
-
|
|
306
|
+
if (options.scale !== undefined) {
|
|
307
|
+
formData.append('scale', options.scale.toString());
|
|
308
|
+
}
|
|
309
|
+
if (options.waitTimeout) {
|
|
310
|
+
formData.append('waitTimeout', options.waitTimeout);
|
|
311
|
+
}
|
|
312
|
+
if (options.waitDelay) {
|
|
313
|
+
formData.append('waitDelay', options.waitDelay);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Определяем endpoint в зависимости от операции
|
|
317
|
+
let endpoint = '';
|
|
318
|
+
switch (operation) {
|
|
319
|
+
case 'office':
|
|
320
|
+
endpoint = '/convert/office';
|
|
321
|
+
break;
|
|
322
|
+
case 'html':
|
|
323
|
+
endpoint = '/convert/html';
|
|
324
|
+
break;
|
|
325
|
+
case 'markdown':
|
|
326
|
+
endpoint = '/convert/markdown';
|
|
327
|
+
break;
|
|
328
|
+
case 'url':
|
|
329
|
+
endpoint = '/convert/url';
|
|
330
|
+
// Для URL операции нужен параметр url из первого элемента списка
|
|
331
|
+
if (listData.length > 0 && listData[0] && typeof listData[0] === 'object' && 'url' in listData[0]) {
|
|
332
|
+
formData.append('url', listData[0].url);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
case 'merge':
|
|
336
|
+
endpoint = '/forms/pdfengines/merge';
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
340
|
+
}
|
|
341
|
+
// Отправляем запрос в Gotenberg
|
|
342
|
+
const url = `${gotenbergUrl.replace(/\/$/, '')}${endpoint}`;
|
|
343
|
+
const response = await this.helpers.httpRequest({
|
|
344
|
+
method: 'POST',
|
|
345
|
+
url,
|
|
346
|
+
body: formData,
|
|
347
|
+
returnFullResponse: true,
|
|
348
|
+
headers: formData.getHeaders(),
|
|
349
|
+
});
|
|
350
|
+
// Проверяем статус ответа
|
|
351
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
352
|
+
let errorText;
|
|
290
353
|
if (Buffer.isBuffer(response.body)) {
|
|
291
|
-
|
|
354
|
+
errorText = response.body.toString('utf-8');
|
|
292
355
|
}
|
|
293
356
|
else if (typeof response.body === 'string') {
|
|
294
|
-
|
|
357
|
+
errorText = response.body;
|
|
295
358
|
}
|
|
296
359
|
else {
|
|
297
|
-
|
|
298
|
-
const bodyArray = new Uint8Array(response.body);
|
|
299
|
-
pdfBuffer = Buffer.from(bodyArray);
|
|
360
|
+
errorText = JSON.stringify(response.body);
|
|
300
361
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
362
|
+
throw new n8n_workflow_2.NodeApiError(this.getNode(), {
|
|
363
|
+
message: `Gotenberg API error: ${response.statusCode}`,
|
|
364
|
+
description: errorText,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
// Получаем PDF buffer - Gotenberg возвращает PDF как binary данные
|
|
368
|
+
let pdfBuffer;
|
|
369
|
+
if (Buffer.isBuffer(response.body)) {
|
|
370
|
+
// Если это уже Buffer - используем напрямую
|
|
371
|
+
pdfBuffer = response.body;
|
|
372
|
+
}
|
|
373
|
+
else if (response.body instanceof ArrayBuffer) {
|
|
374
|
+
// Если это ArrayBuffer - конвертируем в Buffer
|
|
375
|
+
pdfBuffer = Buffer.from(response.body);
|
|
376
|
+
}
|
|
377
|
+
else if (typeof response.body === 'string') {
|
|
378
|
+
// Если это строка - это может быть base64 или raw binary
|
|
379
|
+
// Попробуем сначала как base64, если не получится - как raw
|
|
380
|
+
try {
|
|
381
|
+
pdfBuffer = Buffer.from(response.body, 'base64');
|
|
382
|
+
// Проверяем, что это действительно валидный PDF (начинается с %PDF)
|
|
383
|
+
if (pdfBuffer.length > 4 && pdfBuffer.toString('ascii', 0, 4) !== '%PDF') {
|
|
384
|
+
// Если не PDF, попробуем как raw binary
|
|
385
|
+
pdfBuffer = Buffer.from(response.body, 'binary');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Если не получилось декодировать как base64, используем как raw binary
|
|
390
|
+
pdfBuffer = Buffer.from(response.body, 'binary');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// Если это другой тип - конвертируем через Uint8Array
|
|
395
|
+
const bodyArray = new Uint8Array(response.body);
|
|
396
|
+
pdfBuffer = Buffer.from(bodyArray);
|
|
397
|
+
}
|
|
398
|
+
// Проверяем, что получили валидный PDF
|
|
399
|
+
if (pdfBuffer.length < 4 || pdfBuffer.toString('ascii', 0, 4) !== '%PDF') {
|
|
400
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid PDF received from Gotenberg. The response does not appear to be a valid PDF file.');
|
|
401
|
+
}
|
|
402
|
+
// Создаем один выходной элемент с результатом
|
|
403
|
+
const newItem = {
|
|
404
|
+
json: {
|
|
405
|
+
...aggregatedItem.json,
|
|
406
|
+
success: true,
|
|
407
|
+
operation,
|
|
408
|
+
fileCount: totalFilesCount,
|
|
409
|
+
processedItems: listData.length,
|
|
410
|
+
},
|
|
411
|
+
binary: {
|
|
412
|
+
...aggregatedItem.binary,
|
|
413
|
+
[outputBinaryPropertyName]: {
|
|
414
|
+
data: pdfBuffer.toString('base64'),
|
|
415
|
+
mimeType: 'application/pdf',
|
|
416
|
+
fileName: 'converted.pdf',
|
|
308
417
|
},
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
returnData.push(newItem);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
if (this.continueOnFail()) {
|
|
424
|
+
// Если continueOnFail включен, возвращаем элемент с ошибкой
|
|
425
|
+
returnData.push({
|
|
426
|
+
json: {
|
|
427
|
+
...aggregatedItem.json,
|
|
428
|
+
error: error instanceof Error ? error.message : String(error),
|
|
429
|
+
success: false,
|
|
316
430
|
},
|
|
317
|
-
|
|
318
|
-
|
|
431
|
+
binary: aggregatedItem.binary,
|
|
432
|
+
});
|
|
319
433
|
}
|
|
320
|
-
|
|
321
|
-
if (this.continueOnFail()) {
|
|
322
|
-
returnData.push({
|
|
323
|
-
json: {
|
|
324
|
-
error: error instanceof Error ? error.message : String(error),
|
|
325
|
-
success: false,
|
|
326
|
-
},
|
|
327
|
-
binary: items[itemIndex].binary,
|
|
328
|
-
});
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
434
|
+
else {
|
|
331
435
|
throw error;
|
|
332
436
|
}
|
|
333
437
|
}
|