/** * Controls the communication with LibreOffice */ /* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const async = require('async'); const fs = require('fs'); const log4js = require('log4js'); const os = require('os'); const path = require('path'); const settings = require('./Settings'); const spawn = require('child_process').spawn; // Conversion tasks will be queued up, so we don't overload the system const queue = async.queue(doConvertTask, 1); const libreOfficeLogger = log4js.getLogger('LibreOffice'); /** * Convert a file from one type to another * * @param {String} srcFile The path on disk to convert * @param {String} destFile The path on disk where the converted file should be stored * @param {String} type The type to convert into * @param {Function} callback Standard callback function */ exports.convertFile = function (srcFile, destFile, type, callback) { // Used for the moving of the file, not the conversion const fileExtension = type; if (type === 'html') { // "html:XHTML Writer File:UTF8" does a better job than normal html exports if (path.extname(srcFile).toLowerCase() === '.doc') { type = 'html'; } // PDF files need to be converted with LO Draw ref https://github.com/ether/etherpad-lite/issues/4151 if (path.extname(srcFile).toLowerCase() === '.pdf') { type = 'html:XHTML Draw File'; } } // soffice can't convert from html to doc directly (verified with LO 5 and 6) // we need to convert to odt first, then to doc // to avoid `Error: no export filter for /tmp/xxxx.doc` error if (type === 'doc') { queue.push({ srcFile, destFile: destFile.replace(/\.doc$/, '.odt'), type: 'odt', callback() { queue.push({srcFile: srcFile.replace(/\.html$/, '.odt'), destFile, type, callback, fileExtension}); }, }); } else { queue.push({srcFile, destFile, type, callback, fileExtension}); } }; function doConvertTask(task, callback) { const tmpDir = os.tmpdir(); async.series([ /* * use LibreOffice to convert task.srcFile to another format, given in * task.type */ function (callback) { libreOfficeLogger.debug(`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); const soffice = spawn(settings.soffice, [ '--headless', '--invisible', '--nologo', '--nolockcheck', '--writer', '--convert-to', task.type, task.srcFile, '--outdir', tmpDir, ]); // Soffice/libreoffice is buggy and often hangs. // To remedy this we kill the spawned process after a while. const hangTimeout = setTimeout(() => { soffice.stdin.pause(); // required to kill hanging threads soffice.kill(); }, 120000); let stdoutBuffer = ''; // Delegate the processing of stdout to another function soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); // Append error messages to the buffer soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); soffice.on('exit', (code) => { clearTimeout(hangTimeout); if (code != 0) { // Throw an exception if libreoffice failed return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`); } // if LibreOffice exited succesfully, go on with processing callback(); }); }, // Move the converted file to the correct place function (callback) { const filename = path.basename(task.srcFile); const sourceFilename = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; const sourcePath = path.join(tmpDir, sourceFilename); libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); fs.rename(sourcePath, task.destFile, callback); }, ], (err) => { // Invoke the callback for the local queue callback(); // Invoke the callback for the task task.callback(err); }); }