├── .gitignore ├── package.json ├── kn2pdf-usage-example-01.js ├── README.md └── keynote2pdf.js /.gitignore: -------------------------------------------------------------------------------- 1 | # common ignores 2 | .classpath 3 | .project 4 | .settings 5 | .idea 6 | bin 7 | pojo-bin 8 | target 9 | *~ 10 | *~bak 11 | *-bak 12 | *.bak 13 | *-new 14 | *.rej 15 | *.orig 16 | .DS_Store 17 | .pydevproject 18 | *.pyc 19 | *.iml 20 | *.ipr 21 | *.iws 22 | *.gpd.* 23 | *cpJar* 24 | dependency-tree.log 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keynote2pdf", 3 | "description": "Conversion of zipped Keynote presentations to PDF. Runs only on Mac with Keynote installed (or just returns an error in a callback)", 4 | "version": "0.2.2", 5 | "main": "keynote2pdf.js", 6 | "author": "Nuxeo (http://www.nuxeo.com/)", 7 | "contributors": [ 8 | "Thibaud Arguillere " 9 | ], 10 | "homepage": "https://github.com/ThibArg/node-js-keynote2pdf", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/ThibArg/node-js-keynote2pdf" 14 | }, 15 | "keywords": [ 16 | "keynote" 17 | ], 18 | "engines": { 19 | "node": ">= 0.10.0" 20 | }, 21 | "os": [ 22 | "darwin" 23 | ], 24 | "dependencies": { 25 | "node-uuid": ">=1.4.1", 26 | "adm-zip": ">=0.4.4", 27 | "applescript": ">=0.2.1" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/ThibArg/node-js-keynote2pdf/issues" 31 | }, 32 | "scripts": { 33 | "test": "echo \"Error: no test specified\" && exit 1" 34 | }, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /kn2pdf-usage-example-01.js: -------------------------------------------------------------------------------- 1 | /* kn2pdf-usage-example-01.js 2 | 3 | Basic example with no error check and the strict minimum calls for handling files 4 | 5 | Reminder: 6 | (1) This server *must* run on Mac 7 | and (2) the Mac *must* have Keynote installed. 8 | 9 | A way to test it using curl: 10 | curl --upload-file "/path/to/the/presentation.zip" http://localhost:1337 -o "path/to/the_resultPdf.pdf" 11 | 12 | */ 13 | var http = require('http'); 14 | var fs = require("fs"); // We handle files, don't we? 15 | var os = require("os"); // For storing the received zip in a temporary folder 16 | var keynote2pdf = require("keynote2pdf"); 17 | 18 | // In this example, the server expects to receive only zipped files, there is 19 | // no handler to dispatch the request, etc. 20 | 21 | // ------------------------------------------------------------------------- 22 | // (1) Declare the temporary folder where we will store the zips 23 | // ------------------------------------------------------------------------- 24 | var gDestTempFolder = os.tmpDir() + "MyAppReceivedZips/"; 25 | if(!fs.existsSync(gDestTempFolder)) { 26 | fs.mkdirSync(gDestTempFolder); 27 | } 28 | // This vartiable is used to create "unique" names 29 | // WARNING: Only unqiue during this session 30 | var gCount = 0; 31 | http.createServer(function(request, response) { 32 | var destZipFile, destFileStream; 33 | 34 | // ------------------------------------------------------------------------- 35 | // (2) Get the file sent by the client 36 | // ------------------------------------------------------------------------- 37 | destZipFile = os.tmpDir() + gCount + ".zip"; 38 | destFileStream = fs.createWriteStream(destZipFile); 39 | request.pipe(destFileStream); 40 | // ------------------------------------------------------------------------- 41 | // (3) Once the file is received, convert it 42 | // ------------------------------------------------------------------------- 43 | request.on('end',function(){ 44 | keynote2pdf.convert(destZipFile, function(inError, inData) { 45 | var readStream; 46 | // ------------------------------------------------------------------------- 47 | // (4a) If we have an error, stop everything 48 | // ------------------------------------------------------------------------- 49 | if(inError) { 50 | console.log("Got an error: " + inError); 51 | response.writeHead(500, {'Content-Type': 'text/plain'}); 52 | response.end("Got an error: " + inError); 53 | // Delete our zip 54 | fs.unlinkSync(destZipFile); 55 | } else { 56 | // Just for this example, let's log the steps 57 | console.log(JSON.stringify(inData)); 58 | // ------------------------------------------------------------------------- 59 | // (4b) If we have no error, then return the pdf if we have it 60 | // ------------------------------------------------------------------------- 61 | if(inData.step === keynote2pdf.k.STEP_DONE) { 62 | readStream = fs.createReadStream(inData.pdf); 63 | // When the stream is ready, send the data to the client 64 | readStream.on('open', function () { 65 | // This pipe() API is amazing ;-) 66 | readStream.pipe(response); 67 | }); 68 | // When all is done with no problem, do some cleanup 69 | readStream.on('end', function () { 70 | // Tell keynote2pdf the temporary files used for this conversion 71 | // can be cleaned up 72 | keynote2pdf.canClean(inData.uid); 73 | // Do our own cleanup and delete the zip 74 | fs.unlinkSync(destZipFile); 75 | }); 76 | // In case of error, also do some cleanup 77 | readStream.on('error', function (inErr) { 78 | console.log("readStream -> error: " + inErr); 79 | response.end(inError); 80 | keynote2pdf.canClean(inData.uid); 81 | fs.unlinkSync(destZipFile); 82 | }); 83 | // We don't use this one 84 | /* 85 | readStream.on('data', function (chunk) { 86 | console.log('got %d bytes of data', chunk.length); 87 | }); 88 | */ 89 | } 90 | } 91 | }); // keynote2pdf.convert 92 | }); // request.on('end'...) 93 | }).listen(1337, "localhost", function(){ 94 | console.log("node server started, listening on localhost:1337"); 95 | }); 96 | 97 | // -- EOF-- -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-js-keynote2pdf 2 | =================== 3 | 4 | A node.js module which receives a zipped Keynote presentation (a path to a zip file), converts it to pdf (well, asks Keynote to convert it), and returns the pdf. 5 | 6 | * [Two main points](#two-main-points) 7 | * [Main Principles](#main-principles) 8 | * [Important: Dependencies](#important-dependencies) 9 | * [API](#api) 10 | * [Setup and Examples](#setup-and-examples) 11 | * [Interesting Information](#interesting-information) 12 | * [License (MIT)](#license) 13 | * [About Nuxeo](#about-nuxeo) 14 | 15 | #### Two main points: 16 | 17 | 1. Will **work only on Mac OS**, 18 | 2. and **only with Keynote installed** 19 | 20 | Tested with Mac OS X.9 Mavericks 21 | 22 | ### Main Principles 23 | This module is quite simple: Receives a .zip, returns a .pdf. It assumes the .zip is a keynote presentation. Here is the main flow: 24 | * Conversion: 25 | * Unzip the received file 26 | * Call an AppleScript to tell Keynote to export it to PDF 27 | * **IMPORTANT**: Conversion to PDF is done with the following parameters (these are opetionnal parameters expected by Keynote): 28 | * `compression factor`: `0.3` (we want the final pdf to be small 29 | * `export style`: `IndividualSlides` (one/page) 30 | * `all stages`: `true` (one slide/animation) 31 | * `skipped slides`: `false` (we don't want the skipped slides) 32 | * Current version does not allow changing these parameters. If you want something else, then fork this code (or just duplicate it, whatever), and change the `kAPPLE_SCRIPT_TEMPLATE` variable in `node-js-keynote2pdf`. 33 | * Send back the pdf in the callback 34 | * (all this being done asynchronously) 35 | * Cleanup of the files (.key package, result .pdf) every 5 minutes by default 36 | * The callback receives the regular (error, data) parameters (see below) 37 | 38 | ### Important: Dependencies 39 | Modules to install on your node server: 40 | 41 | npm install node-uuid 42 | npm install adm-zip 43 | npm install applescript 44 | 45 | ### API 46 | 47 | * **`configure`**`(inConfig)`: `inConfig` is an object with the following, optionnal properties: 48 | * `cleanup_timeout` 49 | * Every `cleanup_timeout` ms, the module will delete previous files created by the conversions. 50 | * Default value: 300000 (5 minutes) 51 | * `max_lifespan` 52 | * (Milliseconds) 53 | * When you don't explicitely call `canClean()` after a succesful conversion, temporary files are not deleted. To avoid flooding the disk, files are deleted if they were created since more than max_lifespan. 54 | * You must think about specific and probably rare usecase where either Keynote is very loaded, or a very, very big presentation is being converted and the conversion takes a lot of time, so you want to avoid accidental removal of a temporary file that is infact used. One hour seams to be good. 55 | * Default value: 3600000 (one hour) 56 | * `debug` 57 | * More or less info messages in the console 58 | * Default value is `false` 59 | 60 | * **`getInfo`**`()` returns an object containing the configuration and other info: 61 | * `config`, with `cleanup_timeout`, `max_lifespan`, `debug`, ...(possibly others) properties 62 | * `conversion_folder`: The full path to the conversion folder 63 | * `stats` 64 | * `conversions`: The total count of calls to handleRequest 65 | * `conversions_ok`: The count of succesful requests (pdf was returned to the client) 66 | 67 | * **`convert`**`(inZipFilePath, inCallback)` is the main API 68 | * `inZipFilePath` is the full path to a .zip file containing the Keynote presentation 69 | * `inCallback(inError, inData)` 70 | * When there is no error: 71 | * `inError` is `null` 72 | * `inData` has the following properties: 73 | * `uid`: A unique (string) identifier, to easily identify misc. requests when several are handled concurrently 74 | * `step`: A string telling what is the current step (unzipping the file, converting, sending the pdf) 75 | * Possible values are "Unzipping the file", "Converting the file" and "Done" 76 | * Constants are provided: `keynote2pdf.k.STEP_UNZIP`, `keynote2pdf.k.STEP_CONVERT` and `keynote2pdf.k.STEP_DONE` 77 | * `pdf`: Full path to the pdf, result of the conversion. This property is null as long as `step` is not `keynote2pdf.k.STEP_DONE` 78 | * When an error occured: 79 | * `inError` is not `null` and its `message` field contains the description of the problem 80 | * ìnData` has the following properties: 81 | * `uid`: A unique (string) identifier, to easily identify misc. requests when several are handled concurrently 82 | * `errorLabel`: As its name states. Most of the time, it is the same as `inError.message`. Let's say it's here for future use. 83 | 84 | ### Setup and Examples 85 | * Run only on Mac OS. Was developed under Mavericks (Mac OS X.9.n) 86 | * (if used on Windows/Linux, it just returns an error and does nothing) 87 | * Install Keynote on the Mac 88 | * (install nodejs) 89 | * Install the required external modules, if not yet installed on your server: 90 | 91 | ``` 92 | npm install node-uuid 93 | npm install adm-zip 94 | npm install applescript 95 | ``` 96 | 97 | * Well. Also install this module, `npm install keynote2pdf` 98 | * Actually, if you just need this module, you can install just it 99 | * See the `kn2pdf-usage-example-01.js` example to see how to use the module 100 | 101 | ### Interesting Information 102 | 103 | This module was developed as part of [nuxeo-keynote](https://github.com/ThibArg/nuxeo-keynote), a plug-in for [Nuxeo](http://nuxeo.com), which detects a zip file contains a Keynote presentation, then calls this nodejs server and handles the returned pdf (full text index and preview of the pdf in the browser) 104 | 105 | (yes. It _is_ interesting) 106 | 107 | ### License 108 | ``` 109 | (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. 110 | 111 | Permission is hereby granted, free of charge, to any person 112 | obtaining a copy of this software and associated documentation 113 | files (the "Software"), to deal in the Software without 114 | restriction, including without limitation the rights to use, 115 | copy, modify, merge, publish, distribute, sublicense, and/or sell 116 | copies of the Software, and to permit persons to whom the 117 | Software is furnished to do so, subject to the following 118 | conditions: 119 | 120 | The above copyright notice and this permission notice shall be 121 | included in all copies or substantial portions of the Software. 122 | 123 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 124 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 125 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 126 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 127 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 128 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 129 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 130 | OTHER DEALINGS IN THE SOFTWARE. 131 | 132 | Contributors: 133 | Thibaud Arguillere (https://github.com/ThibArg) 134 | ``` 135 | 136 | 137 | ### About Nuxeo 138 | 139 | Nuxeo provides a modular, extensible Java-based [open source software platform for enterprise content management](http://www.nuxeo.com/en/products/ep) and packaged applications for [document management](http://www.nuxeo.com/en/products/document-management), [digital asset management](http://www.nuxeo.com/en/products/dam) and [case management](http://www.nuxeo.com/en/products/case-management). Designed by developers for developers, the Nuxeo platform offers a modern architecture, a powerful plug-in model and extensive packaging capabilities for building content applications. 140 | 141 | More information on: 142 | -------------------------------------------------------------------------------- /keynote2pdf.js: -------------------------------------------------------------------------------- 1 | /* keynote2pdf.js 2 | 3 | Please, read the README file of the project for details and 4 | explanation, and how-to-use, etc. 5 | 6 | LGPL 2.1 license (see at the end of this file) 7 | 8 | =================================================== 9 | IMPORTANT - DEPENDENCIES - IMPORTANT - DEPENDENCIES 10 | =================================================== 11 | * Modules to install on your node server: 12 | npm install node-uuid 13 | npm install adm-zip 14 | npm install applescript 15 | 16 | * Will work only on Mac OS, and only with Keynote 17 | installed 18 | =================================================== 19 | 20 | Contributors: 21 | Thibaud Arguillere 22 | https://github.com/ThibArg 23 | */ 24 | 25 | (function () { 26 | /* 27 | ==================================================== 28 | require and var declaration 29 | ==================================================== 30 | */ 31 | // -> Require native node modules 32 | var fs = require('fs'); 33 | var path = require("path"); 34 | var os = require("os"); 35 | 36 | // -> Require third-party modules 37 | var uuid = require('node-uuid'); 38 | var AdmZip = require('adm-zip'); 39 | var applescript = require("applescript"); 40 | 41 | // -> Constants 42 | /* CONSTANTS 43 | */ 44 | var CONSTANTS = { 45 | STEP_UNZIP : "Unzipping the file", 46 | STEP_CONVERT: "Converting the file", 47 | STEP_DONE : "Done" 48 | }; 49 | 50 | /* kCONFIG_DEFAULT 51 | */ 52 | var kCONFIG_DEFAULT = { 53 | cleanup_timeout : 300000, 54 | max_lifespan : 3600000, 55 | debug : false 56 | }; 57 | 58 | /* kCONVERSION_FOLDER_PATH 59 | No need for comments I suppose 60 | */ 61 | var kCONVERSION_FOLDER_PATH = os.tmpDir() + "KeynoteToPDFConversions/"; 62 | 63 | /* kAPPLE_SCRIPT_TEMPLATE 64 | Store the AppleScript, with placeholders for the misc. paths and names. 65 | This is where you would tune the thing. For example: Handle queryStirng 66 | parameters to tune the compression factor, exports also the skipped 67 | slides, etc. 68 | */ 69 | var kAPPLE_SCRIPT_TEMPLATE = 'tell application "Keynote"\n' 70 | + '\n' 71 | + '--if playing is true then tell the front document to stop\n' 72 | + '\n' 73 | + 'set conversionFolderPath to ""\n' 74 | + '-- Open the presentation\n' 75 | + 'set pathToKey to ""\n' 76 | + 'open (POSIX file pathToKey) as alias\n' 77 | + '\n' 78 | + '-- Save a reference to this document\n' 79 | + 'set thePresentation to document ""\n' 80 | + '\n' 81 | + '-- Set up names and paths\n' 82 | + 'set documentName to the (name of thePresentation) & ".pdf"\n' 83 | + 'set the targetFileHFSPath to ((POSIX file conversionFolderPath as alias) & documentName) as string\n' 84 | + '\n' 85 | + '-- Convert to pdf\n' 86 | + 'export thePresentation to file targetFileHFSPath as PDF with properties {compression factor:0.3, export style:IndividualSlides, all stages:true, skipped slides:false}\n' 87 | + '\n' 88 | + '-- Done\n' 89 | + 'close thePresentation\n' 90 | + '\n' 91 | + 'return the POSIX path of targetFileHFSPath\n' 92 | + '\n' 93 | + 'end tell\n'; 94 | 95 | // -> Variables 96 | /* gIsMac 97 | Nothing will work if we are not on Mac 98 | */ 99 | gIsMac = os.platform() === "darwin"; 100 | if(!gIsMac) { 101 | console.error("keynote2pdf - Invalid platform error: Found '" + os.platform() + "', needs Mac OS ('darwin')"); 102 | } 103 | 104 | /* gHandledDocs 105 | An array storing the temporary files to cleanup every kAUTO_CLEANUP_TIMEOUT 106 | */ 107 | var gHandledDocs = []; 108 | 109 | /* config 110 | The misc. configuration properties 111 | */ 112 | var gConfig = kCONFIG_DEFAULT; 113 | 114 | /* initDone 115 | A basic flag 116 | */ 117 | var gInitDone = false; 118 | 119 | /* Stats 120 | */ 121 | var gConversionsCount = 0, 122 | gConversionsOkCount = 0; 123 | 124 | 125 | /* 126 | ==================================================== 127 | Private API 128 | ==================================================== 129 | */ 130 | /* fallbackCallback 131 | Stub to be used when caller does not use a callback 132 | (which is an error actually) 133 | */ 134 | function fallbackCallback() { 135 | // Nothing 136 | } 137 | 138 | /* doUnzipConvertAndReturnPDF 139 | 140 | */ 141 | function doUnzipConvertAndReturnPDF(inInfos, inCallback) { 142 | 143 | var pathToExtractionFolder, zip, oldPath, newPath; 144 | 145 | inCallback = typeof inCallback == "function" ? inCallback : fallbackCallback; 146 | 147 | inCallback(null, {uid : inInfos.uid, 148 | step : CONSTANTS.STEP_UNZIP, 149 | pdf : null 150 | }); 151 | 152 | pathToExtractionFolder = appendSlashIfNeeded( inInfos.folderPath ); 153 | try { 154 | zip = new AdmZip(inInfos.pathToFileToHandle); 155 | // Notice: extractAllTo() is synchronous 156 | zip.extractAllTo(pathToExtractionFolder, /*overwrite*/true); 157 | 158 | fs.readdir(pathToExtractionFolder, function(err, files) { 159 | var keynoteFileName = ""; 160 | files.some(function(fileName) { 161 | if(stringEndsWith(fileName, ".key")) { 162 | keynoteFileName = fileName; 163 | return true; 164 | } 165 | return false; 166 | }); 167 | if(keynoteFileName !== "") { 168 | // To handle the fact that several requests could ask to convert 169 | // documents with the same name, and to avoid conflicts in 170 | // Keynote, we use the UUID as name of the document so Keynote 171 | // is not confused (actullay, it is more the AppleScript which 172 | // would be confusing Keynote) 173 | oldPath = pathToExtractionFolder + keynoteFileName; 174 | newPath = pathToExtractionFolder + inInfos.uid + ".key"; 175 | fs.renameSync(oldPath, newPath); 176 | inInfos.pathToFileToHandle = newPath; 177 | doConvertAndReturnPDF(inInfos, inCallback); 178 | } else { 179 | console.log("Error: Can't find the .key file in the unzipped document"); 180 | inCallback(new Error("Can't find the .key file in the unzipped document"), 181 | { uid: inInfos.uid, 182 | errorLabel: "Can't find the .key file in the unzipped document", 183 | }); 184 | // Mark ready for cleanup 185 | inInfos.done = true; 186 | } 187 | }); 188 | } catch (e) { 189 | console.log("Error extracting the .zip "+ e); 190 | inCallback(new Error("Error extracting the .zip "+ e), 191 | { uid: inInfos.uid, 192 | errorLabel: "Error extracting the .zip "+ e, 193 | }); 194 | // Mark ready for cleanup 195 | inInfos.done = true; 196 | } 197 | } 198 | 199 | /* doConvertAndReturnPDF 200 | 201 | */ 202 | function doConvertAndReturnPDF(inInfos, inCallback) { 203 | 204 | var script; 205 | 206 | inCallback = typeof inCallback == "function" ? inCallback : fallbackCallback; 207 | inCallback(null, {uid : inInfos.uid, 208 | step : CONSTANTS.STEP_CONVERT, 209 | pdf : null 210 | }); 211 | 212 | script = kAPPLE_SCRIPT_TEMPLATE.replace("", inInfos.folderPath) 213 | .replace("", inInfos.pathToFileToHandle) 214 | .replace("", path.basename(inInfos.pathToFileToHandle) /*inInfos.uid + ".key"*/); 215 | //logIfDebug("-----------\n" + script + "\n----------"); 216 | 217 | // We wait until the file is really here and valid 218 | waitUntilFileExists(inInfos.pathToFileToHandle, 25, 40, function(result) { 219 | if(result) { 220 | try { 221 | applescript.execString(script, function(err, result) { 222 | if(err) { 223 | console.log("Conversion error: " + err); 224 | inCallback(err, 225 | { uid: inInfos.uid, 226 | errorLabel: "Conversion error:" + err 227 | }); 228 | // Mark ready for cleanup 229 | inInfos.done = true; 230 | } else { 231 | inInfos.pathToFileToHandle = result; 232 | doReturnThePDF(inInfos, inCallback); 233 | } 234 | }); 235 | } catch (e) { 236 | console.log("applescript.execString() error: "+ e); 237 | inCallback(new Error("applescript.execString() error: "+ e), 238 | { uid: inInfos.uid, 239 | errorLabel: "applescript.execString() error: "+ e, 240 | }); 241 | // Mark ready for cleanup 242 | inInfos.done = true; 243 | } 244 | } else { 245 | console.log("Can't find the keynote file at "+ inInfos.pathToFileToHandle); 246 | inCallback(new Error("Can't find the keynote file at "+ inInfos.pathToFileToHandle), 247 | { uid: inInfos.uid, 248 | errorLabel: "Can't find the keynote file at "+ inInfos.pathToFileToHandle, 249 | }); 250 | // Mark ready for cleanup 251 | inInfos.done = true; 252 | } 253 | }); 254 | } 255 | 256 | /* doReturnThePDF 257 | 258 | */ 259 | function doReturnThePDF(inInfos, inCallback) { 260 | 261 | inCallback = typeof inCallback == "function" ? inCallback : fallbackCallback; 262 | gConversionsOkCount += 1; 263 | inCallback(null, {uid : inInfos.uid, 264 | step : CONSTANTS.STEP_DONE, 265 | pdf : inInfos.pathToFileToHandle 266 | }); 267 | // Here we don't inInfos.done = true; because we don't want 268 | // to delete the pdf while the caller is sending it. Caller 269 | // must call canClean() after handling the returned PDF 270 | } 271 | 272 | 273 | /* ============================================================ 274 | Utilities 275 | ============================================================ */ 276 | /* logIfDebug 277 | 278 | */ 279 | function logIfDebug(inWhat) { 280 | if(gConfig.debug) { 281 | console.log(inWhat); 282 | } 283 | } 284 | 285 | 286 | /* cleanupExtractionFolder 287 | 288 | */ 289 | function cleanupExtractionFolder(cleanAll, inNextTimeout) { 290 | 291 | var now, objs; 292 | 293 | cleanAll = cleanAll || false; 294 | 295 | if(cleanAll) { 296 | deleteFolderRecursiveSync(kCONVERSION_FOLDER_PATH); 297 | } else { 298 | now = Date.now(); 299 | 300 | objs = Object.keys(gHandledDocs); 301 | if(objs.length > 0) { 302 | logIfDebug("Cleanup. " + objs.length + " folder"+ (objs.length > 1 ? "s" : "") + " to handle"); 303 | Object.keys(gHandledDocs).forEach(function(key) { 304 | var obj = gHandledDocs[key]; 305 | if(obj && (obj.done || (now - obj.timeStamp) > gConfig.max_lifespan)) { 306 | deleteFolderRecursiveSync(obj.folderPath); 307 | delete gHandledDocs[key]; 308 | } 309 | }); 310 | } 311 | } 312 | // Schedule next iteration 313 | if(typeof inNextTimeout === "number" && inNextTimeout > 0) { 314 | setTimeout(function() { 315 | cleanupExtractionFolder(false, inNextTimeout); 316 | }, inNextTimeout); 317 | } 318 | } 319 | 320 | /* deleteFolderRecursiveSync 321 | 322 | Thanks to http://www.geedew.com/2012/10/24/remove-a-directory-that-is-not-empty-in-nodejs/ 323 | */ 324 | function deleteFolderRecursiveSync(path) { 325 | if( fs.existsSync(path) ) { 326 | fs.readdirSync(path).forEach(function(file,index){ 327 | var curPath = path + "/" + file; 328 | if(fs.lstatSync(curPath).isDirectory()) { // recurse 329 | deleteFolderRecursiveSync(curPath); 330 | } else { // delete file 331 | fs.unlinkSync(curPath); 332 | } 333 | }); 334 | if(path !== kCONVERSION_FOLDER_PATH) { 335 | fs.rmdirSync(path); 336 | } 337 | } 338 | }; 339 | 340 | /* waitUntilFileExists 341 | 342 | */ 343 | function waitUntilFileExists(inPath, inTimeout, inMaxChecks, inCallback) { 344 | if(inMaxChecks <= 0) { 345 | inCallback(false); 346 | } else { 347 | if (fs.existsSync(inPath)) { 348 | inCallback(true); 349 | } else { 350 | setTimeout( function() { 351 | inMaxChecks -= 1; 352 | waitUntilFileExists(inPath, inTimeout, inMaxChecks, inCallback); 353 | }, inTimeout); 354 | } 355 | } 356 | } 357 | 358 | /* appendSlashIfNeeded 359 | 360 | */ 361 | function appendSlashIfNeeded(inPath) { 362 | if(typeof inPath === "string") { 363 | if(inPath.length === 0) { 364 | return "/"; 365 | } else if(inPath[ inPath.length - 1 ] !== "/") { 366 | return inPath + "/"; 367 | } 368 | } 369 | 370 | return inPath; 371 | } 372 | 373 | /* stringEndsWith 374 | 375 | */ 376 | function stringEndsWith(inStr, inToTest) { 377 | var position = inStr.length; 378 | position -= inToTest.length; 379 | var lastIndex = inStr.indexOf(inToTest, position); 380 | return lastIndex !== -1 && lastIndex === position; 381 | } 382 | 383 | 384 | /* 385 | ==================================================== 386 | Public API 387 | ==================================================== 388 | */ 389 | /* initSync() 390 | Set up configuration and the environment. Synchronous call. 391 | 392 | Automatically called when needed if not explicitely called before 393 | convert() 394 | */ 395 | function initSync(inConfig) { 396 | // Get the config 397 | if(inConfig) { 398 | if("cleanup_timeout" in inConfig && inConfig.cleanup_timeout > 0) { 399 | gConfig.cleanup_timeout = inConfig.cleanup_timeout; 400 | } 401 | if("max_lifespan" in inConfig && inConfig.max_lifespan > 0) { 402 | gConfig.max_lifespan = inConfig.max_lifespan; 403 | } 404 | if("debug" in inConfig) { 405 | gConfig.debug = inConfig.debug; 406 | } 407 | } 408 | 409 | // Prepare the stuff 410 | // -> Check conversion folder 411 | if(!fs.existsSync(kCONVERSION_FOLDER_PATH)) { 412 | fs.mkdirSync(kCONVERSION_FOLDER_PATH); 413 | } else { 414 | // -> Cleanup previous temp. files if any 415 | cleanupExtractionFolder(true); 416 | } 417 | 418 | console.log("keynote2pdf configuration:\n" 419 | + " Temp. folder: " + kCONVERSION_FOLDER_PATH + "\n" 420 | + " Cleanup every: " + gConfig.cleanup_timeout + " ms\n" 421 | + " Max lifespan: " + gConfig.max_lifespan + " ms\n" 422 | + " Debug mode: " + gConfig.debug + "\n"); 423 | 424 | // -> Install the cleaning scheduler every config.cleanup_timeout 425 | setTimeout(function() { 426 | cleanupExtractionFolder(false, gConfig.cleanup_timeout); 427 | }, gConfig.cleanup_timeout); 428 | 429 | gInitDone = true; 430 | } 431 | 432 | /* convert 433 | 434 | Main entry point. Handle everything 435 | */ 436 | function convert(inZipFilePath, inCallback) { 437 | var theUid, 438 | destFolder, 439 | infos; 440 | 441 | gConversionsCount += 1; 442 | 443 | inCallback = typeof inCallback == "function" ? inCallback : fallbackCallback; 444 | 445 | if(!gInitDone) { 446 | initSync(); 447 | } 448 | 449 | if(!gIsMac) { 450 | inCallback(new Error("Invalid platform error: keynote2pdf needs Mac OS"), 451 | { errorLabel: "Invalid platform error: keynote2pdf needs Mac OS", 452 | }); 453 | return; 454 | } 455 | 456 | theUid = uuid.v4(); 457 | destFolder = kCONVERSION_FOLDER_PATH + theUid + "/"; 458 | fs.mkdirSync(destFolder); 459 | 460 | infos = { uid: theUid, 461 | folderPath: destFolder, 462 | timeStamp: Date.now(), 463 | done: false, 464 | pathToFileToHandle: inZipFilePath 465 | }; 466 | gHandledDocs[theUid] = infos; 467 | 468 | doUnzipConvertAndReturnPDF(infos, inCallback); 469 | 470 | } 471 | 472 | /* canClean 473 | 474 | To be called after receiving the PDF in the callback and after having 475 | handling it. Let the module delete it in its cleaning loop. 476 | */ 477 | function canClean(inUid) { 478 | if(typeof inUid === "string" && inUid !== "") { 479 | var infos = gHandledDocs[inUid]; 480 | if(infos != null) { 481 | infos.done = true; // set the flag so cleaning can be done 482 | } 483 | } 484 | } 485 | 486 | 487 | /* getInfo() 488 | 489 | */ 490 | function getInfo() { 491 | return { 492 | config : gConfig, 493 | conversion_folder: kCONVERSION_FOLDER_PATH, 494 | stats : { 495 | conversions : gConversionsCount, 496 | conversions_ok : gConversionsOkCount 497 | } 498 | }; 499 | } 500 | 501 | // Give this public API to commonJS 502 | var keynote2pdf = convert; 503 | keynote2pdf.initSync = initSync; 504 | keynote2pdf.convert = convert; 505 | keynote2pdf.getInfo = getInfo; 506 | keynote2pdf.canClean = canClean; 507 | keynote2pdf.k = CONSTANTS; 508 | module.exports = keynote2pdf; 509 | 510 | 511 | }).call(this); 512 | 513 | /* 514 | * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. 515 | * 516 | * All rights reserved. This program and the accompanying materials 517 | * are made available under the terms of the GNU Lesser General Public License 518 | * (LGPL) version 2.1 which accompanies this distribution, and is available at 519 | * http://www.gnu.org/licenses/lgpl-2.1.html 520 | * 521 | * This library is distributed in the hope that it will be useful, 522 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 523 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 524 | * Lesser General Public License for more details. 525 | */ 526 | 527 | // -- EOF-- --------------------------------------------------------------------------------