expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
302 lines (246 loc) • 11.8 kB
text/coffeescript
# EXPRESSER SETTINGS
# -----------------------------------------------------------------------------
crypto = require "crypto"
errors = require "./errors.coffee"
events = require "./events.coffee"
fs = require "fs"
lodash = require "lodash"
path = require "path"
utils = require "./utils.coffee"
# Current environment
env = null
###
# All main settings for an Expresser apps are set and described on the
# settings.default.json file. Do not edit it!!! To change settings please
# create a settings.json on the root of your application and put your
# own values there.
#
# You can also create specific settings for different runtime environments.
# For example to set settings on development, create `settings.development.json`
# and for production a `settings.production.json` file. These will be loaded
# after the main `settings.json` file.
#
# @example Sample settings.json file
# {
# "general": {
# "debug": true
# },
# "app" {
# "title": "My Super App"
# }
# }
###
class Settings
newInstance: ->
obj = new Settings()
obj.load()
return obj
##
# List of loaded settings files.
# @property
# @type Array
files: []
# MAIN METHODS
# --------------------------------------------------------------------------
###
# Load settings from `settings.default.json`, then `settings.json`, then
# environment specific settings.
###
load: =>
@loadFromJson "settings.default.json"
@loadFromJson "settings.json"
@loadFromJson "settings.#{env.toString().toLowerCase()}.json"
events.emit "Settings.on.load"
###
# Load settings from the specified JSON file.
# @param {String} filename The filename or full path to the settings file, mandatory.
# @param {Boolean} extend If true it won't update settings that are already existing, default is false.
# @return {Object} Returns the JSON representation of the loaded file, or null if error / empty.
###
loadFromJson: (filename, extend = false) =>
env = process.env.NODE_ENV or "development"
filename = utils.io.getFilePath filename
settingsJson = null
# Has json? Load it. Try using UTF8 first, if failed, use ASCII.
if filename?
encUtf8 = {encoding: "utf8"}
encAscii = {encoding: "ascii"}
# Try parsing the file with UTF8 first, if fails, try ASCII.
try
settingsJson = fs.readFileSync filename, encUtf8
settingsJson = utils.data.minifyJson settingsJson
catch ex
settingsJson = fs.readFileSync filename, encAscii
settingsJson = utils.data.minifyJson settingsJson
# Helper function to overwrite properties.
xtend = (source, target) ->
for prop, value of source
if value?.constructor is Object
target[prop] = {} if not target[prop]?
xtend source[prop], target[prop]
else if not extend or not target[prop]?
target[prop] = source[prop]
xtend settingsJson, this
# Add file to the `files` list.
@files.push {filename: filename, watching: false} if settingsJson?
if @general.debug and @logger.console and env isnt "test"
console.log "Settings.loadFromJson", filename
events.emit "Settings.on.loadFromJson", filename
# Return the JSON representation of the file (or null if not found / empty).
return settingsJson
###
# Reset to default settings by clearing values and listeners, and re-calling `load`.
###
reset: =>
@unwatch()
@files = []
@instance = new Settings()
@instance.load()
# ENCRYPTION
# --------------------------------------------------------------------------
# Helper to encrypt or decrypt settings files. The default encryption key
# defined on the `Settings.coffee` file is "ExpresserSettings", which
# you should change to your desired value. You can also set the key
# via the EXPRESSER_SETTINGS_CRYPTOKEY environment variable.
# The default cipher algorithm is AES 256.
# @param {Boolean} encrypt Pass true to encrypt, false to decrypt.
# @param {String} filename The file to be encrypted or decrypted.
# @param {Object} options Options to be passed to the cipher.
# @option options {String} cipher The cipher to be used, default is aes256.
# @option options {String} password The encryption key.
cryptoHelper: (encrypt, filename, options) ->
env = process.env
options = {} if not options?
options = lodash.defaults options, {cipher: "aes256", key: env["EXPRESSER_SETTINGS_CRYPTOKEY"]}
options.key = "ExpresserSettings" if not options.key? or options.key is ""
settingsJson = @loadFromJson filename, false
# Settings file not found or invalid? Stop here.
if not settingsJson? and @logger.console
console.warn "Settings.cryptoHelper", encrypt, filename, "File not found or invalid, abort!"
return false
# If trying to encrypt and settings property `encrypted` is true,
# abort encryption and log to the console.
if settingsJson.encrypted is true and encrypt
if @logger.console
console.warn "Settings.cryptoHelper", encrypt, filename, "Property 'encrypted' is true, abort!"
return false
# Helper to parse and encrypt / decrypt settings data.
parser = (obj) =>
currentValue = null
for prop, value of obj
if value?.constructor is Object
parser obj[prop]
else
try
currentValue = obj[prop]
if encrypt
# Check the property data type and prefix the new value.
if lodash.isBoolean currentValue
newValue = "bool:"
else if lodash.isNumber currentValue
newValue = "number:"
else
newValue = "string:"
# Create cipher amd encrypt data.
c = crypto.createCipher options.cipher, options.key
newValue += c.update currentValue.toString(), @general.encoding, "hex"
newValue += c.final "hex"
else
# Split the data as "datatype:encryptedValue".
arrValue = currentValue.split ":"
newValue = ""
# Create cipher and decrypt.
c = crypto.createDecipher options.cipher, options.key
newValue += c.update arrValue[1], "hex", @general.encoding
newValue += c.final @general.encoding
# Cast data type (boolean, number or string).
if arrValue[0] is "bool"
if newValue is "true" or newValue is "1"
newValue = true
else
newValue = false
else if arrValue[0] is "number"
newValue = parseFloat newValue
catch ex
if @logger.console
console.error "Settings.cryptoHelper", encrypt, filename, ex, currentValue
# Update settings property value.
obj[prop] = newValue
# Remove `encrypted` property prior to decrypting.
if not encrypt
delete settingsJson["encrypted"]
# Process settings data.
parser settingsJson
# Add `encrypted` property after file is encrypted.
if encrypt
settingsJson.encrypted = true
# Stringify and save the new settings file.
newSettingsJson = JSON.stringify settingsJson, null, 4
fs.writeFileSync filename, newSettingsJson, {encoding: @general.encoding}
return true
# Helper to encrypt the specified settings file. Please see `cryptoHelper` above.
# @param {String} filename The file to be encrypted.
# @param {Object} options Options to be passed to the cipher.
# @return {Boolean} Returns true if encryption OK, false if something went wrong.
encrypt: (filename, options) ->
@cryptoHelper true, filename, options
# Helper to decrypt the specified settings file. Please see `cryptoHelper` above.
# @param {String} filename The file to be decrypted.
# @param {Object} options Options to be passed to the cipher.
# @return {Boolean} Returns true if decryption OK, false if something went wrong.
decrypt: (filename, options) ->
@cryptoHelper false, filename, options
# FILE WATCHER
# --------------------------------------------------------------------------
###
# Watch loaded settings files for changes by using a file watcher.
# The `callback` is optional in case you want to notify another module about settings updates.
# @param {Function} callback Optional function (event, filename) triggered when a settings file gets updated.
###
watch: (callback) =>
env = process.env.NODE_ENV or "development"
if lodash.isBoolean callback
console.warn "Settings.watch(boolean)", "DEPRECATED! Please use watch(callback) and unwatch(callback)."
if callback? and not lodash.isFunction callback
return errors.throw "callbackMustBeFunction", "Pass null if you don't need a callback for the file watchers."
logToConsole = @general.debug and @logger.console
# Iterate loaded files to create the file system watchers.
for f in @files
do (f) =>
filename = utils.io.getFilePath f.filename
if filename? and not f.watching
fs.watchFile filename, {persistent: true}, (evt, filename) =>
@loadFromJson filename
console.log "Settings.watch", f, "Reloaded!" if env isnt "test"
callback? evt, filename
f.watching = true
if @general.debug and env isnt "test"
console.log "Settings.watch", (if callback? then "With callback" else "No callback")
###
# Unwatch changes on loaded settings files.
# The `callback` is optional in case you want to stop notifying only for that function.
# @param {Function} callback Optional function to be removed from the file watchers.
###
unwatch: (callback) =>
env = process.env.NODE_ENV or "development"
try
for f in @files
filename = utils.io.getFilePath f.filename
if filename?
if callback?
fs.unwatchFile filename, callback
else
fs.unwatchFile filename
f.watching = false
catch ex
console.error "Settings.unwatch", ex
if @general.debug and env isnt "test"
console.log "Settings.unwatch", (if callback? then "With callback" else "No callback")
# Singleton implementation
# -----------------------------------------------------------------------------
Settings.getInstance = ->
if not @instance?
@instance = new Settings()
@instance.load()
return @instance
module.exports = Settings.getInstance()