为什么我的 PDF 不能间歇性地保存在我的 Node 函数中?

Why is my PDF not saving intermittently in my Node function?

首先,让我说一下,我对后端应用程序和 Nodejs 非常陌生。我主要是做移动开发,所以我的语言知识有限。

我在 Firebase Functions 中有一个端点,它根据 Firestore 中的数据和 Storage 中的图像构建和保存 PDF。 PDF 构建工作正常,我没有收到任何错误。但是,保存 PDF 的最后一段代码执行不一致。我有永远不会被触发的日志语句,但有时会保存 PDF。我认为这与我使用异步方法有关,但我不确定。这段代码有什么明显的错误吗?这是我正在使用的全部代码。

const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
const Printer = require('pdfmake');
const fonts = require('pdfmake/build/vfs_fonts.js');
const {Storage} = require('@google-cloud/storage');
const url = require('url');
const https = require('https')
const os = require('os');
const fs = require('fs');
const path = require('path');


const storage = new Storage();

const bucketName = '<BUCKET NAME REMOVED FOR THIS QUESTION>'

admin.initializeApp({
  serviceAccountId: 'firebase-adminsdk-ofnne@perimeter1-d551f.iam.gserviceaccount.com',
  storageBucket: bucketName
});

const bucket = admin.storage().bucket()
const firestore = admin.firestore()

  const fontDescriptors = {
    Roboto: {
      normal: Buffer.from(fonts.pdfMake.vfs['Roboto-Regular.ttf'], 'base64'),
      bold: Buffer.from(fonts.pdfMake.vfs['Roboto-Medium.ttf'], 'base64'),
      italics: Buffer.from(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
      bolditalics: Buffer.from(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
    }
  };

  function buildLog(data) {
    const filePath = data.imageReference;
    const fileName = path.basename(filePath);
    const tempFilePath = path.join(os.tmpdir(), fileName);
    return {
      stack: [
      {
        image: tempFilePath,
        fit: [130, 220]
      },
      {
        text: data["logEventType"],
        style: 'small'
      },
      {
        text: data["date"],
        style: 'small'
      }
      ],
      unbreakable: true,
      width: 130
    }
  }

  function buildLogsBody(data) {
    var body = [];

    var row = []
    var count = 0

    data.forEach(function(logData) {
      const log = buildLog(logData)
      row.push(log)
      count = count + 1
      if (count == 4) {
        body.push([{columns: row, columnGap: 14}])
        body.push([{text: '\n'}])
        row = []
        count = 0
      }
    });

    body.push([{columns: row, columnGap: 14}])

    return body;
  }

  function title(incidentTitle, pageNumber, logCount, messageCount) {
    var pageTitle = "Incident Summary"
    const logPageCount = Math.ceil(logCount / 8)
    if (messageCount > 0 && pageNumber > logPageCount) {
      pageTitle = "Message History"
    }
    var body = [{
      text: incidentTitle + ' | ' + pageTitle,
      style: 'header'
    }]

    return body
  }

  function messageBody(message) {
    var body = {
      stack: [
      {
        columns: [
        {width: 'auto', text: message['senderName'], style: 'messageSender'},
        {text: message['date'], style: 'messageDate'},
        ],
        columnGap: 8,
        lineHeight: 1.5
      },
      {text: message['content'], style: 'message'},
      {text: '\n'}
      ],
      unbreakable: true
    }
    return body
  }

  function buildMessageHistory(messages) {
    var body = []
    if (messages.length > 0) {
      body.push({ text: "", pageBreak: 'after' })
    }
    messages.forEach(function(message) {
      body.push(messageBody(message))
      body.push('\n')
    })
    return body
  }

  const linebreak = "\n"

async function downloadImages(logs) {
  await Promise.all(logs.map(async (log) => {
    functions.logger.log('Image download started for ', log);
    const filePath = log.imageReference;
    const fileName = path.basename(filePath);
    const tempFilePath = path.join(os.tmpdir(), fileName);
    await bucket.file(filePath).download({destination: tempFilePath});
    functions.logger.log('Image downloaded locally to', tempFilePath);
  }));
}

//////////// PDF GENERATION /////////////////
exports.generatePdf = functions.https.onCall(async (data, context) => {
  console.log("PDF GENERATION STARTED **************************")

  // if (request.method !== "GET") {
  //   response.send(405, 'HTTP Method ' + request.method + ' not allowed');
  //   return null;
  // }
  const teamId = data.teamId;
  const incidentId = data.incidentId;
  const incidentRef = firestore.collection('teams/').doc(teamId).collection('/history/').doc(incidentId);
  const incidentDoc = await incidentRef.get()
  const messages = []
  const logs = []

  if (!incidentDoc.exists) {
    throw new functions.https.HttpsError('not-found', 'Incident history not found.');
  }

  const incident = incidentDoc.data()

  const incidentTitle = incident["name"]
  const date = "date" //incident["completedDate"]
  const address = incident["address"]
  
  const eventLogRef = incidentRef.collection('eventLog')
  const logCollection = await eventLogRef.get()
  logCollection.forEach(doc => {
    logs.push(doc.data())
  })

  functions.logger.log("Checking if images need to be downloaded");
  if (logs.length > 0) {
    functions.logger.log("Image download beginning");
    await downloadImages(logs);
  }
  functions.logger.log("Done with image download");

  const messagesRef = incidentRef.collection('messages')
  const messageCollection = await messagesRef.get()
  messageCollection.forEach(doc => {
    messages.push(doc.data())
  })

  ////////////// DOC DEFINITION ///////////////////////
  const docDefinition = {
    pageSize: { width: 612, height: 792 },
    pageOrientation: 'portrait',
    pageMargins: [24,60,24,24],
    header: function(currentPage, pageCount, pageSize) {
      var headerBody = {
        columns: [
            title(incidentTitle, currentPage, logs.length, messages.length),
            { 
                text: 'Page ' + currentPage.toString() + ' of ' + pageCount, 
                alignment: 'right',
                style: 'header'
            }
        ],
        margin: [24, 24, 24, 0]
      }
      return headerBody
    },
    content: [
      date,
      linebreak,
      address,
      linebreak,
      { text: [
        { text: 'Incident Commander:', style: 'header' },
        { text: ' Daniel', style: 'regular'},
        ]
      },
      linebreak,
      { 
        text: [
          { text: 'Members involved:', style: 'header' },
          {text: ' Shawn, Zack, Gabe', style: 'regular'},
        ]
      },
      linebreak,
      buildLogsBody(logs),
      buildMessageHistory(messages)
    ],
    pageBreakBefore: function(currentNode, followingNodesOnPage, nodesOnNextPage, previousNodesOnPage) {
      return currentNode.headlineLevel === 1 && followingNodesOnPage.length === 0;
    },
    styles: {
      header: {
        fontSize: 16,
        bold: true
      },
      regular: {
        fontSize: 16,
        bold: false
      },
      messageSender: {
        fontSize: 14,
        bold: true
      },
      message: {
        fontSize: 14
      },
      messageDate: {
        fontSize: 14,
        color: 'gray'
      }
    }
  }

  const printer = new Printer(fontDescriptors);
  const pdfDoc = printer.createPdfKitDocument(docDefinition);
  var chunks = []
  const pdfName = `${teamId}/${incidentId}/report.pdf`;
  pdfDoc.on('data', function (chunk) {
    chunks.push(chunk);
  });

  pdfDoc.on('end', function () {
    functions.logger.log("PDF on end started")
    const result = Buffer.concat(chunks);

    // Upload generated file to the Cloud Storage
    const fileRef = bucket.file(
      pdfName,
      { 
        metadata: { 
          contentType: 'application/pdf'
        } 
      }
    );
  
    // bucket.upload("report.pdf", { destination: "${teamId}/${incidentId}/report.pdf", public: true})
    fileRef.save(result);
    fileRef.makePublic().catch(console.error);
    // Sending generated file as a response
    // res.send(result);
    functions.logger.log("File genderated and saved.")
    return { "response": result }
  });

  pdfDoc.on('error', function (err) {
    res.status(501).send(err);
    throw new functions.https.HttpsError('internal', err);

  });

  pdfDoc.end();
})

为了快速参考,主要端点方法是 exports.generatePdf,最后的 pdfDoc.on 是应该处理保存的代码,但代码似乎永远不会触发,因为登录它永远不会被记录,文档也不会总是被保存。

这是一个函数生命周期问题,您的函数在完成其任务之前被终止,因为您执行的最后一个操作处理事件处理程序而不是返回 Promise。它有时起作用的原因只是因为你很幸运。一旦一个功能完成,它应该已经完成​​了它需要做的一切。

因此,您需要做的是正确地将数据从 pdfDoc 流传输到云存储,所有这些都包含在 Promise 中,Cloud Functions 可以使用它来监控进度,并且不会在函数完成之前终止您的函数。

最简单的形式如下所示:

const stream = /* ... */;

const storageStream = bucket
  .file(/* path */)
  .createWriteStream(/* options */);

return new Promise((resolve, reject) => {
  storageStream.once("finish", resolve); // resolve when written
  storageStream.once("error", reject);   // reject when either stream errors
  stream.once("error", reject);

  stream.pipe(storageStream);            // pipe the data
});

注意:Google Cloud Storage Node SDK与Firebase客户端的Cloud Storage SDK不一样!

return new Promise((resolve, reject) => {
  const pdfDoc = printer.createPdfKitDocument(docDefinition);
  const pdfName = `${teamId}/${incidentId}/report.pdf`;
  
  // Reference to Cloud Storage upload location
  const fileRef = bucket.file(pdfName);
  
  const pdfReadStream = pdfDoc;
  const storageWriteStream = fileRef.createWriteStream({ 
    predefinedAcl: 'publicRead', // saves calling makePublic()
    contentType: 'application/pdf'
  });
  
  // connect errors from the PDF
  pdfReadStream.on('error', (err) => {
    console.error("PDF stream error: ", err);
    reject(new functions.https.HttpsError('internal', err));
  });
  // connect errors from Cloud Storage
  storageWriteStream.on('error', (err) => {
    console.error("Storage stream error: ", err);
    reject(new functions.https.HttpsError('internal', err));
  });
  // connect upload is complete event.
  storageWriteStream.on('finish', () => {
    functions.logger.log("File generated and saved to Cloud Storage.");
    resolve({ "uploaded": true });
  });

  // pipe data through to Cloud Storage
  pdfReadStream.pipe(storageWriteStream);
  
  // finish the document
  pdfDoc.end();
});