如何在 Meteor 中执行服务器端文件处理操作?

How to execute server side file handling operations in Meteor?

我正在使用 GridFS on the server. I'd like to be able to merge the documents into one Word file by using the docx-builder NPM 包存储 Word(.docx) 文件。

这是我上传文件的方式:

Meteor.methods({
    uploadFiles: function (files) {
      check(files, [Object]);

      if (files.length < 1)
        throw new Meteor.Error("invalid-files", "No files were uploaded");

      var documentPaths = [];

      _.each(files, function (file) {
        ActivityFiles.insert(file, function (error, fileObj) {
          if (error) {
            console.log("Could not upload file");
          } else {
            documentPaths.push("/cfs/files/activities/" + fileObj._id);
          }
        });
      });

      return documentPaths;
    }
})

如何在服务器端执行此操作?我只能在服务器端执行此操作,因为我使用的包需要无法在客户端执行的 fs 包。

这是我目前正在努力解决的问题。我从客户端调用以下方法(声明为 Meteor.method):

print: function(programId) {
  // Get the program by ID.
  var program = Programs.findOne(programId);
  // Create a new document.
  var docx = new docxbuilder.Document();
  // Go through all activities in the program.
  program.activityIds.forEach(function(activityId) {
    // Create a temporary server side folder to store activity files.
    const tempDir = fs.mkdtempSync('/tmp/');
    // Get the activity by ID.
    var activity = Activities.findOne(activityId);
    // Get the document by ID.
    var document = ActivityFiles.findOne(activity.documents.pop()._id);
    // Declare file path to where file will be read.
    const filePath = tempDir + sep + document.name();
    // Create stream to write to path.
    const fileStream = fs.createWriteStream(filePath);
    // Read from document, write to file.
    document.createReadStream().pipe(fileStream);
    // Insert into final document when finished writinf to file.
    fileStream.on('finish', () => {
      docx.insertDocxSync(filePath);
      // Delete file when operation is completed.
      fs.unlinkSync(filePath);
    });
  });
  // Save the merged document.
  docx.save('/tmp' + sep + 'output.docx', function (error) {
    if (error) {
      console.log(error);
    }
    // Insert into Collection so client can access merged document.
    Fiber = Npm.require('fibers');
    Fiber(function() {
      ProgramFiles.insert('/tmp' + sep + 'output.docx');
    }).run();
  });
}

但是,当我从客户端的 ProgramFiles 集合中下载最终文档时,该文档是一个空的 Word 文档。

这里出了什么问题?


我已将@FrederickStark 的回答合并到我的代码中。现在就卡在这部分了。


这是另一个尝试:

