如何将节点中生成的 docx 文件保存到 firebase 存储

How to get an docx file generated in node saved to firebase storage

您好,我对 docxtemplater 很陌生,但我非常喜欢它的工作原理。现在我似乎能够生成一个新的 docx 文档如下:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {Storage} = require('@google-cloud/storage');
var PizZip = require('pizzip');
var Docxtemplater = require('docxtemplater');
admin.initializeApp();
const BUCKET = 'gs://myapp.appspot.com';

exports.test2 = functions.https.onCall((data, context) => {
// The error object contains additional information when logged with JSON.stringify (it contains a properties object containing all suberrors).
function replaceErrors(key, value) {
    if (value instanceof Error) {
        return Object.getOwnPropertyNames(value).reduce(function(error, key) {
            error[key] = value[key];
            return error;
        }, {});
    }
    return value;
}

function errorHandler(error) {
    console.log(JSON.stringify({error: error}, replaceErrors));

    if (error.properties && error.properties.errors instanceof Array) {
        const errorMessages = error.properties.errors.map(function (error) {
            return error.properties.explanation;
        }).join("\n");
        console.log('errorMessages', errorMessages);
        // errorMessages is a humanly readable message looking like this :
        // 'The tag beginning with "foobar" is unopened'
    }
    throw error;
}


let file_name = 'example.docx';// this is the file saved in my firebase storage
const File = storage.bucket(BUCKET).file(file_name);
const read = File.createReadStream();

var buffers = [];
readable.on('data', (buffer) => {
  buffers.push(buffer);
});

readable.on('end', () => {
  var buffer = Buffer.concat(buffers);  
  var zip = new PizZip(buffer);
  var doc;
  try {
      doc = new Docxtemplater(zip);
      doc.setData({
    first_name: 'Fred',
    last_name: 'Flinstone',
    phone: '0652455478',
    description: 'Web app'
});
try {
   
    doc.render();
 doc.pipe(remoteFile2.createReadStream());

}
catch (error) {
    errorHandler(error);
}

  } catch(error) {
      errorHandler(error);
  }

});
});

我的问题是我不断收到 doc.pipe is not a function 的错误。我对 nodejs 很陌生,但是有没有办法让 doc.render() 之后新生成的文档直接保存到 firebase 存储?

查看 doc 的类型,我们发现它是一个 Docxtemplater 对象,并且发现 doc.pipe 不是 class 的函数。要从 Docxtemplater 中获取文件,我们需要使用 doc.getZip() 到 return 文件(这将是 JSZip v2Pizzip 实例,具体取决于什么我们传递给构造函数)。现在我们有了 zip 的对象,我们需要生成 zip 的二进制数据——这是使用 generate({ type: 'nodebuffer' }) 完成的(以获得包含数据的 Node.JS Buffer)。不幸的是,因为 docxtemplater 库不支持 JSZip v3+,我们无法使用 generateNodeStream() 方法来获取要与 pipe().[=40= 一起使用的流]

有了这个缓冲区,我们可以将它重新上传到云存储,或者将它发送回调用该函数的客户端。

第一个方案实现起来比较简单:

import { v4 as uuidv4 } from 'uuid';
/* ... */

const contentBuffer = doc.getZip()
      .generate({type: 'nodebuffer'});
const targetName = "compiled.docx";
  
const targetStorageRef = admin.storage().bucket()
  .file(targetName);
await targetStorageRef.save(contentBuffer);

// send back the bucket-name pair to the caller
return { bucket: targetBucket, name: targetName };

然而,将文件本身发送回客户端并不容易,因为这涉及切换到使用 HTTP Event Function (functions.https.onRequest) because a Callable Cloud Function 只能 return JSON 兼容的数据。这里我们有一个中间件函数,它接受一个可调用的处理函数,但支持 return 将二进制数据发送到客户端。

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import corsInit from "cors";

admin.initializeApp();

const cors = corsInit({ origin: true }); // TODO: Tighten

