UNPKG

82.6 kBJavaScriptView Raw
1#!/usr/bin/env node
2/*
3
4MedImage Server - runs an http server allowing uploads of photos
5and downloads from other MedImage Servers.
6
7../config.json contains the settings for this server.
8
9Usage: node server.js [-verbose]
10
11
12Testing https connection:
13openssl s_client -CApath /etc/ssl/certs -connect yourdomain.com:5566
14
15*/
16
17
18
19var multiparty = require('multiparty');
20var http = require('http');
21var https = require('https');
22var util = require('util');
23var path = require("path");
24var upath = require("upath");
25require("date-format-lite");
26var mv = require('mv');
27var fs = require('fs');
28var exec = require('child_process').exec;
29var spawn = require('child_process').spawn;
30var drivelist = require('drivelist');
31var uuid = require('node-uuid');
32var fsExtra = require('fs-extra');
33var klaw = require('klaw');
34var separateReqPool = {maxSockets: 10};
35var request = require("request");
36var needle = require('needle');
37var readChunk = require('read-chunk'); // npm install read-chunk
38var fileType = require('file-type');
39var shredfile = require('shredfile')();
40var queryStringLib = require('querystring');
41var async = require('async');
42
43
44var verbose = false; //Set to true to display debug info
45var outdirDefaultParent = '/medimage'; //These should have a slash before, and no slash after.
46var outdirPhotos = '/photos'; //These should have a slash before, and no slash after.
47var defaultTitle = "image";
48var currentDisks = [];
49var configFile = __dirname + '/../config.json'; //Default location is one directory back
50var newConfigFile = '/../config-original/linORIGINALconfig.json'; //This is for a new install generally - it will auto-create from this file
51 //if the config doesn't yet exist
52var addonsConfigFile = __dirname + '/../addons/config.json';
53var htmlHeaderFile = __dirname + '/../public/components/header.html';
54var htmlHeaderCode = ""; //This is loaded from disk on startup
55var noFurtherFiles = "none"; //This gets piped out if there are no further files in directory
56var pairingURL = "https://atomjump.com/med-genid.php"; //Redirects to an https connection. In future switch to http://atomjump.org/med-genid.php
57var listenPort = 5566;
58var remoteReadTimer = null;
59var globalId = "";
60var httpsFlag = false; //whether we are serving up https (= true) or http (= false)
61var serverOptions = {}; //default https server options (see nodejs https module)
62var bytesTransferred = 0;
63var noWin = false; //By default we are on Windows
64var maxUploadSize = 10485760; //In bytes, max allowed = 10MB
65var readingRemoteServer = false; //We have started reading the remote server
66var allowPhotosLeaving = false; //An option to allow/prevent photos from leaving the server
67var allowGettingRemotePhotos = false; //An option to allow reading a proxy server - usually the client (often Windows) will need this
68 //set to true
69var changeReadUrl = ""; //If we've changed the readingUrl, we must change the existing reading loop
70var flapSimulation = false; //Simulate service flapping from e.g. a faulty load balancer. Usually 'false' unless testing
71var flapState = false;
72var webProxy = null; //For download requests from the internet, use a local proxy server at this URL
73 //See: http://stackoverflow.com/questions/23585371/proxy-authentication-in-node-js-with-module-request
74
75var allowedTypes = [ { "extension": ".jpg", "mime": "image/jpeg" },
76 { "extension": ".pdf", "mime": "application/pdf" },
77 { "extension": ".mp4", "mime": "video/mp4" },
78 { "extension": ".mp3", "mime": "audio/mpeg" },
79 { "extension": ".m4v", "mime": "video/mp4" },
80 { "extension": ".m4a", "mime": "audio/m4a" },
81 { "extension": ".csv", "mime": "text/csv" },
82 { "extension": ".json", "mime": "application/json" } ];
83
84var allowedChars = "a-zA-z0-9#._-"; //Allowed characters in filename - default
85
86
87var addons = []; //Addon included modules.
88global.globalConfig = null; //This is a global current version of the config file, available to add-ons
89
90//Handle a process sigint to quit smoothly
91process.on('SIGINT', function() {
92 console.log("Requesting a shutdown.");
93 setTimeout(function() {
94 // 100ms later the process kill it self to allow a restart
95 console.log("Clean exit.");
96 process.exit(0);
97 }, 100);
98});
99
100
101//Check any command-line options
102if((process.argv[2]) && (process.argv[2] == '-verbose')){
103 verbose = true;
104}
105
106if(process.env.npm_package_config_configFile) {
107 //This is an npm environment var set for the location of the configFile
108 configFile = process.env.npm_package_config_configFile;
109 console.log("Using config file:" + configFile);
110
111}
112
113
114function pushIfNew(arry, str) {
115 //Push a string to an array if it is new
116 console.log("Attempting to add to array:" + str);
117 for (var i = 0; i < arry.length; i++) {
118 if (arry[i] === str) { // modify whatever property you need
119 return;
120 }
121 }
122 console.log("Pushing string");
123 arry.push(str);
124 return arry;
125}
126
127
128function serverParentDir() {
129 //Get the current parent directory. E.g from C:\snapvolt\bin it will be relative ..\..\ = 'C:'
130 var curdir = normalizeInclWinNetworks(__dirname + "/..");
131 return curdir;
132}
133
134function ensurePhotoReadableWindows(fullPath, cb) {
135 //Optional cb(err) passed back
136 //Check platform is windows
137 var platform = process.platform;
138 if(verbose == true) console.log(process.platform);
139 var isWin = /^win/.test(platform);
140 if(verbose == true) console.log("IsWin=" + isWin);
141 if(isWin) {
142 //See: http://serverfault.com/questions/335625/icacls-granting-access-to-all-users-on-windows-7
143 //Grant all users access, rather than just admin
144 var run = 'icacls ' + fullPath + ' /t /grant Everyone:(OI)(CI)F';
145 if(verbose == true) console.log("Running:" + run);
146 exec(run, function(error, stdout, stderr){
147 console.log(stdout);
148 if(cb) {
149 if(error) {
150 cb(error);
151 } else {
152 cb(null);
153 }
154 }
155 });
156 } else {
157 if(cb) {
158 cb(null);
159 }
160
161 }
162}
163
164
165function shredWrapper(fullPath, theFile) {
166 var platform = process.platform;
167 if(verbose == true) console.log(process.platform);
168 var isWin = /^win/.test(platform);
169 if(verbose == true) console.log("IsWin=" + isWin);
170 if(isWin) {
171
172 var tempFile = fs.openSync(fullPath, 'r');
173 fs.closeSync(tempFile);
174
175 fs.unlinkSync(fullPath);
176 console.log("Sent on and deleted " + theFile);
177
178 } else {
179 //Do a true linux shred
180 shredfile.shred(fullPath, function(err, file) {
181 if(err) {
182 console.log(err);
183 return;
184 }
185 console.log("Sent on and shredded " + theFile);
186 });
187 }
188
189}
190
191
192function ensureDirectoryWritableWindows(fullPath, cb) {
193 //Optional cb(err) passed back
194 //Check platform is windows
195 var platform = process.platform;
196 if(verbose == true) console.log(process.platform);
197 var isWin = /^win/.test(platform);
198 if(verbose == true) console.log("IsWin=" + isWin);
199 if(isWin) {
200 //See: http://serverfault.com/questions/335625/icacls-granting-access-to-all-users-on-windows-7
201 //Grant all users access, rather than just admin
202 var run = 'icacls ' + fullPath + ' /grant Everyone:(OI)(CI)F';
203 if(verbose == true) console.log("Running:" + run);
204 exec(run, function(error, stdout, stderr){
205 console.log(stdout);
206 if(cb) {
207 if(error) {
208 cb(error);
209 } else {
210 cb(null);
211 }
212 }
213 });
214 } else {
215
216 cb(null);
217 }
218}
219
220
221function readHTMLHeader(cb) {
222 //Read the HTML standard header for pages. Returns the HTML code to the cb function, which includes the menu.
223 //This is retained in memory as it is frequently accessed.
224
225 fs.readFile(htmlHeaderFile, function read(err, data) {
226 if (err) {
227
228 cb("Sorry, cannot read the header file " + htmlHeaderFile, null);
229 return;
230 } else {
231 htmlHeaderCode = data; //Set the global
232 cb(null, data);
233 }
234 });
235
236}
237
238
239function checkConfigCurrent(setProxy, cb) {
240 //Reads and updates config to get any new hard-drives added to the system, or a GUID added
241 //setProxy is optional, otherwise set it to null
242 //Returns cb(err) where err = null, or a string with the error
243
244
245 //Write to a json file with the current drive. This can be removed later manually by user, or added to
246 fs.readFile(configFile, function read(err, data) {
247 if (err) {
248
249 //Copy newconfig.json into config.json - it is likely a file that doesn't yet exist
250 //Check file exists
251 fs.stat(configFile, function(ferr, stat) {
252 if(ferr == null) {
253 //File exists, perhaps a file permissions issue.
254 cb("Sorry, cannot read the config file! Please check your file permissions. " + ferr);
255 } else if(ferr.code == 'ENOENT') {
256 // file does not exist. Copy across a new version of newconfig.json to config.json
257 // and re-run
258
259
260
261 fsExtra.copy(__dirname + newConfigFile, configFile, function (err) {
262 if (err) {
263 return console.error(err)
264 } else {
265 //Success - try reading again
266
267
268
269 checkConfigCurrent(setProxy, cb);
270 return;
271 }
272 }) // copies file
273 } else {
274 //Some other error. Perhaps a permissions problem
275 cb("Sorry, cannot read the config file! Please check your file permissions. " + ferr);
276 }
277 });
278
279
280 } else {
281 if(data) {
282 var content = JSON.parse(data);
283 } else {
284 if(global.globalConfig) {
285 content = global.globalConfig;
286 } else {
287 cb("Sorry, the config file is blank.");
288 return;
289 }
290 }
291
292 if(!content.globalId) {
293 //Only need to create the server's ID once. And make sure it is not the same as the developer's ID
294 //Old style:content.globalId = uuid.v4();
295 //Now we assume a blank guid to begin with.
296 }
297
298
299 if(setProxy) {
300 content.readProxy = setProxy;
301 }
302
303 if((globalId)&&(validateGlobalId(globalId) !== false)) {
304 content.globalId = globalId;
305 }
306
307 if(content.globalId) {
308 globalId = content.globalId;
309 }
310
311 if(content.listenPort) {
312 listenPort = content.listenPort;
313 }
314
315 if(content.allowedTypes) {
316 allowedTypes = content.allowedTypes;
317 }
318
319 if(content.httpsKey) {
320 //httpsKey should point to the key .pem file
321 httpsFlag = true;
322 if(!serverOptions.key) {
323 serverOptions.key = fs.readFileSync(content.httpsKey);
324 console.log("https key loaded");
325 }
326 }
327
328 if(content.httpsCert) {
329 //httpsCert should point to the cert .pem file
330 httpsFlag = true;
331 if(!serverOptions.cert) {
332 serverOptions.cert = fs.readFileSync(content.httpsCert);
333 console.log("https cert loaded");
334 }
335
336 }
337
338 if(content.webProxy) {
339 //There is a web proxy server used for download requests from the web.
340 //Format: "http://" + user + ":" + password + "@" + host + ":" + port
341 // or "http://" + host + ":" + port
342 webProxy = content.webProxy;
343
344 }
345
346 //An option to allow/prevent photos from leaving the server (local installs i.e. non 'proxy' Windows clients
347 //should set this to false for security of the photos).
348 if(content.allowPhotosLeaving) {
349 allowPhotosLeaving = content.allowPhotosLeaving;
350 }
351
352 //An option to allow reading a proxy server - usually the client (often Windows) will need this
353 //set to true, but internet based servers should have this to false, so that it cannot eg. read itself.
354 if(content.allowGettingRemotePhotos) {
355 allowGettingRemotePhotos = content.allowGettingRemotePhotos;
356 }
357
358 //An option to only allow certain characters
359 if(content.allowedChars) {
360 allowedChars = content.allowedChars;
361 }
362
363
364 if(bytesTransferred != 0) {
365 //Keep this up-to-date as we download
366 content.transfer = bytesTransferred;
367 } else {
368 if(content.transfer) {
369 //Just starting server = get bytes from transfer
370 bytesTransferred = content.transfer;
371 }
372
373 }
374
375
376 if(content.onStartBackupDriveDetect == true) {
377 //Get the current drives - if we want to auto-detect them when they get inserted
378 drivelist.list(function(error, disks) {
379 if (error) {
380 console.log("Warning: couldn't get the current drives for BackupDriveDetect. Error:" + error);
381
382 } else {
383
384 for(var cnt=0; cnt< disks.length; cnt++) {
385 //On each drive, create a backup standard directory for photos
386 if(verbose == true) console.log("Drive detected:" + JSON.stringify(disks[cnt]));
387 var drive = disks[cnt].mountpoint;
388
389 if(drive) {
390
391 if(serverParentDir().indexOf(drive) < 0) {
392 //Drive is not included in this server parent dir, therefore talking about a different drive
393
394 //Create the dir
395 if (!fs.existsSync(normalizeInclWinNetworks(drive + outdirDefaultParent))){
396 fs.mkdirSync(normalizeInclWinNetworks(drive + outdirDefaultParent));
397 }
398
399 if (!fs.existsSync(normalizeInclWinNetworks(drive + outdirDefaultParent + outdirPhotos))){
400 fs.mkdirSync(normalizeInclWinNetworks(drive + outdirDefaultParent + outdirPhotos));
401 }
402
403 //Append to the file's array if user has configured it as such
404 if(content.onStartBackupDriveDetect == true) {
405 content.backupTo = pushIfNew(content.backupTo, drive + outdirDefaultParent + outdirPhotos);
406 }
407 }
408 }
409 }
410
411 try {
412 var newContent = JSON.stringify(content, null, 6);
413 }
414 catch(err) {
415 cb("Error: the config file cannot be written: " + err.message);
416 return;
417 }
418
419 if(newContent) {
420 //Set the global copy in RAM for add-ons to use
421 globalConfig = content;
422
423 //Write the config file nicely formatted again, after we've added the new backup drives
424 fs.writeFile(configFile, newContent, function(err) {
425
426
427 if(verbose == true) console.log("The config file was saved!");
428
429 //Now start any ping to read from a remote server
430 if((content.readProxy) && (content.readProxy != "")) {
431 startReadRemoteServer(content.readProxy);
432 }
433
434 if(err) {
435 if(verbose == true) console.log("Warning: The config file was not saved! Error: " + err);
436 cb(err);
437 } else {
438 cb(null);
439 }
440 });
441 } else {
442 //This is a blank config file. Leave as is.
443 cb("Error: was about to save a blank config.");
444 }
445
446
447 }
448
449 });
450 } else {
451
452 try {
453 var newContent = JSON.stringify(content, null, 6);
454 }
455 catch(err) {
456 cb("Error: the config file cannot be written: " + err.message);
457 return;
458 }
459
460 if(newContent) {
461 //Set the global copy in RAM for add-ons to use
462 globalConfig = content;
463
464 //Write the config file nicely formatted again
465 fs.writeFile(configFile, newContent, function(err) {
466
467 if(verbose == true) console.log("The config file was saved!");
468
469 //Now start any ping to read from a remote server
470 if((content.readProxy) && (content.readProxy != "")) {
471 startReadRemoteServer(content.readProxy);
472 }
473
474 if(err) {
475 if(verbose == true) console.log("Warning: The config file was not saved! Error: " + err);
476 cb(err);
477 } else {
478 cb(null);
479 }
480 });
481 } else {
482 //This is a blank config file. Leave as is.
483 cb("Error: was about to save a blank config.");
484
485 }
486
487
488
489 }
490
491
492
493
494 };
495 });
496
497}
498
499
500function fileWalk(startDir, cb)
501{
502 //Read and return the first file in dir, and the count of which file it is. Only the cnt = 0 is used
503 var items = [];
504 if(verbose == true) console.log("Searching:" + startDir);
505
506 if (fsExtra.existsSync(normalizeInclWinNetworks(startDir))){
507 try {
508 var walk = klaw(startDir);
509
510 walk.on('data', function (item) {
511 if(verbose == true) console.log("Found:" + item.path);
512 items.push(item.path);
513 })
514 .on('end', function () {
515 for(var cnt = 0; cnt< items.length; cnt++) {
516
517 //Go through allowed file types array
518 for(var type = 0; type < allowedTypes.length; type++) {
519
520 if(items[cnt].indexOf(allowedTypes[type].extension) >= 0) {
521 cb(items[cnt], allowedTypes[type].mime);
522 return;
523 }
524 }
525 }
526
527 cb(null);
528 });
529 } catch(err) {
530 console.log("Error reading:" + err);
531 cb(null);
532 }
533 } else {
534 cb(null);
535
536 }
537
538
539}
540
541//Courtesy http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
542function formatBytes(bytes,decimals) {
543
544 if(verbose == true) console.log("Bytes: " + bytes);
545 if(bytes == 0) return 'None';
546 var k = 1000; // or 1024 for binary
547 var dm = decimals + 1 || 3;
548 var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
549 var i = Math.floor(Math.log(bytes) / Math.log(k));
550 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
551}
552
553
554function download(uri, callback){
555
556
557
558 //Get a header of file first - see if there is any content (this will be pinged once every 10 seconds or so)
559 request.head({url: uri, pool: separateReqPool, forever: true, proxy: webProxy }, function(err, res, body){
560 if(err) {
561 console.log("Error requesting from proxy:" + err);
562 callback(err);
563 } else { //No error
564 if(verbose == true) {
565 console.log(JSON.stringify(res.headers));
566 console.log('content-type:', res.headers['content-type']);
567 console.log('content-length:', res.headers['content-length']);
568 console.log('file-name:', res.headers['file-name']);
569 }
570 //Check if there is a filename
571 if(res.headers['file-name']) {
572
573 //Yes there was a new photo file to download fully.
574 var dirFile = res.headers['file-name'];
575 dirFile = trimChar(dirFile.replace(globalId + '/', '')); //remove our id and any slashes around it
576
577
578 var createFile = normalizeInclWinNetworks(trailSlash(serverParentDir()) + trailSlash(outdirPhotos) + dirFile);
579
580
581 if(verbose == true) console.log("Creating file:" + createFile);
582 var dirCreate = path.dirname(createFile);
583 if(verbose == true) console.log("Creating dir:" + dirCreate);
584
585 //Make sure the directory exists
586 fsExtra.ensureDir(dirCreate, function(err) {
587 if(err) {
588 console.log("Warning: Could not create directory for: " + dirCreate);
589 callback("Warning: Could not create directory for: " + dirCreate);
590 } else {
591 if(verbose == true) console.log("Created dir:" + dirCreate);
592 ensureDirectoryWritableWindows(dirCreate, function(err) {
593
594 if(err) {
595 //Could not get a true directory
596 console.log("Error processing dir:" + err);
597 callback("Error processing dir:" + err);
598 } else {
599 //Got a good directory
600 if(verbose == true) console.log("Directory processed");
601 if(verbose == true) console.log("About to create local file " + createFile + " from uri:" + uri);
602
603
604 var stream = request({url: uri, pool: separateReqPool, forever: true });
605 var alreadyClosed = false;
606 stream.pipe(fs.createWriteStream(createFile)
607 .on('error', function(err) {
608 console.log("Error writing to file");
609 callback(err);
610 })
611 )
612 .on('close', function() {
613
614 console.log("Downloaded successfully!" + createFile);
615 if(alreadyClosed == false) {
616 alreadyClosed = true;
617
618 //Update the data transferred successfully
619 if(uri.indexOf("atomjump.com") >= 0) {
620 try {
621 //File exists
622 var stats = fs.statSync(createFile);
623 var fileSizeInBytes = stats["size"];
624 bytesTransferred += fileSizeInBytes;
625
626 //Save the bytes transferred to atomjump.com for progress
627 checkConfigCurrent(null, function() {
628
629 })
630 }
631 catch(err) {
632 console.log('Warning: file ' + createFile + ' did not exist.');
633 }
634 }
635
636 if(createFile.indexOf(".jpg") >= 0) {
637 var event = "photoWritten";
638 } else {
639 var event = "fileWritten";
640 }
641
642 addOns(event, function(err, normalBackup) {
643 if(err) {
644 console.log("Error writing file:" + err);
645 } else {
646 if(verbose == true) console.log("Add-on completed running");
647
648 }
649
650 if(normalBackup == true) {
651 //Now we have finished processing the file via the addons,
652 //backup the standard files if 'normal' is the case
653 backupFile(createFile, "", dirFile, { });
654 }
655 }, createFile);
656
657
658
659
660 callback(null); //Carry on with normal operations, with the addons working in the background
661 } else {
662 console.log("2nd close event");
663 }
664 });
665
666
667 } //End of got a good directory
668
669
670
671 }); //end of ensureDirectoryWritableWindows
672
673 }
674 }); //end of ensuredir exists
675
676
677 } else {
678 //No filename in returned ping here
679 if(verbose == true) console.log("No file to download");
680 callback(null);
681 }
682
683 } //end of no error from ping of proxy
684 }); //end of request head
685
686
687}
688
689
690
691function startReadRemoteServer(url)
692{
693 if(allowGettingRemotePhotos == false) {
694 console.log("Error: Sorry, this server is not configured to read a remote server.");
695 return;
696 }
697
698 if(readingRemoteServer == false) {
699 readingRemoteServer = true;
700 readRemoteServer(url);
701 } else {
702 //Double reading - switch over the readRemoteServer already existing to the new url
703 changeReadUrl = url;
704
705 }
706}
707
708function readRemoteServer(url)
709{
710 //Every 5 seconds, read the remote server in the config file, and download images to our server.
711 //But pause while each request is coming in until fully downloaded.
712
713 //If it already exists we need to be able to change this ping
714 if(changeReadUrl != "") {
715 url = changeReadUrl;
716 changeReadUrl = "";
717 }
718
719 var _url = url;
720
721
722
723 setTimeout(function() {
724 process.stdout.write("'"); //Display movement to show download pinging
725 var thisComplete = false; //This is a flag for this request only.
726 download(url, function(){
727 if(verbose == true) console.log("Ping status: " + thisComplete);
728 if(thisComplete == false) {
729 //It is possible we have been run by the timeout below, so don't repeat (or we would get faster and faster)
730 thisComplete = true;
731 readRemoteServer(_url);
732 }
733 });
734
735 //Now do a double check there has been no problem during the download. Have a timeout after 5 minutes to restart the ping process.
736 setTimeout(function() {
737 if(verbose == true) console.log("Ping status after 1 minutes time: " + thisComplete);
738 if(thisComplete == false) {
739 //OK restart the ping still.
740 thisComplete = true;
741 readRemoteServer(_url);
742 }
743 }, 60000); //Should be 60000 = 1 minute
744
745 }, 5000);
746
747
748}
749
750function trailSlash(str)
751{
752 if(str.slice(-1) == "/") {
753 return str;
754 } else {
755 var retStr = str + "/";
756 return retStr;
757 }
758
759}
760
761
762function normalizeInclWinNetworks(path)
763{
764 //Tests to see if the path is a Windows network path first, if so, handle this case slightly differently
765 //to a normal upath.normalization.
766 //Run this before
767 if((path[0] == "\\")&&(path[1] == "\\")) {
768
769 return "\/" + upath.normalize(path); //Prepend the first slash
770 } else {
771 if((path[0] == "\/")&&(path[1] == "\/")) {
772 //In unix style syntax, but still a windows style network path
773 return "\/" + upath.normalize(path); //Prepend the first slash
774 } else {
775 return upath.normalize(path);
776 }
777 }
778
779}
780
781
782
783function backupFile(thisPath, outhashdir, finalFileName, opts, cb)
784{
785 // thisPath: full path of the file to be backed up from the root file system
786 // outhashdir: the directory path of the file relative to the root photos dir /photos. But if blank,
787 // this is included in the finalFileName below.
788 // finalFileName: the photo or other file name itself e.g. photo-01-09-17-12-54-56.jpg
789 // opts:
790 // {first: true} Process the first on the list (defaults to true)
791 // {secondary: true} Process the remainder paths on the list (defaults to true)
792 // {keepOriginal: false} Keep the original file (defaults to "useMasterConfig").
793 // which takes this value from the config file
794 //Will call cb once all are completed, with (err, null) if there was an error, or (null, newPath) if none
795 // where newPath is the last output file path (a single file only - most useful when opts.first is true)
796
797 var lastNewPath = thisPath;
798
799 if(!opts) {
800 opts = {};
801 }
802 if(!opts.first) {
803 opts.first = true;
804 }
805 if(!opts.secondary) {
806 opts.secondary = true;
807 }
808 if(!opts.keepOriginal) {
809 opts.keepOriginal = "useMasterConfig";
810 }
811 if(!cb) {
812 cb = function() {};
813 }
814
815
816
817
818 //Read in the config file
819 fs.readFile(configFile, function read(err, data) {
820 if (err) {
821 console.log("Warning: Error reading config file for backup options: " + err);
822 cb(err, null);
823 } else {
824 if(data) {
825 var content = JSON.parse(data);
826 } else {
827 //There was an error reading the data. Use the existing global var.
828 var content = global.globalConfig;
829 }
830
831
832 if(content.backupTo) {
833
834
835
836 //Loop through all the backup directories
837 async.eachOf(content.backupTo,
838 // 2nd param is the function that each item is passed to
839 function(runBlock, cnt, callback){
840
841
842 if((cnt == 0) && (opts.first == false)) {
843 //Early exist on this one
844 callback(null);
845 return;
846 }
847
848 if((cnt > 0)&& (opts.secondary == false)) {
849 //Early exist on this one
850 callback(null);
851 return;
852
853 }
854
855
856 if(outhashdir) {
857 var targetDir = normalizeInclWinNetworks(trailSlash(content.backupTo[cnt]) + trailSlash(outhashdir));
858 } else {
859 var targetDir = normalizeInclWinNetworks(trailSlash(content.backupTo[cnt]));
860
861 }
862 var target = targetDir + finalFileName;
863 target = normalizeInclWinNetworks(target.trim());
864 lastNewPath = target; //Record for the return journey
865 thisPath = normalizeInclWinNetworks(thisPath.trim()); //OLD: Remove double slashes. Normalize will handle that
866
867
868 if(verbose == true) console.log("Backing up " + thisPath + " to:" + target);
869
870 fsExtra.ensureDir(targetDir, function(err) {
871 if(err) {
872 console.log("Warning: Could not create directory for backup: " + targetDir + " Err:" + err);
873 callback(err);
874 } else {
875 try {
876
877 if(thisPath !== target) {
878 console.log("Copying " + thisPath + " to " + target);
879 fsExtra.copy(thisPath, target, function(err) {
880 if (err) {
881 callback(err, null);
882 } else {
883
884 ensurePhotoReadableWindows(target);
885
886 //And finally, remove the old file, if the option is set
887 if(opts.keepOriginal == "useMasterConfig") {
888
889 //Determine whether to keep the original photo file based off the global setting
890 opts.keepOriginal = true;
891 if( typeof content.keepMaster === 'undefined') {
892 //Not considered
893 } else {
894 if(content.keepMaster == false) {
895 opts.keepOriginal = false;
896 }
897 }
898 }
899
900 if(opts.keepOriginal == false) {
901
902 fsExtra.remove(thisPath, function(err) {
903 if (err) {
904 console.error('Warning: there was a problem removing: ' + err.message)
905 callback(err, null);
906 } else {
907 console.log('Removed the file: ' + thisPath);
908 callback(null);
909 }
910 });
911
912 } else {
913 callback(null);
914 }
915 }
916
917
918 }) // copies file
919 } else {
920 //Same file
921 callback(null);
922 }
923
924
925
926 } catch (err) {
927 console.error('Warning: there was a problem backing up: ' + err.message);
928 callback(err, null);
929 }
930 }
931 });
932
933 }, //End of async eachOf single item
934 function(err){
935 // All tasks are done now
936 if(err) {
937 console.log('ERR:' + err);
938 cb(err, null);
939 } else {
940 if(verbose == true) console.log('Completed all backups!');
941 cb(null, lastNewPath);
942 }
943 }
944 ); //End of async eachOf all items
945
946
947
948 } else { //End of check there is a backup
949
950 cb(null, lastNewPath); //Ouput the same file.
951 }
952
953 } //End of no error on reading config
954
955 });
956}
957
958
959function getPlatform() {
960 var platform = process.platform;
961 if(verbose == true) console.log(process.platform);
962 var isWin = /^win/.test(platform);
963 if(verbose == true) console.log("IsWin=" + isWin);
964 if(isWin) {
965 if(process.arch == 'x64') {
966 return "win64";
967 } else {
968 return "win32";
969 }
970 } else {
971 if(platform == "darwin") {
972 return "mac";
973 } else {
974 return "unix";
975 }
976
977 }
978}
979
980
981
982
983
984
985
986function myExec(cmdLine, priority, cb) {
987
988 //Does a system exec command, or runs the command as a process with spawn, depending on priority.
989 //In the spawn case, we need a single command, and discrete arguments in the string, which are divided by spaces.
990 //Priority can be 'high', 'medium', 'low', 'glacial'.
991 // - 'high' in real-time response situations - nodejs included into RAM directly. The first word should be 'node',
992 // 2nd word is the path to the .js file, and then the following params are written into argv.
993 // - 'medium' - uses spawn, rather than exec, so a whole shell is not opened, without that additional overhead.
994 // - 'low' - uses exec, rather than spawn, so a whole shell is opened, but any system command can be used, including pipes, although be careful of the cross-platform limtations.
995 // - 'glacial' - will use the spawn command (no shell), but will insert our own shell, based on platform, with a background
996 // running option set. I.e. in Windows, this is 'cmd.exe /low /s /c', and in unix this is 'nice'
997 //It will call cb with (err, stdout, stderr)
998
999
1000 switch(priority) {
1001 case 'high':
1002 cmds = cmdLine.split(" ");
1003 var argv = [];
1004 var globalId = {};
1005 var scriptPath = "";
1006 if(cmds[0]) {
1007 //Should be 'node'
1008 }
1009
1010 if(cmds[1]) {
1011 scriptPath = cmds[1];
1012 }
1013
1014 if(cmds[2]) {
1015 //Params
1016 cmds.splice(0, 2);
1017 argv = cmds;
1018 } else {
1019 //No arguments
1020 argv = [];
1021 }
1022
1023 if(verbose == true) console.log("Global id:" + globalId + " scriptPath:" + scriptPath + " argv:" + JSON.stringify(argv));
1024
1025 var lib = require(scriptPath);
1026
1027 var mycb = cb;
1028 lib.medImage(argv, function(err, retVal) {
1029 if(err) {
1030 console.log("Error:" + err);
1031 mycb(err, "", "");
1032 } else {
1033 if(retVal) {
1034 if(!retVal.stdout) retVal.stdout = ""; //Ensure not undefined
1035 if(!retVal.stderr) retVal.stderr = "";
1036 mycb(null, retVal.stdout, retVal.stderr);
1037 } else {
1038 mycb(null,"","");
1039
1040 }
1041 }
1042
1043 });
1044
1045
1046 //Wait till finished - the add-on will callback via cb();
1047
1048
1049 break;
1050
1051
1052 case 'medium':
1053 //Break up the cmdLine into 'command', args[]
1054 cmds = cmdLine.split(" ");
1055 var args = [];
1056 var command = "";
1057 if(cmds[0]) {
1058 command = cmds[0];
1059 if(command == "node") {
1060 command = process.execPath; //Get the system node path
1061 }
1062 }
1063
1064 if(cmds[1]) {
1065 cmds.splice(0, 1);
1066 var args = cmds;
1067 }
1068
1069 var outputStdOut = "";
1070 var outputStdError = "";
1071
1072 var running = spawn(command, args);
1073
1074
1075 running.stdout.on('data', (data) => {
1076 if(verbose == true) console.log(data.toString());
1077 outputStdOut += data.toString();
1078
1079 });
1080
1081 running.stderr.on('data', (data) => {
1082 if(verbose == true) console.log(data.toString());
1083 outputStdError += data.toString();
1084 });
1085
1086 running.on('close', (code, signal) => {
1087 if(verbose == true) console.log(`Child process exited with code ${code} ` + outputStdOut);
1088 if(signal) {
1089 cb(code, outputStdOut, outputStdError);
1090 } else {
1091 cb(null, outputStdOut, outputStdError);
1092 }
1093 });
1094
1095 break;
1096
1097 case 'low':
1098 exec(cmdLine, { maxBuffer: 2000 * 1024 }, cb);
1099 break;
1100
1101 case 'glacial':
1102 //Get ready
1103
1104 var outputStdOut = "";
1105 var outputStdError = "";
1106
1107
1108 cmds = cmdLine.split(" ");
1109 var args = [];
1110 var command = "";
1111 if(cmds[0]) {
1112 args = cmds;
1113 }
1114
1115 //Now, based off platform, decide to run it slowly
1116 var platform = getPlatform();
1117 if((platform == "win32")||(platform == "win64")) {
1118 command = "cmd.exe"
1119 args.unshift('/low','/s','/c');
1120
1121 } else {
1122 //Unix/mac
1123 command = "nice";
1124 args.unshift('-10'); //This is a priority of 10, which is pretty low.
1125 }
1126
1127 var running = spawn(command, args);
1128
1129 running.stdout.on('data', (data) => {
1130 if(verbose == true) console.log(data.toString());
1131 outputStdOut += data.toString();
1132
1133 });
1134
1135 running.stderr.on('data', (data) => {
1136 if(verbose == true) console.log(data.toString());
1137 outputStdError += data.toString();
1138 });
1139
1140 running.on('close', (code, signal) => {
1141 if(signal) {
1142 cb(code, outputStdOut, outputStdError);
1143 } else {
1144 cb(null, outputStdOut, outputStdError);
1145 }
1146 });
1147
1148 break;
1149
1150 default:
1151
1152 //Do a full shell script, i.e. the same as 'low'
1153 exec(cmdLine, { maxBuffer: 2000 * 1024 }, cb);
1154
1155 break;
1156
1157 }
1158
1159 return;
1160}
1161
1162
1163function validateGlobalId(globalId) {
1164 //Returns the global id if the input globalId sting is valid, or 'false' if not
1165 //Format is 18 characters, ASCII. If any unusual punctuation characters then it is not correct.
1166 //We will give some leeway over the number of characters in case the pairing server has any changes, but it must
1167 //have more than 16 characters.
1168
1169 if(globalId.length > 16) {
1170 var format = /^[a-zA-Z0-9]+$/;
1171 if(format.test(globalId) === true) {
1172 return globalId;
1173 } else {
1174 return false;
1175 }
1176
1177 }
1178 return false;
1179
1180}
1181
1182
1183function runCommandPhotoWritten(runBlock, backupAtEnd, param1, param2, param3, cb) {
1184
1185 var cmdLine = runBlock.runProcess;
1186 cmdLine = cmdLine.replace(/parentdir/g, serverParentDir());
1187
1188 cmdLine = cmdLine.replace(/param1/g, param1);
1189 cmdLine = cmdLine.replace(/param2/g, param2);
1190 cmdLine = cmdLine.replace(/param3/g, param3);
1191 console.log("Running addon line: " + cmdLine);
1192
1193
1194
1195 myExec(cmdLine, runBlock.priority, function(err, stdout, stderr) {
1196 if (err) {
1197 // node couldn't execute the command
1198 console.log("There was a problem running the addon. Error:" + err + "\n\nStdout:" + stdout + "\n\nStderr:" + stderr);
1199 cb(err);
1200
1201 } else {
1202
1203 if((stdout)||(stderr)) {
1204 console.log("Stdout from command:" + stdout + "\n\nStderr" + stderr);
1205 }
1206
1207 //Potentially get any files that are new and need to be backed-up
1208 //to the config-specified folders. This should be before echoed to stdout as 'backupFiles:' near the end of
1209 //the script output.
1210 backupFilesStr = "backupFiles:";
1211 var backupFiles = "";
1212 var backStart = stdout.lastIndexOf(backupFilesStr);
1213
1214
1215 //Special case of the original file is renamed
1216 backupFileRenamedStr = "backupFileRenamed:";
1217 if(stdout.lastIndexOf(backupFileRenamedStr) > -1) {
1218 backStart = stdout.lastIndexOf(backupFileRenamedStr);
1219 normalBackup = false;
1220 }
1221
1222 returnparams = "returnParams:";
1223 var returnStart = stdout.lastIndexOf(returnparams);
1224
1225
1226 //Backups will take place asyncronously, in the background
1227 if(backStart > -1) {
1228
1229 //Yes string exists
1230 if(returnStart > -1) {
1231 //Go to the start of the returnParams string
1232 var backLen = returnStart - backStart;
1233 backupFiles = stdout.substr(backStart, backLen);
1234 } else {
1235 //Go to the end of the file otherwise
1236 backupFiles = stdout.substr(backStart);
1237
1238 }
1239
1240
1241 console.log("Backing up requested of " + backupFiles);
1242 backupFiles = backupFiles.replace(backupFilesStr,""); //remove locator
1243 backupFiles = backupFiles.replace(backupFileRenamedStr,""); //remove locator
1244 backupFiles = backupFiles.trim(); //remove newlines at the end
1245 if(verbose == true) console.log("Backing up string in server:" + backupFiles);
1246 var backupArray = backupFiles.split(";"); //Should be semi-colon split
1247 if(verbose == true) console.log("Backing up array:" + JSON.stringify(backupArray));
1248
1249 //Now loop through and back-up each of these files.
1250 for(var cnt = 0; cnt<backupArray.length; cnt++) {
1251
1252 // thisPath: full path of the file to be backed up from the root file system
1253 // outhashdir: the directory path of the file relative to the root photos dir /photos. But if blank,
1254 // this is included in the finalFileName below.
1255 // finalFileName: the photo or other file name itself e.g. photo-01-09-17-12-54-56.jpgvar thisPath = path.dirname(backupArray[cnt]);
1256 var photoParentDir = normalizeInclWinNetworks(serverParentDir() + outdirPhotos);
1257 if(verbose == true) console.log("Backing up requested files from script");
1258 if(verbose == true) console.log("photoParentDir=" + photoParentDir);
1259 var finalFileName = normalizeInclWinNetworks(backupArray[cnt]);
1260 finalFileName = finalFileName.replace(photoParentDir,"").trim(); //Remove the photo's directory from the filename
1261 if(verbose == true) console.log("finalFileName=" + finalFileName);
1262 var thisPath = normalizeInclWinNetworks(backupArray[cnt].trim());
1263 if(verbose == true) console.log("thisPath=" + thisPath);
1264 backupAtEnd.push({ "thisPath": thisPath,
1265 "finalFileName": finalFileName });
1266
1267 }
1268 }
1269
1270
1271
1272 reloadConfig = "reloadConfig:true";
1273 if(stdout.lastIndexOf(reloadConfig) > -1) {
1274 checkConfigCurrent(null, function() {
1275 //This is run async - refresh the config in the background.
1276
1277 //And reload the header
1278 readHTMLHeader(function(err) {
1279 if(err) {
1280 console.log(err);
1281 }
1282 });
1283
1284 });
1285 }
1286
1287
1288 // the *entire* stdout and stderr (buffered)
1289 if(verbose == true) console.log(`stdout: ${stdout}`);
1290 if(verbose == true) console.log(`stderr: ${stderr}`);
1291
1292 cb(null);
1293 }
1294
1295 });
1296}
1297
1298
1299
1300//Handle 3rd party and our own add-ons
1301function addOns(eventType, cb, param1, param2, param3)
1302{
1303 //Read in any add-ons that exist in the config?, or in the 'addons' folder.
1304
1305 //Read in the config file
1306 fs.readFile(addonsConfigFile, function read(err, data) {
1307 if (err) {
1308 console.log("Warning: Error reading addons config file: " + err);
1309 } else {
1310 if(data) {
1311 var content = JSON.parse(data);
1312 } else {
1313 //There was an error reading the data. Use the existing global var.
1314 var content = global.globalConfig;
1315 }
1316
1317 if(verbose == true) {
1318 console.log("Got content of addons config");
1319 }
1320
1321 switch(eventType)
1322 {
1323 case "photoWritten":
1324 //param1 is the full path of the new photo in the home directory (not the backed up copy)
1325 if(content.events.photoWritten) {
1326
1327 var normalBackup = true;
1328 var evs = content.events.photoWritten;
1329 var backupAtEnd = [];
1330
1331
1332 console.log("PARAM1:" + param1);
1333
1334
1335 //Asyncronously call each item, but in sequential order. This means the process could
1336 //be doing something processor intensive without holding up the main server, but still allow
1337 //each add-on to potentially process sequentially. This could be useful for chaining image resizing,
1338 //image processing add-ons together in the correct order.
1339 async.eachOfSeries(evs,
1340 // 2nd param is the function that each item is passed to
1341 function(runBlock, cnt, callback){
1342
1343
1344
1345 //for(var cnt = 0; cnt< evs.length; cnt++) {
1346 if(runBlock.active == true) {
1347 //Run the command off the system
1348
1349 //First check if we need to backup the file into the target before running with
1350 //the target version.
1351 if((runBlock.useTargetFolderFile)&&(runBlock.useTargetFolderFile == true)) {
1352
1353
1354
1355 var cmdLine = runBlock.runProcess;
1356 cmdLine = cmdLine.replace(/parentdir/g, serverParentDir());
1357
1358 cmdLine = cmdLine.replace(/param1/g, param1);
1359 cmdLine = cmdLine.replace(/param2/g, param2);
1360 cmdLine = cmdLine.replace(/param3/g, param3);
1361
1362 var photoParentDir = normalizeInclWinNetworks(serverParentDir() + outdirPhotos);
1363 if(verbose == true) console.log("Backing up requested files from script");
1364 if(verbose == true) console.log("photoParentDir=" + photoParentDir);
1365 var finalFileName = normalizeInclWinNetworks(param1);
1366 finalFileName = finalFileName.replace(photoParentDir,"").trim(); //Remove the photo's directory from the filename
1367 if(verbose == true) console.log("finalFileName=" + finalFileName);
1368 var thisPath = normalizeInclWinNetworks(param1);
1369 if(verbose == true) console.log("thisPath=" + thisPath);
1370
1371
1372 //Start with a backup of the file in param1, and replace that for the photoWritten command param1
1373 backupFile(thisPath, "", finalFileName, { }, function(err, newPath) {
1374
1375 if(err) {
1376 callback(err, null);
1377 } else {
1378
1379 runCommandPhotoWritten(runBlock, backupAtEnd, newPath, param2, param3, function(err) {
1380 if(err) {
1381 callback(err);
1382 } else {
1383 //Note: we must call back here once the system command has finished. This allows us
1384 //to go on to the next command, sequentially, though each command is run async
1385 callback(null);
1386 }
1387 });
1388 }
1389
1390 });
1391 } else {
1392
1393 //Do a normal processing of the content from within the transition folder (e.g. C:\MedImage\photos on Windows)
1394 runCommandPhotoWritten(runBlock, backupAtEnd, param1, param2, param3, function(err) {
1395 if(err) {
1396 callback(err);
1397 } else {
1398 //Note: we must call back here once the system command has finished. This allows us
1399 //to go on to the next command, sequentially, though each command is run async
1400 callback(null);
1401 }
1402
1403 });
1404 }
1405 } else { //End of is active check
1406 //We must callback even if it is not active, to go to the next option
1407 callback(null);
1408 }
1409
1410 }, //End of async eachOf single item
1411 function(err){
1412 // All tasks are done now
1413 if(err) {
1414 console.log('ERR:' + err);
1415 } else {
1416 console.log('Completed all photoWritten events!');
1417
1418 //Carry out the backups after all commands have finished
1419 for(var cnt = 0; cnt < backupAtEnd.length; cnt++) {
1420 backupFile(backupAtEnd[cnt].thisPath, "", backupAtEnd[cnt].finalFileName, { });
1421 }
1422
1423
1424
1425 cb(null, normalBackup);
1426 }
1427 }
1428 ); //End of async eachOf all items
1429
1430 }
1431
1432 break;
1433
1434
1435 case "fileWritten":
1436 //Only difference here is it is a generic file that has been written rather than a photo .jpg file
1437 //param1 is the full path of the new file in the home directory (not the backed up copy)
1438 if(content.events.fileWritten) {
1439
1440 var normalBackup = true;
1441 var evs = content.events.fileWritten;
1442 var backupAtEnd = [];
1443
1444
1445 console.log("PARAM1:" + param1);
1446
1447
1448 //Asyncronously call each item, but in sequential order. This means the process could
1449 //be doing something processor intensive without holding up the main server, but still allow
1450 //each add-on to potentially process sequentially. This could be useful for chaining image resizing,
1451 //image processing add-ons together in the correct order.
1452 async.eachOfSeries(evs,
1453 // 2nd param is the function that each item is passed to
1454 function(runBlock, cnt, callback){
1455
1456
1457
1458 //for(var cnt = 0; cnt< evs.length; cnt++) {
1459 if(runBlock.active == true) {
1460 //Run the command off the system
1461
1462 //First check if we need to backup the file into the target before running with
1463 //the target version.
1464 if((runBlock.useTargetFolderFile)&&(runBlock.useTargetFolderFile == true)) {
1465
1466
1467
1468 var cmdLine = runBlock.runProcess;
1469 cmdLine = cmdLine.replace(/parentdir/g, serverParentDir());
1470
1471 cmdLine = cmdLine.replace(/param1/g, param1);
1472 cmdLine = cmdLine.replace(/param2/g, param2);
1473 cmdLine = cmdLine.replace(/param3/g, param3);
1474
1475 var photoParentDir = normalizeInclWinNetworks(serverParentDir() + outdirPhotos);
1476 if(verbose == true) console.log("Backing up requested files from script");
1477 if(verbose == true) console.log("photoParentDir=" + photoParentDir);
1478 var finalFileName = normalizeInclWinNetworks(param1);
1479 finalFileName = finalFileName.replace(photoParentDir,"").trim(); //Remove the photo's directory from the filename
1480 if(verbose == true) console.log("finalFileName=" + finalFileName);
1481 var thisPath = normalizeInclWinNetworks(param1);
1482 if(verbose == true) console.log("thisPath=" + thisPath);
1483
1484
1485 //Start with a backup of the file in param1, and replace that for the photoWritten command param1
1486 backupFile(thisPath, "", finalFileName, { }, function(err, newPath) {
1487
1488 if(err) {
1489 callback(err, null);
1490 } else {
1491
1492 runCommandPhotoWritten(runBlock, backupAtEnd, newPath, param2, param3, function(err) {
1493 if(err) {
1494 callback(err);
1495 } else {
1496 //Note: we must call back here once the system command has finished. This allows us
1497 //to go on to the next command, sequentially, though each command is run async
1498 callback(null);
1499 }
1500 });
1501 }
1502
1503 });
1504 } else {
1505
1506 //Do a normal processing of the content from within the transition folder (e.g. C:\MedImage\photos on Windows)
1507 runCommandPhotoWritten(runBlock, backupAtEnd, param1, param2, param3, function(err) {
1508 if(err) {
1509 callback(err);
1510 } else {
1511 //Note: we must call back here once the system command has finished. This allows us
1512 //to go on to the next command, sequentially, though each command is run async
1513 callback(null);
1514 }
1515
1516 });
1517 }
1518 } else { //End of is active check
1519 //We must callback even if it is not active, to go to the next option
1520 callback(null);
1521 }
1522
1523 }, //End of async eachOf single item
1524 function(err){
1525 // All tasks are done now
1526 if(err) {
1527 console.log('ERR:' + err);
1528 } else {
1529 console.log('Completed all fileWritten events!');
1530
1531 //Carry out the backups after all commands have finished
1532 for(var cnt = 0; cnt < backupAtEnd.length; cnt++) {
1533 backupFile(backupAtEnd[cnt].thisPath, "", backupAtEnd[cnt].finalFileName, { });
1534 }
1535
1536
1537
1538 cb(null, normalBackup);
1539 }
1540 }
1541 ); //End of async eachOf all items
1542
1543 }
1544
1545 break;
1546
1547
1548 case "urlRequest":
1549 if(verbose == true) console.log("URL request of " + param1);
1550
1551 if(content.events && content.events.urlRequest) {
1552
1553 var evs = content.events.urlRequest;
1554 for(var cnt = 0; cnt< evs.length; cnt++) {
1555
1556 //Check if script in param1 starts with the scriptURLName
1557 var scriptChk = evs[cnt].scriptURLName;
1558 if(verbose == true) console.log("Checking against:" + scriptChk);
1559
1560 if(param1.substr(0,scriptChk.length) == scriptChk) {
1561 if(evs[cnt].active == true) {
1562 //Run the command off the system - passing in the URL query string directly as a single url encoded string
1563 var cmdLine = evs[cnt].runProcess;
1564 cmdLine = cmdLine.replace(/parentdir/g, serverParentDir());
1565
1566 param1 = param1.replace("%23", "#"); //allow hashes to be sent in the url - reverse code them. TODO Would this affect e.g. %2345 ?
1567 param1 = param1.replace("%20", " "); //allow spaces to be sent in the url - reverse code them. TODO Would this affect e.g. %2345 ?
1568
1569 var queryString = encodeURIComponent(param1.replace(scriptChk + "?",""));
1570
1571
1572 cmdLine = cmdLine.replace(/param1/g, queryString);
1573 if(verbose == true) console.log("Running addon line: " + cmdLine);
1574
1575 if(evs[cnt].waitForRequestFinish) {
1576 //Forward on to this page afterwards
1577 var waitForIt = evs[cnt].waitForRequestFinish;
1578 }
1579
1580 //Pass through the priority
1581 myExec(cmdLine, evs[cnt].priority, function(err, stdout, stderr) {
1582 if (err) {
1583 // node couldn't execute the command
1584 console.log("There was a problem running the addon. Error:" + err + "\n\nStdout:" + stdout + "\n\nStderr:" + stderr);
1585 return;
1586 }
1587
1588 // the *entire* stdout and stderr (buffered)
1589 if(stdout) {
1590 console.log(`stdout: ${stdout}`);
1591 }
1592 if(stderr) {
1593 console.log(`stderr: ${stderr}`);
1594 }
1595
1596
1597 if(waitForIt) {
1598 //The script has run, now parse for the return parameters
1599 returnparams = "returnParams:";
1600 var params = "";
1601 if(verbose == true) console.log("Stdout:" + stdout);
1602 var returnStart = stdout.lastIndexOf(returnparams);
1603
1604 reloadConfig = "reloadConfig:true";
1605 if(stdout.lastIndexOf(reloadConfig) > -1) {
1606 checkConfigCurrent(null, function() {
1607 //This is run async - refresh the config in the background.
1608
1609 //And reload the header
1610 readHTMLHeader(function(err) {
1611 if(err) {
1612 console.log(err);
1613 }
1614 });
1615
1616 });
1617 }
1618
1619
1620 if(returnStart > -1) {
1621 //Yes return params exists
1622 params = stdout.substr(returnStart);
1623 params = params.replace(returnparams + "?",""); //remove questions
1624 params = params.replace(returnparams,""); //remove the locator
1625 params = params.trim(); //remove newlines at the end
1626 if(verbose == true) console.log("Params returned=" + params);
1627 }
1628
1629
1630 //But also potentially get any files that are new and need to be backed-up
1631 //to the config-specified folders. This should be before the returnParams
1632 backupFilesStr = "backupFiles:";
1633 var backupFiles = "";
1634 var backStart = stdout.lastIndexOf(backupFilesStr);
1635
1636 if(backStart > -1) {
1637 if(verbose == true) console.log("Backing up requested");
1638
1639 //Yes string exists
1640 if(returnStart > -1) {
1641 //Go to the start of the returnParams string
1642 var backLen = returnStart - backStart;
1643 backupFiles = stdout.substr(backStart, backLen);
1644 } else {
1645 //Go to the end of the file otherwise
1646 backupFiles = stdout.substr(backStart);
1647
1648 }
1649 backupFiles = backupFiles.replace(backupFilesStr,""); //remove locator
1650 backupFiles = backupFiles.trim(); //remove newlines at the end
1651 if(verbose == true) console.log("Backing up string in server:" + backupFiles);
1652 var backupArray = backupFiles.split(";"); //Should be semi-colon split
1653 if(verbose == true) console.log("Backing up array:" + JSON.stringify(backupArray));
1654
1655 //Now loop through and back-up each of these files.
1656 for(var cnt = 0; cnt<backupArray.length; cnt++) {
1657
1658 // thisPath: full path of the file to be backed up from the root file system
1659 // outhashdir: the directory path of the file relative to the root photos dir /photos. But if blank,
1660 // this is included in the finalFileName below.
1661 // finalFileName: the photo or other file name itself e.g. photo-01-09-17-12-54-56.jpgvar thisPath = path.dirname(backupArray[cnt]);
1662 var photoParentDir = normalizeInclWinNetworks(serverParentDir() + outdirPhotos);
1663 if(verbose == true) console.log("Backing up requested files from script");
1664 if(verbose == true) console.log("photoParentDir=" + photoParentDir);
1665 var finalFileName = normalizeInclWinNetworks(backupArray[cnt]);
1666 finalFileName = finalFileName.replace(photoParentDir,""); //Remove the photo's directory from the filename
1667 if(verbose == true) console.log("finalFileName=" + finalFileName);
1668 var thisPath = normalizeInclWinNetworks(backupArray[cnt]);
1669 if(verbose == true) console.log("thisPath=" + thisPath);
1670 backupFile(thisPath, "", finalFileName, { });
1671 }
1672 }
1673
1674
1675 cb(waitForIt, params);
1676 } else {
1677 //There is the option of providing a raw file from the photo directory here. Include a blank ""
1678 returnPhotoFile = "returnPhotoFile:";
1679 var params = "";
1680 if(verbose == true) console.log("Stdout:" + stdout);
1681 var returnStart = stdout.lastIndexOf(returnPhotoFile);
1682
1683 if(returnStart > -1) {
1684
1685 params = stdout.substr(returnStart);
1686 params = params.replace("returnPhotoFile:?",""); //remove questions
1687 params = params.replace("returnPhotoFile:",""); //remove questions
1688 params = params.trim(); //remove newlines at the end
1689 if(verbose == true) console.log("Photo file returned=" + params);
1690
1691
1692 cb(params, null); //This will actually serve up this file.
1693
1694 }
1695
1696
1697
1698 }
1699
1700 });
1701
1702 if((evs[cnt].waitForRequestFinish)||(evs[cnt].waitForRequestFinish == "")) {
1703 //Waiting for completion
1704 } else {
1705
1706 if(verbose == true) console.log("Checking after request:" + evs[cnt].afterRequest);
1707 if(evs[cnt].afterRequest) {
1708 //Forward on to this page afterwards
1709 cb(evs[cnt].afterRequest);
1710 } else {
1711 cb("");
1712 }
1713 }
1714 }
1715 }
1716
1717 }
1718
1719 }
1720
1721
1722 break;
1723
1724
1725 case "displayMenu":
1726 //TODO: This is another example: display additional items on the main server menu
1727 break;
1728
1729 };
1730 }
1731 });
1732
1733 return;
1734
1735}
1736
1737
1738function trimChar(string, charToRemove) {
1739 while(string.substring(0,1) == charToRemove) {
1740 string = string.substring(1);
1741 }
1742
1743 while(string.slice(-1) == charToRemove) {
1744 string = string.slice(0, -1);
1745 }
1746
1747 return string;
1748}
1749
1750function escapeRegExp(str) {
1751 return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
1752}
1753
1754function replaceAll(str, find, replace) {
1755 return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
1756}
1757
1758
1759function httpHttpsCreateServer(options) {
1760
1761 //Get the common HTML header here now
1762 readHTMLHeader(function(err) {
1763 if(err) {
1764 console.log(err);
1765 } else {
1766
1767 if(httpsFlag == true) {
1768 console.log("Starting https server.");
1769 https.createServer(options, handleServer).listen(listenPort);
1770
1771
1772 } else {
1773 console.log("Starting http server.");
1774 http.createServer(handleServer).listen(listenPort);
1775 }
1776 }
1777 });
1778
1779}
1780
1781
1782
1783
1784function getFileFromUserStr(inFile)
1785{
1786 if(verbose == true) console.log("getFileFromUserStr:" + inFile);
1787 var outFile = inFile;
1788
1789 outFile = outFile.replace('.jpg',''); //Remove jpg from filename
1790 outFile = outFile.replace('.jpeg',''); //Remove jpg from filename
1791 outFile = replaceAll(outFile, "..", ""); //Remove nasty chars
1792
1793 var re = new RegExp("[^" + allowedChars + "]", "g");
1794 outFile = outFile.replace(re, ""); //Only keep usable chars
1795
1796 outFile = trimChar(outFile, '/'); //Allowed directory slashes within the filename, but otherwise nothing around sides
1797 outFile = trimChar(outFile,'\\');
1798
1799
1800
1801 var words = outFile.split('-');
1802
1803
1804 var finalFileName = "";
1805 var outhashdir = "";
1806 //Array of distinct words
1807 for(var cnt = 0; cnt< words.length; cnt++) {
1808 if(words[cnt].charAt(0) == '#') {
1809 var getDir = words[cnt].replace('#','');
1810
1811 //Do some trimming of this directory name
1812 getDir = trimChar(getDir, '/');
1813 getDir = trimChar(getDir, '\\');
1814
1815
1816 if(verbose == true) console.log('Comparing ' + getDir + ' with ' + globalId);
1817 if(getDir != globalId) {
1818 outhashdir = outhashdir + '/' + getDir;
1819 if(verbose == true) console.log('OutHashDir:' + outhashdir);
1820 }
1821 } else {
1822 //Do some odd char trimming of this
1823 var thisWord = words[cnt];
1824 thisWord = trimChar(thisWord, '/');
1825 thisWord = trimChar(thisWord, '\\');
1826
1827 //Start building back filename with hyphens between words
1828 if(finalFileName.length > 0) {
1829 finalFileName = finalFileName + '-';
1830 }
1831 finalFileName = finalFileName + thisWord;
1832 }
1833 } //end of loop
1834
1835 finalFileName = finalFileName + '.jpg';
1836 return normalizeInclWinNetworks(outhashdir + '/' + finalFileName);
1837}
1838
1839
1840function handleServer(_req, _res) {
1841
1842 var req = _req;
1843 var res = _res;
1844 var body = [];
1845
1846
1847
1848 if (req.url === '/api/photo' && req.method === 'POST') {
1849 // parse a file upload
1850
1851 var form = new multiparty.Form({maxFilesSize: maxUploadSize});
1852
1853
1854 form.parse(req, function(err, fields, files) {
1855 //Process filename of uploaded file, then move into the server's directory, and finally
1856 //copy the files into any backup directories
1857
1858 if(err) {
1859 console.log("Error uploading file " + JSON.stringify(err))
1860
1861 res.writeHead(400, {'content-type': 'text/plain'});
1862 res.end("Invalid request: " + err.message);
1863 return;
1864
1865 } else {
1866
1867 //The standard outdir is the drive from the current server script
1868 var parentDir = serverParentDir();
1869 if(verbose == true) console.log("This drive:" + parentDir);
1870 var outdir = parentDir + outdirPhotos;
1871
1872 if(verbose == true) console.log('Outdir:' + outdir);
1873
1874
1875 if(verbose == true) console.log('Files ' + JSON.stringify(files, null, 4));
1876 //Use original filename for name
1877 if(files && files.file1 && files.file1[0]) {
1878
1879 //Uploaded file exists
1880 //Confirm is a valid .jpg file
1881
1882
1883 var buffer = readChunk.sync(files.file1[0].path, 0, 12);
1884 var fileObj = fileType(buffer); //Display the file type
1885 if((!fileObj)||(!fileObj.mime) || (fileObj.mime != 'image/jpeg')) {
1886 //Not a photo file - check if it is in our allowed types
1887 var ext = null;
1888
1889 if(fileObj) {
1890 for(var type = 0; type < allowedTypes.length; type++) {
1891 if(fileObj.mime === allowedTypes[type].mime) {
1892 //This is an allowed type
1893 var ext = allowedTypes[type].extension;
1894 var ext2 = ext; //The same for the 2nd one to replace
1895 }
1896
1897 }
1898 }
1899
1900 if(!ext) {
1901 //No file exists
1902 console.log("Error uploading file. Only certain files (e.g. jpg) are allowed.");
1903 res.statusCode = 400; //Error during transmission - tell the app about it
1904 res.end();
1905 return;
1906 }
1907 } else {
1908 var ext = ".jpg";
1909 var ext2 = ".jpeg";
1910
1911 }
1912
1913
1914
1915
1916
1917 var title = files.file1[0].originalFilename;
1918
1919 res.writeHead(200, {'content-type': 'text/plain'});
1920 var returnStr = 'Received upload successfully!';
1921 if(verbose == true) returnStr += 'Check ' + normalizeInclWinNetworks(parentDir + outdirPhotos) + ' for your image.';
1922 res.write(returnStr + '\n\n');
1923 res.end();
1924
1925
1926 //Copy file to eg. c:/snapvolt/photos
1927 var outFile = title;
1928 outFile = outFile.replace(ext,''); //Remove jpg from filename
1929 outFile = outFile.replace(ext2,''); //Remove jpeg from filename
1930 outFile = replaceAll(outFile, "..", ""); //Remove nasty chars
1931
1932 var re = new RegExp("[^" + allowedChars + "]", "g");
1933 outFile = outFile.replace(re, ""); //Only keep usable chars
1934
1935
1936 outFile = trimChar(outFile, '/'); //Allowed directory slashes within the filename, but otherwise nothing around sides
1937 outFile = trimChar(outFile,'\\');
1938
1939
1940
1941 var words = outFile.split('-');
1942
1943 var finalFileName = "";
1944 var outhashdir = "";
1945 //Array of distinct words
1946 for(var cnt = 0; cnt< words.length; cnt++) {
1947 if(words[cnt].charAt(0) == '#') {
1948 var getDir = words[cnt].replace('#','');
1949
1950 //Do some trimming of this directory name
1951 getDir = trimChar(getDir, '/');
1952 getDir = trimChar(getDir, '\\');
1953
1954
1955 if(verbose == true) console.log('Comparing ' + getDir + ' with ' + globalId);
1956 if(getDir != globalId) {
1957 outhashdir = outhashdir + '/' + getDir;
1958 if(verbose == true) console.log('OutHashDir:' + outhashdir);
1959 }
1960 } else {
1961 //Do some odd char trimming of this
1962 var thisWord = words[cnt];
1963 thisWord = trimChar(thisWord, '/');
1964 thisWord = trimChar(thisWord, '\\');
1965
1966 //Start building back filename with hyphens between words
1967 if(finalFileName.length > 0) {
1968 finalFileName = finalFileName + '-';
1969 }
1970 finalFileName = finalFileName + thisWord;
1971 }
1972 } //end of loop
1973
1974 //Check the directory exists, and create
1975
1976 if (!fs.existsSync(normalizeInclWinNetworks(parentDir + outdirPhotos))){
1977 if(verbose == true) console.log('Creating dir:' + normalizeInclWinNetworks(parentDir + outdirPhotos));
1978
1979 fsExtra.mkdirsSync(normalizeInclWinNetworks(parentDir + outdirPhotos));
1980 if(verbose == true) console.log('Created OK dir:' + normalizeInclWinNetworks(parentDir + outdirPhotos));
1981
1982 }
1983
1984 //Create the final hash outdir
1985 outdir = parentDir + outdirPhotos + outhashdir;
1986 if (!fs.existsSync(normalizeInclWinNetworks(outdir))){
1987 if(verbose == true) console.log('Creating dir:' + normalizeInclWinNetworks(outdir));
1988 fsExtra.mkdirsSync(normalizeInclWinNetworks(outdir));
1989 if(verbose == true) console.log('Created OK');
1990
1991 }
1992
1993
1994
1995 finalFileName = finalFileName + ext;
1996
1997 //Move the file into the standard location of this server
1998 var fullPath = outdir + '/' + finalFileName;
1999 if(verbose == true) console.log("Moving " + files.file1[0].path + " to " + fullPath);
2000 mv(files.file1[0].path, fullPath, {mkdirp: true}, function(err) { //path.normalize(
2001 // done. it tried fs.rename first, and then falls back to
2002 // piping the source file to the dest file and then unlinking
2003 // the source file.
2004 if(err) {
2005 console.log(err);
2006
2007 } else {
2008 console.log('\n' + finalFileName + ' file uploaded');
2009
2010 //Ensure no admin restictions on Windows
2011 ensurePhotoReadableWindows(fullPath);
2012
2013
2014
2015
2016
2017
2018
2019 //Run the addons events on this new photo
2020 if(ext === ".jpg") {
2021 var event = "photoWritten";
2022 } else {
2023 var event = "fileWritten";
2024 }
2025
2026
2027
2028 addOns(event, function(err, normalBackup) {
2029 if(err) {
2030 console.log("Error writing file:" + err);
2031 } else {
2032 if(verbose == true) console.log("Add-on completed running");
2033
2034 }
2035
2036 if(normalBackup == true) {
2037 //Now we have finished processing the file via the addons,
2038 //backup the standard files if 'normal' is the case
2039
2040 //Now copy to any other backup directories
2041 if(verbose == true) console.log("Backups:");
2042 var thisPath = fullPath;
2043
2044 //Now backup to any directories specified in the config
2045 backupFile(thisPath, outhashdir, finalFileName, { });
2046 }
2047 }, fullPath);
2048
2049
2050
2051
2052
2053 }
2054 });
2055 } else { //End of file exists
2056 //No file exists
2057 console.log("Error uploading file. No file on server.");
2058 res.statusCode = 400; //Error during transmission - tell the app about it
2059 res.end();
2060 return;
2061 }
2062
2063 } //End of form no parse error
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073 }); //End of form.parse
2074
2075 return;
2076
2077 } else { //end of api upload
2078
2079 //Start ordinary error handling
2080 req.on('error', function(err) {
2081 // This prints the error message and stack trace to `stderr`.
2082 console.error(err.stack);
2083
2084 res.statusCode = 400; //Error during transmission - tell the app about it
2085 res.end();
2086 });
2087
2088 req.on('data', function(chunk) {
2089 body.push(chunk);
2090 });
2091
2092 req.on('end', function() {
2093
2094 if(flapSimulation == true) {
2095 if(flapState == true) {
2096 flapState = false;
2097
2098 return; //Exit here prematurely to simulate flapping
2099 } else {
2100 flapState = true;
2101 }
2102
2103 }
2104
2105
2106 //A get request to pull from the server
2107 // show a file upload form
2108 var url = req.url;
2109 if((url == '/') || (url == "") || (url == "/index.html")) {
2110 url = "/pages/index.html";
2111
2112 //The homepage has a custom string of the number of bytes transferred
2113 var formattedBytes = formatBytes(bytesTransferred, 1);
2114 var customString = { "CUSTOMSTRING": formattedBytes };
2115
2116 if(allowGettingRemotePhotos == false) {
2117 //If we can't sync, don't try - switch off the buttons
2118 customString.SYNCING = "false";
2119
2120 } else {
2121 customString.SYNCING = "true";
2122 }
2123
2124
2125 } else {
2126 //Mainly we don't have any custom strings
2127 var customString = null;
2128 }
2129
2130 var removeAfterwards = false;
2131 var read = '/read/';
2132 var pair = '/pair';
2133 var check = '/check=';
2134 var addonreq = '/addon/';
2135
2136 if(verbose == true) console.log("Url requested:" + url);
2137
2138 if(url.substr(0,pair.length) == pair) {
2139 //Do a get request from the known aj server
2140 //for a new pairing guid
2141 var fullPairingUrl = pairingURL;
2142
2143 var queryString = url.substr(pair.length);
2144
2145
2146 checkConfigCurrent(null, function() {
2147
2148 //Split the url into separate vars for a post below
2149 var data = {};
2150 var vars = queryString.split('&');
2151 for (var i = 0; i < vars.length; i++) {
2152 var pair = vars[i].split('=');
2153 data[pair[0]] = decodeURIComponent(pair[1]);
2154
2155 }
2156
2157
2158 if(globalId != "") {
2159 //We already know the global id - use it to update the passcode only
2160 data.guid = globalId;
2161
2162 }
2163
2164 if(queryString) {
2165 fullPairingUrl = fullPairingUrl + queryString;
2166
2167 }
2168 console.log("Request for pairing:" + fullPairingUrl);
2169
2170 var options = {};
2171 if(webProxy) {
2172 options.proxy = webProxy;
2173 }
2174 options.follow = 1; //Allow redirection once to the secure page.
2175
2176
2177 needle.post(fullPairingUrl, data, options, function(error, response) {
2178 if(error) {
2179 console.log("Pairing error:" + error);
2180 var replace = {
2181 "CUSTOMCODE": "[Pairing error: " + error + "]",
2182 "CUSTOMCOUNTRY": "[Unknown]",
2183 "STANDARDHEADER": htmlHeaderCode
2184 };
2185
2186 //Write full proxy to config file
2187 checkConfigCurrent(readProx, function() {
2188
2189
2190 //Display passcode to user
2191 var outdir = __dirname + "/../public/pages/passcode.html";
2192 serveUpFile(outdir, null, res, false, replace);
2193 return;
2194 });
2195 } else {
2196
2197 if (response.statusCode == 200) {
2198 console.log(response.body);
2199
2200 var codes = response.body.split(" ");
2201 var passcode = codes[0];
2202 newGlobalId = validateGlobalId(codes[1]);
2203 if(newGlobalId !== false) {
2204 globalId = codes[1];
2205 var guid = globalId;
2206 var proxyServer = codes[2].replace("\n", "");
2207 if(codes[3]) {
2208 var country = decodeURIComponent(codes[3].replace("\n", ""));
2209 } else {
2210 //Defaults to an unknown country.
2211 var country = "[Unknown]";
2212 }
2213
2214 var readProx = proxyServer + "/read/" + guid;
2215 console.log("Proxy set to:" + readProx);
2216 } else {
2217 passcode = "----";
2218 var country = "[Sorry there was a problem contacting the pairing server. Please try again, or check 'Service Status'.]";
2219 }
2220
2221
2222
2223 var replace = {
2224 "CUSTOMCODE": passcode,
2225 "CUSTOMCOUNTRY": country,
2226 "STANDARDHEADER": htmlHeaderCode
2227 };
2228
2229 //Write full proxy to config file
2230 checkConfigCurrent(readProx, function() {
2231
2232
2233 //Display passcode to user
2234 var outdir = __dirname + "/../public/pages/passcode.html";
2235 serveUpFile(outdir, null, res, false, replace);
2236 return;
2237 });
2238
2239
2240 } else {
2241 //No connection available
2242 console.log("Pairing error:" + error + " Status: " + response.statusCode);
2243 var replace = {
2244 "CUSTOMCODE": "[Pairing error: " + error + " Status: " + response.statusCode + "]",
2245 "CUSTOMCOUNTRY": "[Unknown]",
2246 "STANDARDHEADER": htmlHeaderCode
2247 };
2248
2249 //Write full proxy to config file
2250 checkConfigCurrent(readProx, function() {
2251
2252
2253 //Display passcode to user
2254 var outdir = __dirname + "/../public/pages/passcode.html";
2255 serveUpFile(outdir, null, res, false, replace);
2256 return;
2257 });
2258
2259 }
2260 }
2261 });
2262 });
2263
2264
2265 } else { //end of pair
2266
2267
2268 if(url.substr(0,read.length) == read) {
2269
2270 if(allowPhotosLeaving != true) {
2271
2272 console.log("Read request detected (blocked by config.json): " + url);
2273 res.writeHead(400, {'content-type': 'text/html'});
2274 res.end("Sorry, you cannot read from this server. Please check the server's config.json.");
2275 return;
2276 }
2277
2278
2279 //Get uploaded photos from coded subdir
2280 var codeDir = url.substr(read.length);
2281 var parentDir = serverParentDir();
2282 if(verbose == true) console.log("This drive:" + parentDir);
2283 if(verbose == true) console.log("Coded directory:" + codeDir);
2284
2285 if(codeDir.length <= 0) {
2286 console.log("Cannot read without a directory");
2287 return;
2288 }
2289
2290 var outdir = normalizeInclWinNetworks(parentDir + outdirPhotos + '/' + codeDir);
2291 var compareWith = normalizeInclWinNetworks(parentDir + outdirPhotos);
2292
2293 if(verbose == true) console.log("Output directory to scan " + outdir + ". Must include:" + compareWith);
2294 //For security purposes the path must include the parentDir and outdiePhotos in a complete form
2295 //ie. be subdirectories. Otherwise a ../../ would allow deletion of an internal file
2296 if(outdir.indexOf(compareWith) > -1) {
2297
2298
2299 //Get first file in the directory list
2300 fileWalk(outdir, function(outfile, mime, cnt) {
2301
2302 if(outfile) {
2303 //Get outfile - compareWith
2304 outfile = normalizeInclWinNetworks(outfile);
2305 var localFileName = outfile.replace(compareWith, "");
2306 if(verbose == true) console.log("Local file to download via proxy as:" + localFileName);
2307 if(verbose == true) console.log("About to download (eventually delete): " + outfile);
2308
2309 if(req.method === "HEAD") {
2310 //Serve the header only
2311 res.writeHead(200, {'content-type': mime, 'file-name': localFileName });
2312 res.end();
2313
2314 } else {
2315 //Now serve the full file
2316 serveUpFile(outfile,localFileName, res, true);
2317 }
2318
2319 } else {
2320 //Reply with a 'no further files' simple text response to client
2321
2322 if(verbose == true) {
2323 console.log("No images");
2324 } else {
2325 process.stdout.write(".");
2326 }
2327 res.writeHead(200, {'content-type': 'text/html'});
2328 res.end(noFurtherFiles);
2329 return;
2330
2331 }
2332 });
2333 } else { //end of outdir compare
2334 console.log("Security exception detected in " + outdir);
2335 return;
2336 }
2337
2338 } else { //end of url read
2339
2340
2341
2342 if(url.substr(0,check.length) == check) {
2343
2344 //Right, do a check to see if the photo file exists already on the server
2345 //The request would be e.g. /check/hfghhgm34gd/path/file.jpg
2346
2347 if(allowPhotosLeaving != true) {
2348 console.log("Read request detected (blocked by config.json): " + url);
2349 res.writeHead(400, {'content-type': 'text/html'});
2350 res.end("Sorry, you cannot read from this server. Please check the server's config.json.");
2351 return;
2352 }
2353
2354 //Check uploaded photo exists from coded subdir
2355 var codeFile = decodeURIComponent(url.substr(check.length));
2356
2357
2358
2359
2360 var parentDir = serverParentDir();
2361 if(verbose == true) console.log("This drive:" + parentDir);
2362 if(verbose == true) console.log("Coded file:" + codeFile);
2363
2364 if(codeFile.length <= 0) {
2365 console.log("Cannot read without a file");
2366 return;
2367 }
2368
2369 var checkFile = getFileFromUserStr(codeFile);
2370 var fullCheck = parentDir + outdirPhotos + checkFile;
2371 if(verbose == true) console.log("Checking file:" + fullCheck);
2372
2373 //Check file exists async
2374 fs.stat(fullCheck, function(ferr, stat) {
2375 if((ferr == null)&&(stat.isFile() == true)) { //Note: it must be a full filename, to prevent bulk checks for the directories
2376 //File exists
2377 res.writeHead(200, {'content-type': 'text/html'});
2378 res.end("true");
2379 if(verbose == true) console.log("true");
2380 return;
2381
2382 } else {
2383 //File doesn't exist
2384 res.writeHead(200, {'content-type': 'text/html'});
2385 res.end("false");
2386 if(verbose == true) console.log("false");
2387 return;
2388 }
2389 });
2390
2391
2392
2393 } else { //end of check read
2394
2395
2396 if(url.substr(0,addonreq.length) == addonreq) {
2397 //So it is an addon's request
2398 //Read the addon config, and determine what to do
2399 var thisQueryString = url.substr(addonreq.length);
2400 var newLocation = "";
2401
2402
2403
2404 /*E.g. var replace = {
2405 "CUSTOMIMAGE": "yo/10-Aug-2017-09-21-45.jpg",
2406 "CUSTOMWOUNDIMAGE": "yo/10-Aug-2017-09-21-45.wound-view.jpg",
2407 "CUSTOMAREA": "123456"
2408 };*/
2409
2410
2411 addOns("urlRequest", function(newLocation, params) {
2412
2413 //Close off the request to the browser
2414 if(newLocation != "") {
2415
2416 if(params) {
2417 var replace = queryStringLib.parse(params);
2418 } else {
2419 var replace = null;
2420 }
2421
2422 if((replace) && (replace.CHANGELOCATION)) {
2423 newLocation = replace.CHANGELOCATION;
2424
2425 }
2426
2427 if(replace) {
2428 replace.STANDARDHEADER = htmlHeaderCode; //Set the header to the startup header code
2429 }
2430
2431 var outdir = __dirname + "/../public/pages/" + newLocation;
2432 if(verbose == true) console.log("Serving up file:" + outdir);
2433
2434 //Since this is dynamic content we don't want to cache it.
2435 if((res.headersSent) && (res.headersSent == true)) {
2436 //Sorry, the header has already been sent
2437 } else {
2438 res.setHeader("Cache-Control", 'private, no-cache, no-store, must-revalidate');
2439 res.setHeader("Expires", "-1");
2440 res.setHeader("Pragma", "no-cache");
2441 }
2442
2443 serveUpFile(outdir, null, res, false, replace);
2444 } else {
2445 //Just complete the browser request
2446 if(verbose == true) console.log("Completing browser request");
2447 res.writeHead(200, {'content-type': 'text/html'});
2448 res.end();
2449 }
2450
2451
2452 }, thisQueryString);
2453
2454
2455
2456 } else {
2457
2458 //Get a front-end facing image or html file
2459 var outdir = __dirname + "/../public" + url;
2460
2461 if(customString) {
2462 customString.STANDARDHEADER = htmlHeaderCode; //Set the header to the startup header code
2463 } else {
2464
2465 if((url.endsWith(".html"))&&(url !== "snippet.html")) { //We don't want to continually pass this header data around snippets
2466 //Yes, will likely need the standard header
2467 var customString = {};
2468 customString.STANDARDHEADER = htmlHeaderCode;
2469
2470 }
2471 }
2472
2473 serveUpFile(outdir, null, res, false, customString);
2474 }
2475 }
2476 }
2477 } //end of check for pairing
2478
2479 }); //Request end end
2480 } //end of ordinary file processing
2481}
2482
2483
2484function serveUpFile(fullFile, theFile, res, deleteAfterwards, customStringList) {
2485
2486 //CustomStringList should be in format:
2487 // {
2488 // "STRINGTOREPLACE1": withValue1,
2489 // "STRINGTOREPLACE2": withValue2
2490 // }
2491
2492
2493 var sections = normalizeInclWinNetworks(fullFile).split("?");
2494 var normpath = sections[0]; //Ignore any query parameters to the right
2495
2496 if(verbose == true) console.log(normpath);
2497
2498
2499
2500 // set the content type
2501 var ext = path.extname(normpath);
2502 var contentType = 'text/html';
2503 var stream = true;
2504
2505 if(customStringList) {
2506 //Likely html which we need to load in and edit before sending
2507 stream = false;
2508
2509 }
2510
2511 //Handle images
2512 if (ext === '.png') {
2513 contentType = 'image/png';
2514 stream = false;
2515 }
2516 if (ext === '.jpg') {
2517 contentType = 'image/jpeg';
2518 stream = false;
2519 }
2520
2521 if(ext === '.svg') {
2522 contentType = 'image/svg+xml';
2523 stream = false; //for some reason svg doesn't like being streamed
2524 }
2525
2526 if(ext === '.css') {
2527 contentType = 'text/css';
2528 }
2529
2530 //Run through the user-defined file types
2531 for(var type = 0; type < allowedTypes.length; type++) {
2532 if(ext === allowedTypes[type].extension) {
2533 contentType = allowedTypes[type].mime;
2534 if(verbose == true) console.log(contentType);
2535 }
2536
2537 }
2538
2539
2540 //Being preparation to send
2541
2542
2543
2544 if((stream == false)&&(deleteAfterwards != true)) {
2545 //Implies we need to modify this file, and it is likely and html request - i.e. fairly rare
2546 //Use the slow method:
2547 fs.readFile(normpath, function (err,data) {
2548
2549
2550 if (err) {
2551 res.writeHead(404);
2552 res.end(JSON.stringify(err));
2553 return;
2554 }
2555
2556
2557 if((contentType != 'image/jpeg')&&
2558 (contentType != 'image/png')) {
2559
2560 //This is use for a replace on an HTML file with custom strings
2561 var strData = data.toString();
2562
2563 for (var key in customStringList) {
2564 strData = strData.replace(new RegExp(key, 'g'), customStringList[key]);
2565
2566 }
2567
2568 data = JSON.parse( JSON.stringify( strData ) );
2569 }
2570
2571 res.on('error', function(err){
2572 //Handle the errors here
2573 res.statusCode = 400;
2574 res.end();
2575 })
2576
2577 if((res.headersSent) && (res.headersSent == true)) {
2578 //Sorry, the header has already been sent
2579 } else {
2580 res.writeHead(200, {'Content-Type': contentType, 'file-name': theFile}); //Trying with cap Content-Type, was content-type
2581 }
2582
2583 res.end(data, function(err) {
2584 //Wait until finished sending, then delete locally
2585 if(err) {
2586 console.log(err);
2587 } else {
2588 //success, do nothing
2589
2590 if(deleteAfterwards == true) {
2591 //Delete the file 'normpath' from the server. This server is like a proxy cache and
2592 //doesn't hold permanently
2593
2594 //Note: we may need to check the client has got the full file before deleting it?
2595 //e.g. timeout/ or a whole new request.
2596 if(verbose == true) console.log("About to shred:" + normpath);
2597 shredWrapper(normpath, theFile);
2598
2599
2600 }
2601
2602 }
2603 });
2604 }); //End of readFile
2605 } else { //End of if custom string
2606
2607 //Use streams instead for a larger file
2608
2609 //Still pipe a mime type back
2610 if((res.headersSent) && (res.headersSent == true)) {
2611 //Sorry, the header has already been sent
2612 } else {
2613 res.writeHead(200, {'content-type': contentType, 'file-name': theFile});
2614 }
2615
2616 //Read the file from disk, then send to client
2617 var stream = fs.createReadStream(normpath);
2618 stream.on('error', function(err) {
2619 console.log(JSON.stringify(err))
2620
2621 return;
2622 })
2623
2624 stream.on('end', function() {
2625
2626 if(deleteAfterwards == true) {
2627 //Delete the file 'normpath' from the server. This server is like a proxy cache and
2628 //doesn't hold permanently
2629
2630 //Note: we may need to check the client has got the full file before deleting it?
2631 //e.g. timeout/ or a whole new request.
2632 if(verbose == true) console.log("About to shred:" + normpath);
2633 shredWrapper(normpath, theFile);
2634
2635
2636 }
2637 });
2638
2639 stream.on('finish', function() {
2640 console.log("On finish event");
2641 });
2642
2643 stream.pipe(res);
2644 } //end of streams
2645
2646}
2647
2648//This section runs on startup.
2649checkConfigCurrent(null, function(err) {
2650
2651 if(err) {
2652 console.log("Error updating config.json: " + err);
2653 process.exit(0);
2654 }
2655
2656 httpHttpsCreateServer(serverOptions); //end of createServer
2657}); //end of checkConfigCurrent