Node.js Fetch() Буфер файла PDF в памяти для вложения Nodemailer

У меня есть ночной процесс, который отправляет запланированные отчеты, вызывая созданную мной конечную точку, которая возвращает оперативный отчет в формате PDF.

Проблема, с которой я столкнулся, заключалась в том, чтобы получить двоичный файл, возвращенный из конечной точки, в буфер, который Nodemailer может использовать для прикрепления отчета в формате PDF.

Я просто использую обещание, возвращаемое из fetch() и arrayBuffer(). Я НЕ использую Async Await.

Приведенный ниже фрагмент кода с использованием arrayBuffer() и Buffer.from() работает, но мне интересно, есть ли более эффективный способ обработки этого, особенно при работе с большими файлами PDF.

Я предпочитаю работать в памяти, а не записывать на диск. Я выделил много памяти в Express и пока НЕ ​​вижу проблем с памятью.

const sendPDFReport = (reportScheduleId, cb) => {

let scheduleObj;
//*** Get Report Schedule Data from MongoDB and Assign to scheduleObj

let emailAddresses = [];
//*** Push Recipient Emails into Email Array

//*** Do Work Like Build Fetch URL and Connection Properties Object ie., 

let fetchBody={};
//*** Build JSON object of POST Params and assign to fetchBody

let fetchURL = process.env.APPSERVER_URL+some_report_path;

let config = {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(fetchBody),
    responseType: 'blob'
};

let transporter = nodemailer.createTransport({
    host: 'mail.whatever.com',
    service: "Outlook365",
    secure: true,
    port: 465,
    auth: {
        user: process.env.NO_REPLY,
        pass: process.env.NO_REPLY_PW
    },
    tls: {
        ciphers: 'SSLv3',
        rejectUnauthorized: false
    }
});


   fetch(fetchURL, config)
    .then(response => {

        if (response.ok) {

        //*** PDF Binary Response Buffering for Nodemailer
            
        response.arrayBuffer()
           .then(resBufferAr => {
             const pdfBuffer = Buffer.from(resBufferAr);
             
             // **** Build Email mailOptions for Nodemailer Transporter Object ***

             let mailOptions = {
                 from: process.env.NO_REPLY,
                 to: emailAddresses.join([separator = ',']),
                 subject: 'Scheduled Report:  ' + scheduleObj.reportType + ' ' + scheduleObj.selectedReport,
                 html: '<h4> See Attached Report </h4>';
             
              //*** Nodemailer Attachment Section

                 attachments: [{
                     filename: scheduleObj.reportType + '_' + scheduleObj.selectedReport + '_' + now + '.pdf',
                     content: pdfBuffer,
                     encoding: 'base64',
                     contentType: 'application/pdf'
                 }]
              };

             transporter.sendMail(mailOptions, function (err) {
                if (err) {
                   console.log('Transporter Error:  ' + err);
                   return cb(err);
                }
                return cb(null);
             }
   })
   .catch((err) => {
            console.log('Problem Processing Alert Notification:  ' + err);
            return cb(err);
   })

    return cb(null);

}

Любые комментарии или предложения были бы замечательными.


person Brian Blackman    schedule 18.06.2021    source источник
comment
Почему бы вам НЕ использовать async/await? И почему все ваши переменные let? Конечно, большинство из них могут быть const. И ваш обратный вызов будет вызываться дважды... Один раз сразу после выполнения функции, а затем еще раз в блоках .then() или .catch(). ;)   -  person TJBlackman    schedule 22.06.2021
comment
@TJBlackman Очень полезно понимать обещания, прежде чем с головой погрузиться в асинхронность / ожидание, поскольку последнее построено поверх первого. И, основываясь на проблеме, на которую вы указали, когда OP дважды выполнял обратный вызов (кстати, хороший улов), он, безусловно, мог бы извлечь выгоду из лучшего понимания. Кроме того, некоторые API поддерживают только обратные вызовы, и вы не можете ожидать их в том виде, в каком они есть. Кроме того, лично для меня const редко имеет ценность ниже верхнего уровня, поскольку const не предотвращает мутацию объекта или затенение переменных, что, по моему мнению, является настоящей проблемой, особенно если у вас уже есть линтер.   -  person user3781737    schedule 22.06.2021


Ответы (1)


Да, есть более эффективный способ: потоки

Вкратце, потоки — это встроенные структуры данных в Node, которые позволяют вам работать с фрагментами входящих данных, а не ждать, пока все данные будут загружены в память перед их обработкой.

node-fetch поддерживает потоки в качестве полезной нагрузки ответа, а nodemailer поддерживает потоки в виде вложений, поэтому вы можете эффективно передавать тело ответа на выборку в nodemailer. Теоретически, тогда ваша машина никогда не хранит в памяти весь PDF-файл в любой момент времени, а вместо этого всегда имеет только части PDF-файла, которые он просто передает из одной боковой сети в другую по мере их поступления (хотя есть причины, почему это иногда не выдерживает).

Я не проверял это полностью, но это должно быть, по крайней мере, близко к рабочему решению, включающему потоки между node-fetch и nodemailer:

if (response.ok) {
    let readableStream = response.body;

    let mailOptions = {
        from: process.env.NO_REPLY,
        to: emailAddresses.join(','),
        subject: 'Scheduled Report:  ' + scheduleObj.reportType + ' ' + scheduleObj.selectedReport,
        html: '<h4> See Attached Report </h4>',

        attachments: [{
            filename: scheduleObj.reportType + '_' + scheduleObj.selectedReport + '_' + now + '.pdf',
            content: readableStream,
            encoding: 'base64',
            contentType: 'application/pdf'
        }]
    };

   // ...the rest of your code

ОБНОВЛЕНИЕ: хотя я и не использовал node-fetch, я проверил использование памяти при чтении 3-мегабайтного файла с диска и отправке его себе по электронной почте через nodemailer. После чтения всего файла в память и последующей передачи его в nodemailer размер резидентного набора (rss) составил ~48 МБ, а внешняя куча C++ — ~11 МБ. При предоставлении nodemailer потока чтения файла вместо этого rss составлял ~ 37 МБ, а внешняя куча - ~ 4 МБ. Так точно эффективнее.

person user3781737    schedule 18.06.2021
comment
Реквизиты для потоков в качестве полезной нагрузки/вложений и работа по захвату распределения памяти. Я обязательно проведу рефакторинг для обработки большого размера отчета в формате PDF. - person Brian Blackman; 29.06.2021
comment
@BrianBlackman рад помочь. Кроме того, я только что заметил, что в свойстве .to вашего объекта mailOptions вы написали emailAddresses.join([separator = ',']). Не уверен, что вы пришли из Python, но, вероятно, это не то, что вы думаете. Написав здесь separator, вы не указываете именованный параметр, вы фактически объявляете separator как глобальную переменную. Это по-прежнему работает, потому что выражение [separator = ','] по-прежнему возвращает [','], которое приводится к типу ',' с помощью .join(), так что оно по-прежнему оказывается .join(','), но с большими накладными расходами и случайным глобальным значением! - person user3781737; 29.06.2021