function callableRequest(handler) {
  if (!handler) {
    throw new TypeError("handler is required");
  }
  
  return (req, res) => {
    cors(req, res, (corsErr) => {
      if (corsErr) {
        console.error("Request rejected by CORS", corsErr);
        res.status(412).json({ error: "cors", message: "origin rejected" });
        return;
      }

      // for validateFirebaseIdToken, see https://github.com/firebase/functions-samples/blob/main/authorized-https-endpoint/functions/index.js
      validateFirebaseIdToken(req, res, () => { // validateFirebaseIdToken won't pass errors to `next()`

        try {
          const data = req.body;
          const context = {
            auth: req.user ? { token: req.user, uid: req.user.uid } : null,
            instanceIdToken: req.get("Firebase-Instance-ID-Token"); // this is used with FCM
            rawRequest: req
          };

          let result: any = await handler(data, context);

          if (result && typeof result === "object" && "buffer" in result) {
            res.writeHead(200, [
              ["Content-Type", res.contentType],
              ["Content-Disposition", "attachment; filename=" + res.filename]
            ]);
            
            res.end(result.buffer);
          } else {
            result = functions.https.encode(result);

            res.status(200).send({ result });
          }
        } catch (err) {
          if (!(err instanceof HttpsError)) {
            // This doesn't count as an 'explicit' error.
            console.error("Unhandled error", err);
            err = new HttpsError("internal", "INTERNAL");
          }

          const { status } = err.httpErrorCode;
          const body = { error: err.toJSON() };

          res.status(status).send(body);
        }
      });
    });
  };
})

functions.https.onRequest(callableRequest(async (data, context) => {
  /* ... */

  const contentBuffer = doc.getZip()
      .generate({type: "nodebuffer"});
  const targetName = "compiled.docx";

  return {
    buffer: contentBuffer,
    contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    filename: targetName
  }
}));

在您当前的代码中,有许多奇怪的段,您在不同的范围内嵌套了 try-catch 块和变量。为了帮助解决这个问题,我们可以利用 File#download() 和 return 中的 Promise 来解析 Node.JS Buffer 和 [=32= 中的文件内容] 那 return 是一个 Promise,在给定的 Buffer 上传时解析。

将这些合并在一起以重新上传到云存储得到:

// This code is based off the examples provided for docxtemplater
// Copyright (c) Edgar HIPP [Dual License: MIT/GPLv3]

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import PizZip from "pizzip";
import Docxtemplater from "docxtemplater";

admin.initializeApp();

// The error object contains additional information when logged with JSON.stringify (it contains a properties object containing all suberrors).
function replaceErrors(key, value) {
  if (value instanceof Error) {
    return Object.getOwnPropertyNames(value).reduce(
      function (error, key) {
        error[key] = value[key];
        return error;
      },
      {}
    );
  }
  return value;
}

function errorHandler(error) {
  console.log(JSON.stringify({ error: error }, replaceErrors));

  if (error.properties && error.properties.errors instanceof Array) {
    const errorMessages = error.properties.errors
      .map(function (error) {
        return error.properties.explanation;
      })
      .join("\n");
    console.log("errorMessages", errorMessages);
    // errorMessages is a humanly readable message looking like this :
    // 'The tag beginning with "foobar" is unopened'
  }
  throw error;
}

exports.test2 = functions.https.onCall(async (data, context) => {
  const file_name = "example.docx"; // this is the file saved in my firebase storage
  const templateRef = await admin.storage().bucket()
      .file(file_name);
  const template_content = (await templateRef.download())[0];
  const zip = new PizZip(template_content);

  let doc;
  try {
    doc = new Docxtemplater(zip);
  } catch (error) {
    // Catch compilation errors (errors caused by the compilation of the template : misplaced tags)
    errorHandler(error);
  }

  doc.setData({
    first_name: "Fred",
    last_name: "Flinstone",
    phone: "0652455478",
    description: "Web app",
  });

  try {
    doc.render();
  } catch (error) {
    errorHandler(error);
  }

  const contentBuffer = doc.getZip().generate({ type: "nodebuffer" });

  // do something with contentBuffer
  // e.g. reupload to Cloud Storage
  const targetStorageRef = admin.storage().bucket().file("compiled.docx");
  await targetStorageRef.save(contentBuffer);

  return { bucket: targetStorageRef.bucket.name, name: targetName };
});

除了向调用者return发送一个桶名对,您还可以考虑return向调用者发送一个访问URL。这可能是 signed url that can last for up to 7 days, a download token URL (like getDownloadURL(), ) that can last until the token is revoked, Google Storage URI (gs://BUCKET_NAME/FILE_NAME) (not an access URL, but can be passed to a client SDK that can access it if the client passes storage security rules) or access it directly using its public URL(在文件被标记为 public 之后)。

根据以上代码,您应该可以自己直接合并 return 文件。