├── config.json ├── package.json ├── opml ├── lib │ ├── filesystem.opml │ ├── s3.opml │ └── utils.opml ├── readme.opml └── upstream.opml ├── LICENSE ├── README.md ├── lib ├── s3.js ├── filesystem.js └── utils.js └── upstream.js /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3path": "/bullmancuso/backups/server12/", 3 | "folder": "/home/ubuntu/server12/", 4 | "ctSecsBetwScans": 5 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstreamer", 3 | "description": "Easy folder backups to S3.", 4 | "author": "Dave Winer ", 5 | "version": "0.40.0", 6 | "scripts": { 7 | "start": "node upstream.js" 8 | }, 9 | "repository": { 10 | "type" : "git", 11 | "url" : "https://github.com/scripting/upstreamer.git" 12 | }, 13 | "dependencies" : { 14 | "mime": "*", 15 | "aws-sdk": "*" 16 | }, 17 | "license": "MIT", 18 | "engines": { 19 | "node": "0.10.*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /opml/lib/filesystem.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | lib/filesystem.js 5 | <%dateModified%> 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Upstreamer 2 | 3 | Upstreamer is a Node.js app that keeps a folder backed up on Amazon S3. 4 | 5 | #### How upstreamer works 6 | 7 | I have a server running a set of Node apps using forever to keep them running. 8 | 9 | Each app is in a sub-folder of a main folder, where all its code and prefs/stats are stored. In some cases the user data is stored there too. 10 | 11 | I want to keep a copy of all the files in all the sub-folders in an S3 bucket. The backups are continuously updating as things change. 12 | 13 | My copy of upstreamer is configured to do this. 14 | 15 | #### How to set up 16 | 17 | To install: 18 | 19 |
npm install
20 | 21 | Edit config.json and set s3path to point to the place in your S3 bucket where you want this server's backups to be stored. 22 | 23 | folder is the folder hierchy we'll scan on this machine. 24 | 25 | ctSecsBetwScans is the number of seconds between each scan. 26 | 27 | upstream.js is the app, to run it: 28 | 29 |
node upstream.js
30 | 31 | #### Questions, comments? 32 | 33 | Please post a note on the Server-Snacks mail list. 34 | 35 | -------------------------------------------------------------------------------- /opml/lib/s3.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | lib/s3.js 5 | <%dateModified%> 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /opml/readme.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | readme.md 5 | <%dateModified%> 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /opml/lib/utils.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | lib/utils.js 5 | <%dateModified%> 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/s3.js: -------------------------------------------------------------------------------- 1 | var AWS = require ("aws-sdk"); 2 | var s3 = new AWS.S3 (); 3 | 4 | exports.stats = s3stats; 5 | exports.defaultType = s3defaultType; 6 | exports.defaultAcl = s3defaultAcl; 7 | exports.splitPath = s3SplitPath; 8 | exports.newObject = s3NewObject; 9 | exports.redirect = s3Redirect; 10 | exports.getObjectMetadata = s3GetObjectMetadata; 11 | exports.getObject = s3GetObject; 12 | exports.listObjects = s3ListObjects; 13 | 14 | 15 | 16 | var s3defaultType = "text/plain"; 17 | var s3defaultAcl = "public-read"; 18 | 19 | var s3stats = { 20 | ctReads: 0, ctBytesRead: 0, ctReadErrors: 0, 21 | ctWrites: 0, ctBytesWritten: 0, ctWriteErrors: 0 22 | }; 23 | 24 | function s3SplitPath (path) { //split path like this: /tmp.scripting.com/testing/one.txt -- into bucketname and path. 25 | var bucketname = ""; 26 | if (path.length > 0) { 27 | if (path [0] == "/") { //delete the slash 28 | path = path.substr (1); 29 | } 30 | var ix = path.indexOf ("/"); 31 | bucketname = path.substr (0, ix); 32 | path = path.substr (ix + 1); 33 | } 34 | return ({Bucket: bucketname, Key: path}); 35 | } 36 | function s3NewObject (path, data, type, acl, callback, metadata) { 37 | var splitpath = s3SplitPath (path); 38 | if (type === undefined) { 39 | type = s3defaultType; 40 | } 41 | if (acl === undefined) { 42 | acl = s3defaultAcl; 43 | } 44 | var params = { 45 | ACL: acl, 46 | ContentType: type, 47 | Body: data, 48 | Bucket: splitpath.Bucket, 49 | Key: splitpath.Key, 50 | Metadata: metadata 51 | }; 52 | s3.putObject (params, function (err, data) { 53 | if (err) { 54 | console.log ("s3NewObject: error == " + err.message); 55 | s3stats.ctWriteErrors++; 56 | if (callback != undefined) { 57 | callback (err, data); 58 | } 59 | } 60 | else { 61 | s3stats.ctWrites++; 62 | s3stats.ctBytesWritten += params.Body.length; 63 | if (callback != undefined) { 64 | callback (err, data); 65 | } 66 | } 67 | }); 68 | } 69 | function s3Redirect (path, url) { //1/30/14 by DW -- doesn't appear to work -- don't know why 70 | var splitpath = s3SplitPath (path); 71 | var params = { 72 | WebsiteRedirectLocation: url, 73 | Bucket: splitpath.Bucket, 74 | Key: splitpath.Key, 75 | Body: " " 76 | }; 77 | s3.putObject (params, function (err, data) { 78 | if (err != null) { 79 | console.log ("s3Redirect: err.message = " + err.message + "."); 80 | } 81 | else { 82 | console.log ("s3Redirect: path = " + path + ", url = " + url + ", data = ", JSON.stringify (data)); 83 | } 84 | }); 85 | } 86 | function s3GetObjectMetadata (path, callback) { 87 | var params = s3SplitPath (path); 88 | s3.headObject (params, function (err, data) { 89 | callback (data); 90 | }); 91 | } 92 | function s3GetObject (path, callback) { 93 | var params = s3SplitPath (path); 94 | s3.getObject (params, function (err, data) { 95 | if (err) { 96 | s3stats.ctReadErrors++; 97 | } 98 | else { 99 | s3stats.ctReads++; 100 | s3stats.ctBytesRead += data.Body.length; 101 | } 102 | callback (err, data); 103 | }); 104 | } 105 | function s3ListObjects (path, callback) { 106 | var splitpath = s3SplitPath (path); 107 | function getNextGroup (marker) { 108 | var params = {Bucket: splitpath.Bucket, Prefix: splitpath.Key}; 109 | if (marker != undefined) { 110 | params = {Bucket: splitpath.Bucket, Prefix: splitpath.Key, Marker: marker}; 111 | } 112 | s3.listObjects (params, function (err, data) { 113 | if (err) { 114 | console.log ("s3ListObjects: error == " + err.message); 115 | } 116 | else { 117 | var lastobj = data.Contents [data.Contents.length - 1]; 118 | for (var i = 0; i < data.Contents.length; i++) { 119 | data.Contents [i].s3path = splitpath.Bucket + "/" + data.Contents [i].Key; //5/22/14 by DW 120 | callback (data.Contents [i]); 121 | } 122 | if (data.IsTruncated) { 123 | getNextGroup (lastobj.Key); 124 | } 125 | else { 126 | var obj = new Object (); 127 | obj.flLastObject = true; 128 | callback (obj); 129 | } 130 | } 131 | }); 132 | } 133 | getNextGroup (); 134 | } 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /lib/filesystem.js: -------------------------------------------------------------------------------- 1 | exports.deleteDirectory = fsDeleteDirectory; 2 | exports.sureFilePath = fsSureFilePath; 3 | exports.newObject = fsNewObject; 4 | exports.getObject = fsGetObject; 5 | exports.recursivelyVisitFiles = fsRecursivelyVisitFiles; 6 | 7 | var fs = require ("fs"); 8 | 9 | var fsStats = { 10 | ctWrites: 0, 11 | ctBytesWritten: 0, 12 | ctWriteErrors: 0, 13 | ctReads: 0, 14 | ctBytesRead: 0, 15 | ctReadErrors: 0 16 | }; 17 | 18 | 19 | 20 | function fsSureFilePath (path, callback) { 21 | var splits = path.split ("/"); 22 | path = ""; //1/8/15 by DW 23 | if (splits.length > 0) { 24 | function doLevel (levelnum) { 25 | if (levelnum < (splits.length - 1)) { 26 | path += splits [levelnum] + "/"; 27 | fs.exists (path, function (flExists) { 28 | if (flExists) { 29 | doLevel (levelnum + 1); 30 | } 31 | else { 32 | fs.mkdir (path, undefined, function () { 33 | doLevel (levelnum + 1); 34 | }); 35 | } 36 | }); 37 | } 38 | else { 39 | if (callback != undefined) { 40 | callback (); 41 | } 42 | } 43 | } 44 | doLevel (0); 45 | } 46 | else { 47 | if (callback != undefined) { 48 | callback (); 49 | } 50 | } 51 | } 52 | function fsNewObject (path, data, type, acl, callback, metadata) { 53 | fsSureFilePath (path, function () { 54 | fs.writeFile (path, data, function (err) { 55 | var dataAboutWrite = { 56 | }; 57 | if (err) { 58 | console.log ("fsNewObject: error == " + JSON.stringify (err, undefined, 4)); 59 | fsStats.ctWriteErrors++; 60 | if (callback != undefined) { 61 | callback (err, dataAboutWrite); 62 | } 63 | } 64 | else { 65 | fsStats.ctWrites++; 66 | fsStats.ctBytesWritten += data.length; 67 | if (callback != undefined) { 68 | callback (err, dataAboutWrite); 69 | } 70 | } 71 | }); 72 | }); 73 | } 74 | function fsGetObject (path, callback) { 75 | fs.readFile (path, "utf8", function (err, data) { 76 | var dataAboutRead = { 77 | Body: data 78 | }; 79 | if (err) { 80 | fsStats.ctReadErrors++; 81 | } 82 | else { 83 | fsStats.ctReads++; 84 | fsStats.ctBytesRead += dataAboutRead.Body.length; 85 | } 86 | callback (err, dataAboutRead); 87 | }); 88 | } 89 | function fsListObjects (path, callback) { 90 | function endsWithChar (s, chPossibleEndchar) { 91 | if ((s === undefined) || (s.length == 0)) { 92 | return (false); 93 | } 94 | else { 95 | return (s [s.length - 1] == chPossibleEndchar); 96 | } 97 | } 98 | fs.readdir (path, function (err, list) { 99 | if (!endsWithChar (path, "/")) { 100 | path += "/"; 101 | } 102 | if (list !== undefined) { //6/4/15 by DW 103 | for (var i = 0; i < list.length; i++) { 104 | var obj = { 105 | s3path: path + list [i], 106 | path: path + list [i], //11/21/14 by DW 107 | Size: 1 108 | }; 109 | callback (obj); 110 | } 111 | } 112 | callback ({flLastObject: true}); 113 | }); 114 | } 115 | function fsRecursivelyVisitFiles (folderpath, fileCallback, completionCallback) { //3/23/16 by DW 116 | if (folderpath [folderpath.length - 1] != "/") { 117 | folderpath += "/"; 118 | } 119 | fs.readdir (folderpath, function (err, list) { 120 | function doListItem (ix) { 121 | if (ix < list.length) { 122 | var f = folderpath + list [ix]; 123 | fs.stat (f, function (err, stats) { 124 | if (err) { 125 | doListItem (ix + 1); 126 | } 127 | else { 128 | if (stats.isDirectory ()) { //dive into the directory 129 | fsRecursivelyVisitFiles (f, fileCallback, function () { 130 | doListItem (ix + 1); 131 | }); 132 | } 133 | else { 134 | if (fileCallback !== undefined) { 135 | fileCallback (f); 136 | doListItem (ix + 1); 137 | } 138 | } 139 | } 140 | }); 141 | } 142 | else { 143 | if (completionCallback !== undefined) { 144 | completionCallback (); 145 | } 146 | else { 147 | if (fileCallback !== undefined) { 148 | fileCallback (undefined); 149 | } 150 | } 151 | } 152 | } 153 | if (list !== undefined) { //6/4/15 by DW 154 | doListItem (0); 155 | } 156 | }); 157 | } 158 | function fsDeleteDirectory (folderpath, callback) { //3/25/16 by DW 159 | if (folderpath [folderpath.length - 1] != "/") { 160 | folderpath += "/"; 161 | } 162 | fs.readdir (folderpath, function (err, list) { 163 | if (err) { 164 | console.log ("fsDeleteDirectory: err.message == " + err.message); 165 | } 166 | else { 167 | function doListItem (ix) { 168 | if (ix < list.length) { 169 | var f = folderpath + list [ix]; 170 | fs.stat (f, function (err, stats) { 171 | if (err) { 172 | doListItem (ix + 1); 173 | } 174 | else { 175 | if (stats.isDirectory ()) { //dive into the directory 176 | fsDeleteDirectory (f, function () { 177 | doListItem (ix + 1); 178 | }); 179 | } 180 | else { 181 | fs.unlink (f, function () { 182 | doListItem (ix + 1); 183 | }); 184 | } 185 | } 186 | }); 187 | } 188 | else { 189 | fs.rmdir (folderpath, function () { 190 | if (callback !== undefined) { 191 | callback (); 192 | } 193 | }); 194 | } 195 | } 196 | doListItem (0); 197 | } 198 | }); 199 | } 200 | 201 | -------------------------------------------------------------------------------- /upstream.js: -------------------------------------------------------------------------------- 1 | var myVersion = "0.41a", myProductName = "upstream"; 2 | 3 | var fs = require ("fs"); 4 | var mime = require ("mime"); 5 | var s3 = require ("./lib/s3.js"); 6 | var utils = require ("./lib/utils.js"); 7 | var filesystem = require ("./lib/filesystem.js"); 8 | 9 | var config = { 10 | s3path: "/scripting/upstream/test/", //where we upload to 11 | folder: "myFiles/", //where we upload from 12 | ctSecsBetwScans: 5 13 | }; 14 | var fnameConfig = "config.json"; 15 | 16 | var stats = { 17 | ctSaves: 0, 18 | ctScans: 0, 19 | ctSecsLastScan: 0, 20 | whenLastSave: new Date (0), 21 | fileQueue: [], 22 | theFiles: { 23 | } 24 | }; 25 | var fnameStats = "stats.json", flStatsDirty = false; 26 | var flFileQueueBusy = false; 27 | var whenLastUpload = undefined; 28 | var maxSecsWait = 60; //if it's been this many secs since a file upload, the queue isn't busy 29 | 30 | 31 | function addFileToQueue (f) { 32 | for (var i = 0; i < stats.fileQueue.length; i++) { //see if it's already on the queue 33 | if (stats.fileQueue [i].f == f) { 34 | return; 35 | } 36 | } 37 | stats.fileQueue [stats.fileQueue.length] = { 38 | f: f 39 | } 40 | } 41 | function emptyFileQueue (callback) { 42 | function doNext () { 43 | if (stats.fileQueue.length > 0) { 44 | var f = stats.fileQueue [0].f; 45 | stats.fileQueue.splice (0, 1); //remove first item 46 | whenLastUpload = new Date (); //5/16/16 by DW 47 | uploadOneFile (f, function () { 48 | doNext (); 49 | }); 50 | } 51 | else { 52 | if (callback !== undefined) { 53 | callback (); 54 | } 55 | } 56 | } 57 | doNext (); 58 | } 59 | function checkFileQueue () { 60 | if (utils.secondsSince (whenLastUpload) > maxSecsWait) { //5/16/16 by DW 61 | flFileQueueBusy = false; 62 | } 63 | if (!flFileQueueBusy) { 64 | flFileQueueBusy = true; 65 | emptyFileQueue (function () { 66 | flFileQueueBusy = false; 67 | }); 68 | } 69 | } 70 | function myStatsFullFilePath () { 71 | var folder = __dirname; 72 | if (folder [folder.length - 1] != "/") { 73 | folder += "/"; 74 | } 75 | return (folder + fnameStats); 76 | } 77 | 78 | function skipFile (f) { 79 | if (f == myStatsFullFilePath ()) { //don't upload our own stats file, it's always changing 80 | return (true); 81 | } 82 | if (config.namesToSkip !== undefined) { 83 | var name = utils.stringLastField (f, "/"); 84 | for (var i = 0; i < config.namesToSkip.length; i++) { 85 | if (name == config.namesToSkip [i]) { 86 | return (true); 87 | } 88 | } 89 | } 90 | return (false); 91 | } 92 | function getFileItemName (f) { 93 | var name = utils.stringDelete (f, 1, config.folder.length); 94 | return (name); 95 | } 96 | function uploadOneFile (f, callback) { 97 | var item = stats.theFiles [getFileItemName (f)], whenStart = new Date (); 98 | function getMimeType (f) { 99 | var ext = utils.stringLastField (f, "."); 100 | mime.default_type = "text/plain"; 101 | return (mime.lookup (ext)); 102 | } 103 | fs.readFile (f, function (err, data) { 104 | if (!err) { 105 | var path = config.s3path + utils.stringDelete (f, 1, config.folder.length); 106 | console.log ("uploadOneFile: " + path); 107 | s3.newObject (path, data, getMimeType (path), "private", function () { 108 | item.ctWrites++; 109 | item.whenLastWrite = whenStart; 110 | item.ctSecsLastWrite = utils.secondsSince (whenStart); 111 | flStatsDirty = true; 112 | if (callback !== undefined) { 113 | callback (); 114 | } 115 | }); 116 | } 117 | }); 118 | } 119 | function getFileModDate (f) { 120 | var stats = fs.statSync (f); 121 | return (new Date (stats.mtime)); 122 | } 123 | function watchFolder () { 124 | var whenScanStart = new Date (); 125 | function checkFile (f) { 126 | if (!skipFile (f)) { 127 | var flupload = true, whenStart = new Date (), whenModified = getFileModDate (f); 128 | if (stats.theFiles [getFileItemName (f)] === undefined) { 129 | stats.theFiles [getFileItemName (f)] = { 130 | ctWrites: 0, 131 | whenModified: whenModified 132 | }; 133 | flStatsDirty = true; 134 | } 135 | else { 136 | var savedMod = new Date (stats.theFiles [getFileItemName (f)].whenModified); 137 | if (whenModified <= savedMod) { 138 | flupload = false; 139 | } 140 | } 141 | if (flupload) { 142 | var item = stats.theFiles [getFileItemName (f)]; 143 | item.whenModified = whenModified; 144 | flStatsDirty = true; 145 | addFileToQueue (f); 146 | } 147 | } 148 | } 149 | filesystem.sureFilePath (config.folder + "xxx", function () { 150 | filesystem.recursivelyVisitFiles (config.folder, function (f) { 151 | if (f !== undefined) { 152 | checkFile (f); 153 | } 154 | else { 155 | stats.ctScans++; 156 | stats.ctSecsLastScan = utils.secondsSince (whenScanStart); 157 | flStatsDirty = true; 158 | } 159 | }); 160 | }); 161 | } 162 | function loadStats (callback) { 163 | fs.readFile (fnameStats, function (err, data) { 164 | if (err) { 165 | } 166 | else { 167 | stats = JSON.parse (data.toString ()); 168 | if (stats.ctScans === undefined) { 169 | stats.ctScans = 0; 170 | } 171 | if (stats.fileQueue === undefined) { 172 | stats.fileQueue = []; 173 | } 174 | } 175 | if (callback !== undefined) { 176 | callback (); 177 | } 178 | }); 179 | } 180 | function loadConfig (callback) { 181 | fs.readFile (fnameConfig, function (err, data) { 182 | if (err) { 183 | console.log ("loadConfig: err.message == " + err.message); 184 | } 185 | else { 186 | var myConfig = JSON.parse (data.toString ()); 187 | for (var x in myConfig) { 188 | config [x] = myConfig [x]; 189 | } 190 | } 191 | if (callback !== undefined) { 192 | callback (); 193 | } 194 | }); 195 | } 196 | function everyMinute () { 197 | var now = new Date (); 198 | console.log ("\neveryMinute: " + now.toLocaleTimeString () + ", v" + myVersion + ", " + stats.fileQueue.length + " files in queue.\n"); 199 | } 200 | function everySecond () { 201 | if (flStatsDirty) { 202 | stats.ctSaves++; 203 | stats.whenLastSave = new Date (); 204 | fs.writeFile (fnameStats, utils.jsonStringify (stats)); 205 | flStatsDirty = false; 206 | } 207 | checkFileQueue (); 208 | } 209 | function startup () { 210 | console.log ("\n" + myProductName + " v" + myVersion + "\n"); 211 | loadConfig (function () { 212 | loadStats (function () { 213 | console.log ("startup: config == " + utils.jsonStringify (config)); 214 | watchFolder (); //do a scan at startup 215 | setInterval (everySecond, 1000); 216 | setInterval (everyMinute, 60000); 217 | setInterval (watchFolder, config.ctSecsBetwScans * 1000); 218 | }); 219 | }); 220 | } 221 | 222 | startup (); 223 | -------------------------------------------------------------------------------- /opml/upstream.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | upstream.js 5 | <%dateModified%> 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require ("fs"); 2 | 3 | exports.beginsWith = beginsWith; 4 | exports.endsWith = endsWith; 5 | exports.stringCountFields = stringCountFields; 6 | exports.stringDelete = stringDelete; 7 | exports.stringMid = stringMid; 8 | exports.padWithZeros = padWithZeros; 9 | exports.getDatePath = getDatePath; 10 | exports.secondsSince = secondsSince; 11 | exports.bumpUrlString = bumpUrlString; 12 | exports.stringContains = stringContains; 13 | exports.sameDay = sameDay; 14 | exports.jsonStringify = jsonStringify; 15 | exports.stringNthField = stringNthField; 16 | exports.getBoolean = getBoolean; 17 | exports.isAlpha = isAlpha; 18 | exports.isNumeric = isNumeric; 19 | exports.stringLastField = stringLastField; 20 | exports.multipleReplaceAll = multipleReplaceAll; 21 | exports.replaceAll = replaceAll; //2/17/15 by DW 22 | exports.kilobyteString = kilobyteString; 23 | exports.megabyteString = megabyteString; 24 | exports.gigabyteString = gigabyteString; 25 | exports.stringLower = stringLower; 26 | exports.filledString = filledString; 27 | exports.innerCaseName = innerCaseName; 28 | exports.copyScalars = copyScalars; 29 | exports.stripMarkup = stripMarkup; 30 | exports.replaceAll = replaceAll; 31 | exports.hotUpText = hotUpText; 32 | exports.secondsSince = secondsSince; 33 | exports.encodeXml = encodeXml; 34 | exports.getFileModDate = getFileModDate; //8/26/15 by DW 35 | exports.getRandomPassword = getRandomPassword; //8/28/15 by DW 36 | exports.trimWhitespace = trimWhitespace; //9/1/15 by DW 37 | exports.viewDate = viewDate; //11/29/15 by DW 38 | exports.stringPopLastField = stringPopLastField; //3/19/16 by DW 39 | exports.equalStrings = equalStrings; //4/7/16 by DW 40 | 41 | function sameDay (d1, d2) { 42 | //returns true if the two dates are on the same day 43 | d1 = new Date (d1); 44 | d2 = new Date (d2); 45 | return ((d1.getFullYear () == d2.getFullYear ()) && (d1.getMonth () == d2.getMonth ()) && (d1.getDate () == d2.getDate ())); 46 | } 47 | function dayGreaterThanOrEqual (d1, d2) { //9/2/14 by DW 48 | d1 = new Date (d1); 49 | d1.setHours (0); 50 | d1.setMinutes (0); 51 | d1.setSeconds (0); 52 | 53 | d2 = new Date (d2); 54 | d2.setHours (0); 55 | d2.setMinutes (0); 56 | d2.setSeconds (0); 57 | 58 | return (d1 >= d2); 59 | } 60 | function stringLower (s) { 61 | if (s === undefined) { //1/26/15 by DW 62 | return (""); 63 | } 64 | s = s.toString (); //1/26/15 by DW 65 | return (s.toLowerCase ()); 66 | } 67 | function secondsSince (when) { 68 | var now = new Date (); 69 | when = new Date (when); 70 | return ((now - when) / 1000); 71 | } 72 | function padWithZeros (num, ctplaces) { 73 | var s = num.toString (); 74 | while (s.length < ctplaces) { 75 | s = "0" + s; 76 | } 77 | return (s); 78 | } 79 | function getDatePath (theDate, flLastSeparator) { 80 | if (theDate === undefined) { 81 | theDate = new Date (); 82 | } 83 | else { 84 | theDate = new Date (theDate); //8/12/14 by DW -- make sure it's a date type 85 | } 86 | if (flLastSeparator === undefined) { 87 | flLastSeparator = true; 88 | } 89 | 90 | var month = padWithZeros (theDate.getMonth () + 1, 2); 91 | var day = padWithZeros (theDate.getDate (), 2); 92 | var year = theDate.getFullYear (); 93 | 94 | if (flLastSeparator) { 95 | return (year + "/" + month + "/" + day + "/"); 96 | } 97 | else { 98 | return (year + "/" + month + "/" + day); 99 | } 100 | } 101 | function multipleReplaceAll (s, adrTable, flCaseSensitive, startCharacters, endCharacters) { 102 | if(flCaseSensitive===undefined){ 103 | flCaseSensitive = false; 104 | } 105 | if(startCharacters===undefined){ 106 | startCharacters=""; 107 | } 108 | if(endCharacters===undefined){ 109 | endCharacters=""; 110 | } 111 | for( var item in adrTable){ 112 | var replacementValue = adrTable[item]; 113 | var regularExpressionModifier = "g"; 114 | if(!flCaseSensitive){ 115 | regularExpressionModifier = "gi"; 116 | } 117 | var regularExpressionString = (startCharacters+item+endCharacters).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 118 | var regularExpression = new RegExp(regularExpressionString, regularExpressionModifier); 119 | s = s.replace(regularExpression, replacementValue); 120 | } 121 | return s; 122 | } 123 | function endsWith (s, possibleEnding, flUnicase) { 124 | if ((s === undefined) || (s.length == 0)) { 125 | return (false); 126 | } 127 | var ixstring = s.length - 1; 128 | if (flUnicase === undefined) { 129 | flUnicase = true; 130 | } 131 | if (flUnicase) { 132 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 133 | if (stringLower (s [ixstring--]) != stringLower (possibleEnding [i])) { 134 | return (false); 135 | } 136 | } 137 | } 138 | else { 139 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 140 | if (s [ixstring--] != possibleEnding [i]) { 141 | return (false); 142 | } 143 | } 144 | } 145 | return (true); 146 | } 147 | function stringContains (s, whatItMightContain, flUnicase) { //11/9/14 by DW 148 | if (flUnicase === undefined) { 149 | flUnicase = true; 150 | } 151 | if (flUnicase) { 152 | s = s.toLowerCase (); 153 | whatItMightContain = whatItMightContain.toLowerCase (); 154 | } 155 | return (s.indexOf (whatItMightContain) != -1); 156 | } 157 | function beginsWith (s, possibleBeginning, flUnicase) { 158 | if (s === undefined) { //7/15/15 by DW 159 | return (false); 160 | } 161 | if (s.length == 0) { //1/1/14 by DW 162 | return (false); 163 | } 164 | if (flUnicase === undefined) { 165 | flUnicase = true; 166 | } 167 | if (flUnicase) { 168 | for (var i = 0; i < possibleBeginning.length; i++) { 169 | if (stringLower (s [i]) != stringLower (possibleBeginning [i])) { 170 | return (false); 171 | } 172 | } 173 | } 174 | else { 175 | for (var i = 0; i < possibleBeginning.length; i++) { 176 | if (s [i] != possibleBeginning [i]) { 177 | return (false); 178 | } 179 | } 180 | } 181 | return (true); 182 | } 183 | function isAlpha (ch) { 184 | return (((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z'))); 185 | } 186 | function isNumeric (ch) { 187 | return ((ch >= '0') && (ch <= '9')); 188 | } 189 | function trimLeading (s, ch) { 190 | while (s.charAt (0) === ch) { 191 | s = s.substr (1); 192 | } 193 | return (s); 194 | } 195 | function trimTrailing (s, ch) { 196 | while (s.charAt (s.length - 1) === ch) { 197 | s = s.substr (0, s.length - 1); 198 | } 199 | return (s); 200 | } 201 | function trimWhitespace (s) { //rewrite -- 5/30/14 by DW 202 | function isWhite (ch) { 203 | switch (ch) { 204 | case " ": case "\r": case "\n": case "\t": 205 | return (true); 206 | } 207 | return (false); 208 | } 209 | if (s === undefined) { //9/10/14 by DW 210 | return (""); 211 | } 212 | while (isWhite (s.charAt (0))) { 213 | s = s.substr (1); 214 | } 215 | while (s.length > 0) { 216 | if (!isWhite (s.charAt (0))) { 217 | break; 218 | } 219 | s = s.substr (1); 220 | } 221 | while (s.length > 0) { 222 | if (!isWhite (s.charAt (s.length - 1))) { 223 | break; 224 | } 225 | s = s.substr (0, s.length - 1); 226 | } 227 | return (s); 228 | } 229 | function addPeriodAtEnd (s) { 230 | s = trimWhitespace (s); 231 | if (s.length == 0) { 232 | return (s); 233 | } 234 | switch (s [s.length - 1]) { 235 | case ".": 236 | case ",": 237 | case "?": 238 | case "\"": 239 | case "'": 240 | case ":": 241 | case ";": 242 | case "!": 243 | return (s); 244 | default: 245 | return (s + "."); 246 | } 247 | } 248 | function getBoolean (val) { //12/5/13 by DW 249 | switch (typeof (val)) { 250 | case "string": 251 | if (val.toLowerCase () == "true") { 252 | return (true); 253 | } 254 | break; 255 | case "boolean": 256 | return (val); 257 | case "number": 258 | if (val == 1) { 259 | return (true); 260 | } 261 | break; 262 | } 263 | return (false); 264 | } 265 | function bumpUrlString (s) { //5/10/14 by DW 266 | if (s === undefined) { 267 | s = "0"; 268 | } 269 | function bumpChar (ch) { 270 | function num (ch) { 271 | return (ch.charCodeAt (0)); 272 | } 273 | if ((ch >= "0") && (ch <= "8")) { 274 | ch = String.fromCharCode (num (ch) + 1); 275 | } 276 | else { 277 | if (ch == "9") { 278 | ch = "a"; 279 | } 280 | else { 281 | if ((ch >= "a") && (ch <= "y")) { 282 | ch = String.fromCharCode (num (ch) + 1); 283 | } 284 | else { 285 | throw "rollover!"; 286 | } 287 | } 288 | } 289 | return (ch); 290 | } 291 | try { 292 | var chlast = bumpChar (s [s.length - 1]); 293 | s = s.substr (0, s.length - 1) + chlast; 294 | return (s); 295 | } 296 | catch (tryError) { 297 | if (s.length == 1) { 298 | return ("00"); 299 | } 300 | else { 301 | s = s.substr (0, s.length - 1); 302 | s = bumpUrlString (s) + "0"; 303 | return (s); 304 | } 305 | } 306 | } 307 | function stringDelete (s, ix, ct) { 308 | var start = ix - 1; 309 | var end = (ix + ct) - 1; 310 | var s1 = s.substr (0, start); 311 | var s2 = s.substr (end); 312 | return (s1 + s2); 313 | } 314 | function replaceAll (s, searchfor, replacewith) { 315 | function escapeRegExp (string) { 316 | return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 317 | } 318 | return (s.replace (new RegExp (escapeRegExp (searchfor), 'g'), replacewith)); 319 | } 320 | function stringCountFields (s, chdelim) { 321 | var ct = 1; 322 | if (s.length == 0) { 323 | return (0); 324 | } 325 | for (var i = 0; i < s.length; i++) { 326 | if (s [i] == chdelim) { 327 | ct++; 328 | } 329 | } 330 | return (ct) 331 | } 332 | function stringNthField (s, chdelim, n) { 333 | var splits = s.split (chdelim); 334 | if (splits.length >= n) { 335 | return splits [n-1]; 336 | } 337 | return (""); 338 | } 339 | function dateYesterday (d) { 340 | return (new Date (new Date (d) - (24 * 60 * 60 * 1000))); 341 | } 342 | function stripMarkup (s) { //5/24/14 by DW 343 | if ((s === undefined) || (s == null) || (s.length == 0)) { 344 | return (""); 345 | } 346 | return (s.replace (/(<([^>]+)>)/ig, "")); 347 | } 348 | function maxStringLength (s, len, flWholeWordAtEnd, flAddElipses) { 349 | if ((s === undefined) || (s === null)) { 350 | return (""); 351 | } 352 | else { 353 | if (flWholeWordAtEnd === undefined) { 354 | flWholeWordAtEnd = true; 355 | } 356 | if (flAddElipses === undefined) { //6/2/14 by DW 357 | flAddElipses = true; 358 | } 359 | if (s.length > len) { 360 | s = s.substr (0, len); 361 | if (flWholeWordAtEnd) { 362 | while (s.length > 0) { 363 | if (s [s.length - 1] == " ") { 364 | if (flAddElipses) { 365 | s += "..."; 366 | } 367 | break; 368 | } 369 | s = s.substr (0, s.length - 1); //pop last char 370 | } 371 | } 372 | } 373 | return (s); 374 | } 375 | } 376 | function random (lower, upper) { 377 | var range = upper - lower + 1; 378 | return (Math.floor ((Math.random () * range) + lower)); 379 | } 380 | function removeMultipleBlanks (s) { //7/30/14 by DW 381 | return (s.toString().replace (/ +/g, " ")); 382 | } 383 | function jsonStringify (jstruct, flFixBreakage) { //7/30/14 by DW 384 | //Changes 385 | //6/16/15; 10:43:25 AM by DW 386 | //Andrew Shell reported an issue in the encoding of JSON that's solved by doing character replacement. 387 | //However, this is too big a change to make for all the code that calls this library routine, so we added a boolean flag, flFixBreakage. 388 | //If this proves to be harmless, we'll change the default to true. 389 | //http://river4.smallpict.com/2015/06/16/jsonEncodingIssueSolved.html 390 | if (flFixBreakage === undefined) { 391 | flFixBreakage = false; 392 | } 393 | var s = JSON.stringify (jstruct, undefined, 4); 394 | if (flFixBreakage) { 395 | s = s.replace (/\u2028/g,'\\u2028').replace (/\u2029/g,'\\u2029'); 396 | } 397 | return (s); 398 | } 399 | function stringAddCommas (x) { //5/27/14 by DW 400 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 401 | } 402 | function readHttpFile (url, callback, timeoutInMilliseconds, headers) { //5/27/14 by DW xxx 403 | if (timeoutInMilliseconds === undefined) { 404 | timeoutInMilliseconds = 30000; 405 | } 406 | var jxhr = $.ajax ({ 407 | url: url, 408 | dataType: "text", 409 | headers: headers, 410 | timeout: timeoutInMilliseconds 411 | }) 412 | .success (function (data, status) { 413 | callback (data); 414 | }) 415 | .error (function (status) { 416 | console.log ("readHttpFile: url == " + url + ", error == " + jsonStringify (status)); 417 | callback (undefined); 418 | }); 419 | } 420 | function readHttpFileThruProxy (url, type, callback) { //10/25/14 by DW 421 | var urlReadFileApi = "http://pub2.fargo.io/httpReadUrl"; //"http://pub2.fargo.io:5347/httpReadUrl"; 422 | if (type === undefined) { 423 | type = "text/plain"; 424 | } 425 | var urlAjax = urlReadFileApi + "?url=" + encodeURIComponent (url) + "&type=" + encodeURIComponent (type); 426 | var jxhr = $.ajax ({ 427 | url: urlAjax, 428 | dataType: "text" , 429 | timeout: 30000 430 | }) 431 | .success (function (data, status) { 432 | if (callback != undefined) { 433 | callback (data); 434 | } 435 | }) 436 | .error (function (status) { 437 | console.log ("readHttpFileThruProxy: url == " + url + ", error == " + status.statusText + "."); 438 | if (callback != undefined) { 439 | callback (undefined); 440 | } 441 | }); 442 | } 443 | function stringPopLastField (s, chdelim) { //5/28/14 by DW 444 | if (s.length == 0) { 445 | return (s); 446 | } 447 | if (endsWith (s, chdelim)) { 448 | s = stringDelete (s, s.length, 1); 449 | } 450 | while (s.length > 0) { 451 | if (endsWith (s, chdelim)) { 452 | return (stringDelete (s, s.length, 1)); 453 | } 454 | s = stringDelete (s, s.length, 1); 455 | } 456 | return (s); 457 | } 458 | function stringPopExtension (s) { //4/29/15 by DW 459 | for (var i = s.length - 1; i >= 0; i--) { 460 | if (s [i] == ".") { 461 | return (stringMid (s, 1, i)); 462 | } 463 | } 464 | return (s); 465 | } 466 | function filledString (ch, ct) { //6/4/14 by DW 467 | var s = ""; 468 | for (var i = 0; i < ct; i++) { 469 | s += ch; 470 | } 471 | return (s); 472 | } 473 | function encodeXml (s) { //7/15/14 by DW 474 | if (s === undefined) { 475 | return (""); 476 | } 477 | else { 478 | var charMap = { 479 | '<': '<', 480 | '>': '>', 481 | '&': '&', 482 | '"': '&'+'quot;' 483 | }; 484 | s = s.toString(); 485 | s = s.replace(/\u00A0/g, " "); 486 | var escaped = s.replace(/[<>&"]/g, function(ch) { 487 | return charMap [ch]; 488 | }); 489 | return escaped; 490 | } 491 | } 492 | function decodeXml (s) { //11/7/14 by DW 493 | return (s.replace (/</g,'<').replace(/>/g,'>').replace(/&/g,'&')); 494 | } 495 | function hotUpText (s, url) { //7/18/14 by DW 496 | 497 | if (url === undefined) { //makes it easier to call -- 3/14/14 by DW 498 | return (s); 499 | } 500 | 501 | function linkit (s) { 502 | return ("" + s + ""); 503 | } 504 | var ixleft = s.indexOf ("["), ixright = s.indexOf ("]"); 505 | if ((ixleft == -1) || (ixright == -1)) { 506 | return (linkit (s)); 507 | } 508 | if (ixright < ixleft) { 509 | return (linkit (s)); 510 | } 511 | 512 | var linktext = s.substr (ixleft + 1, ixright - ixleft - 1); //string.mid (s, ixleft, ixright - ixleft + 1); 513 | linktext = "" + linktext + ""; 514 | 515 | var leftpart = s.substr (0, ixleft); 516 | var rightpart = s.substr (ixright + 1, s.length); 517 | s = leftpart + linktext + rightpart; 518 | return (s); 519 | } 520 | function getDomainFromUrl (url) { //7/11/15 by DW 521 | if ((url != null ) && (url != "")) { 522 | url = url.replace("www.","").replace("www2.", "").replace("feedproxy.", "").replace("feeds.", ""); 523 | var root = url.split('?')[0]; // cleans urls of form http://domain.com?a=1&b=2 524 | url = root.split('/')[2]; 525 | } 526 | return (url); 527 | }; 528 | function getFavicon (url) { //7/18/14 by DW 529 | var domain = getDomainFromUrl (url); 530 | return ("http://www.google.com/s2/favicons?domain=" + domain); 531 | }; 532 | function getURLParameter (name) { //7/21/14 by DW 533 | return (decodeURI ((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1])); 534 | } 535 | function urlSplitter (url) { //7/15/14 by DW 536 | var pattern = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/; 537 | var result = pattern.exec (url); 538 | if (result == null) { 539 | result = []; 540 | result [5] = url; 541 | } 542 | var splitUrl = { 543 | scheme: result [1], 544 | host: result [3], 545 | port: result [4], 546 | path: result [5], 547 | query: result [6], 548 | hash: result [7] 549 | }; 550 | return (splitUrl); 551 | } 552 | function innerCaseName (text) { //8/12/14 by DW 553 | var s = "", ch, flNextUpper = false; 554 | text = stripMarkup (text); 555 | for (var i = 0; i < text.length; i++) { 556 | ch = text [i]; 557 | if (isAlpha (ch) || isNumeric (ch)) { 558 | if (flNextUpper) { 559 | ch = ch.toUpperCase (); 560 | flNextUpper = false; 561 | } 562 | else { 563 | ch = ch.toLowerCase (); 564 | } 565 | s += ch; 566 | } 567 | else { 568 | if (ch == ' ') { 569 | flNextUpper = true; 570 | } 571 | } 572 | } 573 | return (s); 574 | } 575 | function hitCounter (counterGroup, counterServer) { //8/12/14 by DW 576 | var defaultCounterGroup = "scripting", defaultCounterServer = "http://counter2.fargo.io:5337/counter"; 577 | var thispageurl = location.href; 578 | if (counterGroup === undefined) { 579 | counterGroup = defaultCounterGroup; 580 | } 581 | if (counterServer === undefined) { 582 | counterServer = defaultCounterServer; 583 | } 584 | if (thispageurl === undefined) { 585 | thispageurl = ""; 586 | } 587 | if (endsWith (thispageurl, "#")) { 588 | thispageurl = thispageurl.substr (0, thispageurl.length - 1); 589 | } 590 | var jxhr = $.ajax ({ 591 | url: counterServer + "?group=" + encodeURIComponent (counterGroup) + "&referer=" + encodeURIComponent (document.referrer) + "&url=" + encodeURIComponent (thispageurl), 592 | dataType: "jsonp", 593 | jsonpCallback : "getData", 594 | timeout: 30000 595 | }) 596 | .success (function (data, status, xhr) { 597 | console.log ("hitCounter: counter ping accepted by server, group == " + counterGroup + ", page url == " + thispageurl); 598 | }) 599 | .error (function (status, textStatus, errorThrown) { 600 | console.log ("hitCounter: counter ping error: " + textStatus); 601 | }); 602 | } 603 | function stringMid (s, ix, len) { //8/12/14 by DW 604 | return (s.substr (ix-1, len)); 605 | } 606 | function getCmdKeyPrefix () { //8/15/14 by DW 607 | if (navigator.platform.toLowerCase ().substr (0, 3) == "mac") { 608 | return ("⌘"); 609 | } 610 | else { 611 | return ("Ctrl+"); 612 | } 613 | } 614 | function getRandomSnarkySlogan () { //8/15/14 by DW 615 | var snarkySlogans = [ 616 | "Good for the environment.", 617 | "All baking done on premises.", 618 | "Still diggin!", 619 | "It's even worse than it appears.", 620 | "You should never argue with a crazy man.", 621 | "Welcome back my friends to the show that never ends.", 622 | "Greetings, citizen of Planet Earth. We are your overlords. :-)", 623 | "We don't need no stinkin rock stars.", 624 | "This aggression will not stand.", 625 | "Pay no attention to the man behind the curtain.", 626 | "Only steal from the best.", 627 | "Reallll soooon now...", 628 | "What a long strange trip it's been.", 629 | "Ask not what the Internet can do for you.", 630 | "When in doubt, blog.", 631 | "Shut up and eat your vegetables.", 632 | "Don't slam the door on the way out.", 633 | "Yeah well, that's just, you know, like, your opinion, man.", 634 | "So, it has come to this.", 635 | "We now return to our regularly scheduled program.", 636 | "That rug really tied the room together.", 637 | "It's a good time for a backup.", 638 | "Takes a lickin, keeps on tickin." 639 | ] 640 | return (snarkySlogans [random (0, snarkySlogans.length - 1)]); 641 | } 642 | function dayOfWeekToString (theDay) { //8/23/14 by DW 643 | var weekday = [ 644 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 645 | ]; 646 | return (weekday[theDay]); 647 | } 648 | function viewDate (when, flShortDayOfWeek) { //8/23/14 by DW 649 | var now = new Date (); 650 | when = new Date (when); 651 | if (sameDay (when, now)) { 652 | return (timeString (when, false)) //2/9/13 by DW; 653 | } 654 | else { 655 | var oneweek = 1000 * 60 * 60 * 24 * 7; 656 | var cutoff = now - oneweek; 657 | if (when > cutoff) { //within the last week 658 | var s = dayOfWeekToString (when.getDay ()); 659 | if (flShortDayOfWeek) { 660 | s = s.substring (0, 3); 661 | } 662 | return (s); 663 | } 664 | else { 665 | return (when.toLocaleDateString ()); 666 | } 667 | } 668 | } 669 | function timeString (when, flIncludeSeconds) { //8/26/14 by DW 670 | var hour = when.getHours (), minutes = when.getMinutes (), ampm = "AM", s; 671 | if (hour >= 12) { 672 | ampm = "PM"; 673 | } 674 | if (hour > 12) { 675 | hour -= 12; 676 | } 677 | if (hour == 0) { 678 | hour = 12; 679 | } 680 | if (minutes < 10) { 681 | minutes = "0" + minutes; 682 | } 683 | if (flIncludeSeconds) { 684 | var seconds = when.getSeconds (); 685 | if (seconds < 10) { 686 | seconds = "0" + seconds; 687 | } 688 | s = hour + ":" + minutes + ":" + seconds + ampm; 689 | } 690 | else { 691 | s = hour + ":" + minutes + ampm; 692 | } 693 | return (s); 694 | } 695 | function stringLastField (s, chdelim) { //8/27/14 by DW 696 | var ct = stringCountFields (s, chdelim); 697 | if (ct == 0) { //8/31/14 by DW 698 | return (s); 699 | } 700 | return (stringNthField (s, chdelim, ct)); 701 | } 702 | function maxLengthString (s, maxlength) { //8/27/14 by DW 703 | if (s.length > maxlength) { 704 | s = s.substr (0, maxlength); 705 | while (true) { 706 | var len = s.length; flbreak = false; 707 | if (len == 0) { 708 | break; 709 | } 710 | if (s [len - 1] == " ") { 711 | flbreak = true; 712 | } 713 | s = s.substr (0, len - 1); 714 | if (flbreak) { 715 | break; 716 | } 717 | } 718 | s = s + "..."; 719 | } 720 | return (s); 721 | } 722 | function formatDate (theDate, dateformat, timezone) { //8/28/14 by DW 723 | if (theDate === undefined) { 724 | theDate = new Date (); 725 | } 726 | if (dateformat === undefined) { 727 | dateformat = "%c"; 728 | } 729 | if (timezone === undefined) { 730 | timezone = - (new Date ().getTimezoneOffset () / 60); 731 | } 732 | try { 733 | var offset = new Number (timezone); 734 | var d = new Date (theDate); 735 | var localTime = d.getTime (); 736 | var localOffset = d.getTimezoneOffset () * 60000; 737 | var utc = localTime + localOffset; 738 | var newTime = utc + (3600000 * offset); 739 | return (new Date (newTime).strftime (dateformat)); 740 | } 741 | catch (tryerror) { 742 | return (new Date (theDate).strftime (dateformat)); 743 | } 744 | } 745 | function addPeriodToSentence (s) { //8/29/14 by DW 746 | if (s.length > 0) { 747 | var fladd = true; 748 | var ch = s [s.length - 1]; 749 | switch (ch) { 750 | case "!": case "?": case ":": 751 | fladd = false; 752 | break; 753 | default: 754 | if (endsWith (s, ".\"")) { 755 | fladd = false; 756 | } 757 | else { 758 | if (endsWith (s, ".'")) { 759 | fladd = false; 760 | } 761 | } 762 | } 763 | if (fladd) { 764 | s += "."; 765 | } 766 | } 767 | return (s); 768 | } 769 | function copyScalars (source, dest) { //8/31/14 by DW 770 | for (var x in source) { 771 | var type, val = source [x]; 772 | if (val instanceof Date) { 773 | val = val.toString (); 774 | } 775 | type = typeof (val); 776 | if ((type != "object") && (type != undefined)) { 777 | dest [x] = val; 778 | } 779 | } 780 | } 781 | function linkToDomainFromUrl (url, flshort, maxlength) { //10/10/14 by DW 782 | var splitUrl = urlSplitter (url), host; 783 | if (splitUrl.host === undefined) { //1/21/16 by DW 784 | host = ""; 785 | } 786 | else { 787 | host = splitUrl.host.toLowerCase (); 788 | } 789 | if (flshort === undefined) { 790 | flshort = false; 791 | } 792 | if (flshort) { 793 | var splithost = host.split ("."); 794 | if (splithost.length == 3) { 795 | host = splithost [1]; 796 | } 797 | else { 798 | host = splithost [0]; 799 | } 800 | } 801 | else { 802 | if (beginsWith (host, "www.")) { 803 | host = stringDelete (host, 1, 4); 804 | } 805 | } 806 | 807 | if (maxlength != undefined) { //10/10/14; 10:46:56 PM by DW 808 | if (host.length > maxlength) { 809 | host = stringMid (host, 1, maxlength) + "..."; 810 | } 811 | } 812 | 813 | return ("" + host + ""); 814 | } 815 | function getRandomPassword (ctchars) { //10/14/14 by DW 816 | var s= "", ch; 817 | while (s.length < ctchars) { 818 | ch = String.fromCharCode (random (33, 122)); 819 | if (isAlpha (ch) || isNumeric (ch)) { 820 | s += ch; 821 | } 822 | } 823 | return (s.toLowerCase ()); 824 | } 825 | function monthToString (theMonthNum) { //11/4/14 by DW 826 | 827 | 828 | var theDate; 829 | if (theMonthNum === undefined) { 830 | theDate = new Date (); 831 | } 832 | else { 833 | theDate = new Date ((theMonthNum + 1) + "/1/2014"); 834 | } 835 | return (formatDate (theDate, "%B")); 836 | } 837 | function getCanonicalName (text) { //11/4/14 by DW 838 | var s = "", ch, flNextUpper = false; 839 | text = stripMarkup (text); //6/30/13 by DW 840 | for (var i = 0; i < text.length; i++) { 841 | ch = text [i]; 842 | if (isAlpha (ch) || isNumeric (ch)) { 843 | if (flNextUpper) { 844 | ch = ch.toUpperCase (); 845 | flNextUpper = false; 846 | } 847 | else { 848 | ch = ch.toLowerCase (); 849 | } 850 | s += ch; 851 | } 852 | else { 853 | if (ch == ' ') { 854 | flNextUpper = true; 855 | } 856 | } 857 | } 858 | return (s); 859 | } 860 | function clockNow () { //11/7/14 by DW 861 | return (new Date ()); 862 | } 863 | function sleepTillTopOfMinute (callback) { //11/22/14 by DW 864 | var ctseconds = Math.round (60 - (new Date ().getSeconds () + 60) % 60); 865 | if (ctseconds == 0) { 866 | ctseconds = 60; 867 | } 868 | setTimeout (callback, ctseconds * 1000); //8/13/15 by DW -- was hard-coded to "everyMinute" ignored the callback param, fixed 869 | } 870 | function scheduleNextRun (callback, ctMillisecsBetwRuns) { //11/27/14 by DW 871 | var ctmilliseconds = ctMillisecsBetwRuns - (Number (new Date ().getMilliseconds ()) + ctMillisecsBetwRuns) % ctMillisecsBetwRuns; 872 | setTimeout (callback, ctmilliseconds); 873 | } 874 | function urlEncode (s) { //12/4/14 by DW 875 | return (encodeURIComponent (s)); 876 | } 877 | function popTweetNameAtStart (s) { //12/8/14 by DW 878 | var ch; 879 | s = trimWhitespace (s); 880 | if (s.length > 0) { 881 | if (s.charAt (0) == "@") { 882 | while (s.charAt (0) != " ") { 883 | s = s.substr (1) 884 | } 885 | while (s.length > 0) { 886 | ch = s.charAt (0); 887 | if ((ch != " ") && (ch != "-")) { 888 | break; 889 | } 890 | s = s.substr (1) 891 | } 892 | } 893 | } 894 | return (s); 895 | } 896 | function httpHeadRequest (url, callback) { //12/17/14 by DW 897 | var jxhr = $.ajax ({ 898 | url: url, 899 | type: "HEAD", 900 | dataType: "text", 901 | timeout: 30000 902 | }) 903 | .success (function (data, status, xhr) { 904 | callback (xhr); //you can do xhr.getResponseHeader to get one of the header elements 905 | }) 906 | } 907 | function httpExt2MIME (ext) { //12/24/14 by DW 908 | var lowerext = stringLower (ext); 909 | var map = { 910 | "au": "audio/basic", 911 | "avi": "application/x-msvideo", 912 | "bin": "application/x-macbinary", 913 | "css": "text/css", 914 | "dcr": "application/x-director", 915 | "dir": "application/x-director", 916 | "dll": "application/octet-stream", 917 | "doc": "application/msword", 918 | "dtd": "text/dtd", 919 | "dxr": "application/x-director", 920 | "exe": "application/octet-stream", 921 | "fatp": "text/html", 922 | "ftsc": "text/html", 923 | "fttb": "text/html", 924 | "gif": "image/gif", 925 | "gz": "application/x-gzip", 926 | "hqx": "application/mac-binhex40", 927 | "htm": "text/html", 928 | "html": "text/html", 929 | "jpeg": "image/jpeg", 930 | "jpg": "image/jpeg", 931 | "js": "application/javascript", 932 | "mid": "audio/x-midi", 933 | "midi": "audio/x-midi", 934 | "mov": "video/quicktime", 935 | "mp3": "audio/mpeg", 936 | "pdf": "application/pdf", 937 | "png": "image/png", 938 | "ppt": "application/mspowerpoint", 939 | "ps": "application/postscript", 940 | "ra": "audio/x-pn-realaudio", 941 | "ram": "audio/x-pn-realaudio", 942 | "sit": "application/x-stuffit", 943 | "sys": "application/octet-stream", 944 | "tar": "application/x-tar", 945 | "text": "text/plain", 946 | "txt": "text/plain", 947 | "wav": "audio/x-wav", 948 | "wrl": "x-world/x-vrml", 949 | "xml": "text/xml", 950 | "zip": "application/zip" 951 | }; 952 | for (x in map) { 953 | if (stringLower (x) == lowerext) { 954 | return (map [x]); 955 | } 956 | } 957 | return ("text/plain"); 958 | } 959 | function kilobyteString (num) { //1/24/15 by DW 960 | num = Number (num) / 1024; 961 | return (num.toFixed (2) + "K"); 962 | } 963 | function megabyteString (num) { //1/24/15 by DW 964 | var onemeg = 1024 * 1024; 965 | if (num <= onemeg) { 966 | return (kilobyteString (num)); 967 | } 968 | num = Number (num) / onemeg; 969 | return (num.toFixed (2) + "MB"); 970 | } 971 | function gigabyteString (num) { //1/24/15 by DW 972 | var onegig = 1024 * 1024 * 1024; 973 | if (num <= onegig) { 974 | return (megabyteString (num)); 975 | } 976 | num = Number (num) / onegig; 977 | return (num.toFixed (2) + "GB"); 978 | } 979 | function dateToNumber (theDate) { //2/15/15 by DW 980 | return (Number (new Date (theDate))); 981 | } 982 | function getFileModDate (f, callback) { //8/26/15 by DW 983 | fs.exists (f, function (flExists) { 984 | if (flExists) { 985 | fs.stat (f, function (err, stats) { 986 | if (err) { 987 | callback (undefined); 988 | } 989 | else { 990 | callback (new Date (stats.mtime).toString ()); 991 | } 992 | }); 993 | } 994 | else { 995 | callback (undefined); 996 | } 997 | }); 998 | } 999 | function getFileCreationDate (f, callback) { //12/15/15 by DW 1000 | fs.exists (f, function (flExists) { 1001 | if (flExists) { 1002 | fs.stat (f, function (err, stats) { 1003 | if (err) { 1004 | callback (undefined); 1005 | } 1006 | else { 1007 | callback (new Date (stats.birthtime).toString ()); 1008 | } 1009 | }); 1010 | } 1011 | else { 1012 | callback (undefined); 1013 | } 1014 | }); 1015 | } 1016 | function getAppUrl () { //11/13/15 by DW 1017 | var url = stringNthField (window.location.href, "?", 1); 1018 | url = stringNthField (url, "#", 1); 1019 | return (url); 1020 | } 1021 | function getFacebookTimeString (when) { //11/13/15 by DW 1022 | when = new Date (when); //make sure it's a date 1023 | var ctsecs = secondsSince (when), ct, s; 1024 | if (ctsecs < 60) { 1025 | return ("Just now"); 1026 | } 1027 | 1028 | var ctminutes = ctsecs / 60; 1029 | if (ctminutes < 60) { 1030 | ct = Math.floor (ctminutes); 1031 | s = ct + " min"; 1032 | if (ct != 1) { 1033 | s += "s"; 1034 | } 1035 | return (s); 1036 | } 1037 | 1038 | var cthours = ctminutes / 60; 1039 | if (cthours < 24) { 1040 | ct = Math.floor (cthours); 1041 | s = ct + " hr"; 1042 | if (ct != 1) { 1043 | s += "s"; 1044 | } 1045 | return (s); 1046 | } 1047 | 1048 | var now = new Date (); 1049 | if (sameDay (when, dateYesterday (now))) { 1050 | return ("Yesterday at " + formatDate (when, "%l:%M %p")); 1051 | } 1052 | 1053 | var formatstring = "%b %e"; 1054 | if (when.getFullYear () != now.getFullYear ()) { 1055 | formatstring += ", %Y"; 1056 | } 1057 | return (formatDate (when, formatstring)); 1058 | } 1059 | function stringUpper (s) { //11/15/15 by DW 1060 | if (s === undefined) { 1061 | return (""); 1062 | } 1063 | s = s.toString (); 1064 | return (s.toUpperCase ()); 1065 | } 1066 | 1067 | function upperCaseFirstChar (s) { //11/15/15 by DW 1068 | if ((s === undefined) || (s.length == 0)) { 1069 | return (""); 1070 | } 1071 | s = stringUpper (s [0]) + stringDelete (s, 1, 1); 1072 | return (s); 1073 | } 1074 | function cacheConfuse (url) { //3/1/16 by DW 1075 | return (url + "?x=" + random (0, 10000000)); 1076 | } 1077 | function equalStrings (s1, s2, flUnicase) { //4/7/16 by DW 1078 | if (flUnicase === undefined) { 1079 | flUnicase = true; 1080 | } 1081 | if (flUnicase) { 1082 | return (s1.toLowerCase () == s2.toLowerCase ()); 1083 | } 1084 | else { 1085 | return (s1 == s2); 1086 | } 1087 | } 1088 | 1089 | --------------------------------------------------------------------------------