'click .merge-icon': (e) => {
    var programId = Router.current().url.split('/').pop();
    var programObj = Programs.findOne(programId);
    var insertedDocuments = [];
    programObj.activityIds.forEach(function(activityId) {
      var activityObj = Activities.findOne(activityId);
      var documentObj = ActivityFiles.findOne(activityObj.documents.pop()._id);
      JSZipUtils.getBinaryContent(documentObj.url(), callback);
      function callback(error, content) {
        var zip = new JSZip(content);
        var doc = new Docxtemplater().loadZip(zip);
        var xml = zip.files[doc.fileTypeConfig.textPath].asText();
        xml = xml.substring(xml.indexOf("<w:body>") + 8);
        xml = xml.substring(0, xml.indexOf("</w:body>"));
        xml = xml.substring(0, xml.indexOf("<w:sectPr"));
        insertedDocuments.push(xml);
      }
    });
    JSZipUtils.getBinaryContent('/assets/template.docx', callback);
    function callback(error, content) {
      var zip = new JSZip(content);
      var doc = new Docxtemplater().loadZip(zip);
      console.log(doc);
      setData(doc);
    }


    function setData(doc) {
      doc.setData({
        // Insert blank line between contents.
        inserted_docs_formatted: insertedDocuments.join('<w:br/><w:br/>')
        // The template file must use a `{@inserted_docs_formatted}` placeholder
        // that will be replaced by the above value.
      });

      doc.render();

      useResult(doc);
    }

    function useResult(doc) {
      var out = doc.getZip().generate({
        type: 'blob',
        mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
      });
      saveAs(out, 'output.docx');
    }

查看docx-builder的文档,它只支持从文件系统读取docx文件。调用 document.url() 的问题是它给了你一个可以通过 http 访问的 url,而不是文件系统上的路径。

因此,要使用 GridFS,您首先需要将文件写入临时文件夹,然后 docx-builder 才能读取它们。

import fs from 'fs';
import { sep } from 'path';
const tempDir = fs.mkdtempSync('/tmp/' + sep);

program.activityIds.forEach(function(activityId) {
  var activity = Activities.findOne(activityId);
  console.log(activity);
  var document = ActivityFiles.findOne(activity.documents.pop()._id);
  documents.push(document);

  // Build a file path in the temp folder
  const filePath = tempDir + sep + document.name();

  // Write the document to the file system using streams
  const fileStream = fs.createWriteStream(filePath);
  document.createReadStream().pipe(fileStream);

  // When the stream has finished writing the file, add it to your docx
  fileStream.on('finish', () => {
    console.log(filePath);
    docx.insertDocxSync(filePath);
    // Delete the file after you're done
    fs.unlinkSync(filePath);
  });

});

我怀疑您可以使用 fs.writeFileSync(filePath, document.data) 同步执行此操作但不确定,因此未在示例中使用它。

或者,您可以寻找可以支持从流或缓冲区读取的 docx 包,这样您就不需要临时文件了。

I can only do this server side because the package I'm using requires the fs package which cannot be executed client side.

尽管您当前使用的 docx-builder 库确实依赖于 fs(因此在 Node 环境中),但可以直接使用它的依赖性 docxtemplater 和仅在客户端(浏览器)端使用它。

非常像docx-builder的工作方式,您从一个"standard"模板文件开始,您可以将本地docx文件合并到其中,然后生成生成的docx文件并将其保存在本地或发送到你的服务器。

实现您正在尝试做的事情(即合并 docx 文件)但在客户端的基本步骤是:

  1. 在客户端检索文件 如果它们已经存在于网络中,或者让用户通过文件类型输入和 HTML5 File API 从他们的本地文件系统中选择文件:
<input type="file" id="fileInput" />
  1. 读取文件内容并提取其正文,如docx-builder:
var insertedDocsFormatted = [];

// If the file is locally provided through File type input:
document.getElementById('fileInput').addEventListener('change', function (evt) {
  var file = evt.currentTarget.files[0],
      fr = new FileReader();

  fr.onload = function () {
    insertDocContent(fr.result);
  };
  fr.readAsArrayBuffer(file);
});

// Or if the file already exists on a server:
JSZipUtils.getBinaryContent(url, function (err, content) {
  insertDocContent(content);
});

function insertDocContent(content) {
  var zip = new JSZip(content),
      doc = new Docxtemplater().loadZip(zip);

  var xml = zip.files[doc.fileTypeConfig.textPath].asText();

  // Inspired from https://github.com/raulbojalil/docx-builder
  xml = xml.substring(xml.indexOf("<w:body>") + 8);
  xml = xml.substring(0, xml.indexOf("</w:body>"));
  xml = xml.substring(0, xml.indexOf("<w:sectPr"));

  // Keep for later use.
  insertedDocsFormatted.push(xml);
}
  1. 处理完所有要合并的文件后,加载入门模板文件:
// 'template.docx' is a static file on your server (e.g. in `public/` folder)
// But it could even be replaced by a user locally provided file,
// as done in step 2 above.
JSZipUtils.getBinaryContent('template.docx', callback);

function callback(err, content) {
  var zip = new JSZip(content);
  var doc = new Docxtemplater().loadZip(zip);

  setData(doc);
}
  1. 加入contents并定义一个带格式的data key,以便插入到模板文件中,然后渲染文档:
function setData(doc) {
  doc.setData({
    // Insert blank line between contents.
    inserted_docs_formatted: insertedDocsFormatted.join('<w:br/><w:br/>')
    // The template file must use a `{@inserted_docs_formatted}` placeholder
    // that will be replaced by the above value.
  });

  doc.render();

  useResult(doc);
}
  1. 提示一个 "Save As" 对话框或将文件 (blob) 发送到您的服务器。
function useResult(doc) {
  var out = doc.getZip().generate({
    type: 'blob',
    mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  });
  saveAs(out, 'output.docx');
}

在线演示:https://ghybs.github.io/docx-builder-demo/(完全没有服务器端处理,纯客户端逻辑)

源代码:https://github.com/ghybs/docx-builder-demo

图书馆: