2017-07-24 1 views
5

Je stocke des fichiers Word (.docx) en utilisant GridFS sur le serveur. J'aimerais pouvoir fusionner les documents dans un fichier Word en utilisant le package NPM docx-builder.Comment exécuter les opérations de gestion de fichiers côté serveur dans Meteor?

Voilà comment je télécharger les fichiers:

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

Comment puis-je aller sur le faire sur le côté serveur? Je ne peux faire ce côté serveur que parce que le paquet que j'utilise nécessite le paquet fs qui ne peut pas être exécuté côté client.

Voici comment j'essaie de travailler sur ce sujet pour le moment. À partir du client, j'appelle la méthode suivante (qui est déclarée comme 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(); 
    }); 
} 

Cependant, quand je télécharge le document final de la collection ProgramFiles du côté client, le document est un Vider le document Word.

Qu'est-ce qui ne va pas ici?


J'ai incorporé la réponse @ FrederickStark dans mon code. Juste coincé sur cette partie maintenant.


Voici un autre essai:

'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'); 
    } 
+1

ce que vous avez essayé jusqu'à présent? Vous devriez juste être en mesure d'interroger les fichiers de la base de données et les transmettre dans docx-builder –

+1

@FrederickStark J'ai ajouté mon travail. –

Répondre

3

En regardant les documents pour docx-builder, il ne supporte que la lecture des fichiers docx du système de fichiers. Le problème avec l'appel document.url() est qu'il vous donne une URL que vous pouvez accéder sur http, pas un chemin sur le système de fichiers. Donc, pour utiliser GridFS, vous devez d'abord écrire les fichiers dans un dossier temporaire avant que le docx-builder puisse les lire.

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

}); 

Je pense que vous pouvez le faire en utilisant de manière synchrone fs.writeFileSync(filePath, document.data) mais était pas sûr, donc ne pas l'utiliser dans l'exemple.

Vous pouvez également rechercher un package docx prenant en charge la lecture à partir d'un flux ou d'un tampon, sans avoir besoin de fichiers temporaires.

+0

C'est génial. C'est vraiment très utile. Juste une question. Une fois que j'ai fini de fusionner et que j'utilise 'docx.save()' pour écrire dans un autre fichier du répertoire '/ tmp /', comment puis-je rendre ce fichier disponible au client pour le téléchargement? Je suppose que ce que j'essaie de dire, c'est comment j'expose le fichier final que 'docx.save()' crée. Je vous remercie. –

+1

Je voudrais l'ajouter à nouveau dans une collection de fichiers avec GridFS, puis obtenir l'URL téléchargeable avec 'document.url()' –

+1

En outre, j'ai oublié de mentionner que vous pouvez nettoyer le code un tas en remplaçant 'fooColleciton.find (fooId) .fetch(). pop() 'avec' fooCollection.findOne (fooId) '.'FindOne' renverra immédiatement le document avec cet identifiant au lieu d'avoir à récupérer le tableau, puis afficher le seul élément –

3

Je ne peux faire ce côté serveur que parce que le paquet que j'utilise nécessite le paquet fs qui ne peut pas être exécuté côté client.

considérant qu'il est vrai que la bibliothèque docx-builder que vous utilisez actuellement dépend fs (donc sur un environnement de nœud), il est possible d'utiliser directement sa dépendance docxtemplater et à l'utiliser uniquement sur le côté client (navigateur). Tout comme docx-builder fonctionne, vous commencez avec un fichier modèle "standard" et vous pouvez y fusionner des fichiers docx locaux, puis générer le fichier docx résultant et l'enregistrer localement ou l'envoyer à votre serveur.

Les étapes essentielles pour atteindre ce que vous essayez de faire (c.-à-d.la fusion des fichiers docx), mais sur le côté client serait:

  1. Récupérer des fichiers sur le côté client si elles existent déjà dans le réseau, ou laisser l'utilisateur choisir les fichiers à partir de leur système de fichiers local par le biais de l'entrée de type de fichier et HTML5 File API:
<input type="file" id="fileInput" /> 
  1. Lire le contenu du fichier et extraire son corps, comme cela se fait dans 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. Une fois que tous les fichiers à fusionner sont traités, charger le fichier modèle de démarrage:
// '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. Joindre le contenu et définir une clé de données avec formatage, de sorte qu'il est inséré dans le fichier modèle, puis rendre le 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. Soit une invite "Enregistrer sous" dialogue ou envoyer le fichier (blob) sur votre serveur.
function useResult(doc) { 
    var out = doc.getZip().generate({ 
    type: 'blob', 
    mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 
    }); 
    saveAs(out, 'output.docx'); 
} 

démo en ligne: https://ghybs.github.io/docx-builder-demo/ (pas de traitement côté serveur du tout, la logique pure côté client)

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

Bibliothèques:

  • JSZip à décompressez docx fichiers
  • JSZipUtils lire un fichier sous forme binaire (qui peut être introduit dans JSZip)
  • docxtemplater pour manipuler un fichier .docx
  • FileSaver.js pour la « Enregistrer sous » boîte de dialogue (en option)
+0

Pourriez-vous élaborer un peu sur la façon dont vous feriez la partie 2 avec des fichiers stockés dans 'CollectionFS'? Où dois-je passer dans 'document.url()' pour charger les fichiers (documents) que je reçois de ma collection? –

+0

Pour 'JSZipUtils.getBinaryContent (document.url(), callbackForInsertedDoc)' (utiliser un rappel différent de celui du modèle, évidemment) – ghybs

+0

Voir l'étape 2 dans la réponse mise à jour. – ghybs