From eeddf1348f351c92355d010ced56c3b511ca8f46 Mon Sep 17 00:00:00 2001 From: Peter 'Pita' Martischka Date: Tue, 19 Jul 2011 19:48:11 +0100 Subject: [PATCH] Added support for document export, no link from the pad so far --- node/Abiword.js | 93 +++++++++++++++++++++++++++++++++ node/ExportHandler.js | 116 +++++++++++++++++++++++++++++++++++++++++ node/server.js | 24 +++++++++ node/settings.js | 7 ++- settings.json.template | 6 ++- 5 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 node/Abiword.js create mode 100644 node/ExportHandler.js diff --git a/node/Abiword.js b/node/Abiword.js new file mode 100644 index 000000000..4e70d3fbd --- /dev/null +++ b/node/Abiword.js @@ -0,0 +1,93 @@ +/** + * Controls the communication with the Abiword application + */ + +/* + * 2011 Peter 'Pita' Martischka + * + * 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. + */ + +var util = require('util'); +var spawn = require('child_process').spawn; +var async = require("async"); +var settings = require("./settings"); + +//Queue with the converts we have to do +var queue = async.queue(doConvertTask, 1); + +//spawn the abiword process +var abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); + +//output error messages to stderr +abiword.stderr.on('data', function (data) +{ + console.error("Abiword: " + data); +}); + +//throw exceptions if abiword is dieing +abiword.on('exit', function (code) +{ + throw "Abiword died with exit code " + code; +}); + +//delegate the processing of stdout to a other function +abiword.stdout.on('data',onAbiwordStdout); + +var stdoutCallback = null; +var stdoutBuffer = ""; +var firstPrompt = true; + +function onAbiwordStdout(data) +{ + //add data to buffer + stdoutBuffer+=data.toString(); + + //we're searching for the prompt, cause this means everything we need is in the buffer + if(stdoutBuffer.search("AbiWord:>") != -1) + { + //filter the feedback message + var lines = stdoutBuffer.split("\n"); + var err = lines [1] == "OK" ? null : lines[1]; + + //reset the buffer + stdoutBuffer = ""; + + //call the callback with the error message + //skip the first prompt + if(stdoutCallback != null && !firstPrompt) + { + stdoutCallback(err); + stdoutCallback = null; + } + + firstPrompt = false; + } +} + +function doConvertTask(task, callback) +{ + abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n"); + + //create a callback that calls the task callback and the caller callback + stdoutCallback = function (err) + { + callback(); + task.callback(err); + }; +} + +exports.convertFile = function(srcFile, destFile, type, callback) +{ + queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback}); +}; diff --git a/node/ExportHandler.js b/node/ExportHandler.js new file mode 100644 index 000000000..e72b2dc59 --- /dev/null +++ b/node/ExportHandler.js @@ -0,0 +1,116 @@ +/** + * Handles the export requests + */ + +/* + * 2011 Peter 'Pita' Martischka + * + * 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. + */ + +var exporthtml = require("./exporters/exporthtml"); +var padManager = require("./PadManager"); +var async = require("async"); +var fs = require("fs"); +var settings = require('./settings'); + +//load abiword only if its enabled +if(settings.abiword != null) + var abiword = require("./Abiword"); + +/** + * do a requested export + */ +exports.doExport = function(req, res, padId, type) +{ + //tell the browser that this is a downloadable file + res.attachment(padId + "." + type); + + //if this is a plain text export, we can do this directly + if(type == "txt") + { + padManager.getPad(padId, function(err, pad) + { + if(err) + throw err; + + res.send(pad.text()); + }); + } + else + { + var html; + var randNum; + var srcFile, destFile; + + async.series([ + //render the html document + function(callback) + { + exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html) + { + html = _html; + callback(err); + }); + }, + //decide what to do with the html export + function(callback) + { + //if this is a html export, we can send this from here directly + if(type == "html") + { + res.send(html); + callback("stop"); + } + //write the html export to a file + else + { + randNum = Math.floor(Math.random()*new Date().getTime()); + srcFile = "/tmp/eplite_export_" + randNum + ".html"; + fs.writeFile(srcFile, html, callback); + } + }, + //send the convert job to abiword + function(callback) + { + //ensure html can be collected by the garbage collector + html = null; + + destFile = "/tmp/eplite_export_" + randNum + "." + type; + abiword.convertFile(srcFile, destFile, type, callback); + }, + //send the file + function(callback) + { + res.sendfile(destFile, null, callback); + }, + //clean up temporary files + function(callback) + { + async.parallel([ + function(callback) + { + fs.unlink(srcFile, callback); + }, + function(callback) + { + fs.unlink(destFile, callback); + } + ], callback); + } + ], function(err) + { + if(err && err != "stop") throw err; + }) + } +}; diff --git a/node/server.js b/node/server.js index 4887f24fd..2bc57a2a0 100644 --- a/node/server.js +++ b/node/server.js @@ -31,6 +31,7 @@ var async = require('async'); var express = require('express'); var path = require('path'); var minify = require('./minify'); +var exportHandler; var exporthtml; var readOnlyManager; @@ -68,6 +69,7 @@ async.waterfall([ //load modules that needs a initalized db readOnlyManager = require("./ReadOnlyManager"); exporthtml = require("./exporters/exporthtml"); + exportHandler = require('./ExportHandler'); //set logging if(settings.logHTTP) @@ -191,6 +193,28 @@ async.waterfall([ res.sendfile(filePath, { maxAge: exports.maxAge }); }); + //serve timeslider.html under /p/$padname/timeslider + app.get('/p/:pad/export/:type', function(req, res, next) + { + var types = ["pdf", "doc", "txt", "html", "odt"]; + //send a 404 if we don't support this filetype + if(types.indexOf(req.params.type) == -1) + { + next(); + return; + } + + //if abiword is disabled, and this is a format we only support with abiword, output a message + if(settings.abiword == null && req.params.type != "html" && req.params.type != "txt" ) + { + res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature"); + return; + } + + res.header("Server", serverName); + exportHandler.doExport(req, res, req.params.pad, req.params.type); + }); + //serve index.html under / app.get('/', function(req, res) { diff --git a/node/settings.js b/node/settings.js index 19f7eab1f..eb820e8ff 100644 --- a/node/settings.js +++ b/node/settings.js @@ -46,6 +46,11 @@ exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis pad text is synchron */ exports.minify = true; +/** + * The path of the abiword executable + */ +exports.abiword = null; + //read the settings sync var settingsStr = fs.readFileSync("../settings.json").toString(); @@ -75,7 +80,7 @@ for(var i in settings) } //we know this setting, so we overwrite it - if(exports[i]) + if(exports[i] !== undefined) { exports[i] = settings[i]; } diff --git a/settings.json.template b/settings.json.template index 3c985f0f3..6b80021ba 100644 --- a/settings.json.template +++ b/settings.json.template @@ -31,5 +31,9 @@ /* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly, but makes it impossible to debug the javascript/css */ - "minify" : true + "minify" : true, + + /* This is the path to the Abiword executable. Setting it to null, disables abiword. + Abiword is needed to enable the import/export of pads*/ + "abiword" : null }