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