How to perform server-side file processing operations in Meteor?

I store Word files (.docx) using GridFS on the server. I would like to combine documents into a single Word file using the docx-builder NPM package.

This is how I upload files:

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; } }) 

How can I do this on the server side? I can only do this on the server side, because the package I use requires the fs package, which cannot be executed on the client side.

This is how I am working on it right now. From the client, I call the following method (which is declared as 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(); }); } 

However, when I download the final document from the Client-side ProgramFiles collection, the document is an empty Word document.

What is wrong here?


I have included @FrederickStark answer in my code. Just stuck on this part now.


Here's another try:

 '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'); } 
+5
source share
2 answers

Looking at the docx-builder , it only supports reading docx files from the file system. The problem with calling document.url() is that it gives you a URL with which you can access via http, and not the path to the file system.

So, to use GridFS, you will first need to write the files to a temporary folder before docx-builder can read them.

 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); }); }); 

I suspect you can do this synchronously using fs.writeFileSync(filePath, document.data) , but not sure, therefore, have not used it in this example.

Alternatively, you can look for a docx package that can support reading from a stream or buffer, and then you do not need temporary files.

+3
source

I can only use this server part, because the package I use requires the fs package, which cannot be executed on the client side.

While it is true that the docx-builder library you are currently using depends on fs (hence, in the Node environment), you can directly use its docxtemplater dependency and use it only on the client (browser) side.

Very similar to docx-builder , you start with a standard template file and you can combine the local docx files into it, then generate the resulting docx file and save it locally or send it to your server.

Important steps to achieve what you are trying to do (e.g. merge docx files), but on the client side:

  • Get files on the client side , if they already exist on the network, or allow the user to select files from their local file system through the file type input and HTML5 file API :
 <input type="file" id="fileInput" /> 
  1. Read the contents of the file and extract its body, as done in 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. After all the files to be combined are processed, download the starter template file :
 // 'template.docx' is a static file on your server (eg 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. Attach the content and define a formatted data key so that it is inserted into the template file, then draw the document:
 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. Either request the Save As dialog or send the file (blob) to your server.
 function useResult(doc) { var out = doc.getZip().generate({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); saveAs(out, 'output.docx'); } 

Online demo: https://ghybs.imtqy.com/docx-builder-demo/ (no server-side processing, pure client-side logic)

Source code: https://github.com/ghybs/docx-builder-demo

Libraries:

+3
source

Source: https://habr.com/ru/post/1270177/


All Articles