expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
297 lines (244 loc) • 11.6 kB
text/coffeescript
# EXPRESSER UTILS
# -----------------------------------------------------------------------------
# General network, IO, client and server utilities. As this module can't reference
# any other module but Settings, all its logging will be done to the console only.
class Utils
crypto = require "crypto"
fs = require "fs"
lodash = require "lodash"
moment = require "moment"
os = require "os"
path = require "path"
settings = require "./settings.coffee"
# Temporary variable used to calculate CPU usage.
lastCpuLoad = null
# SERVER INFO UTILS
# --------------------------------------------------------------------------
# Helper to get the correct filename for general files. For example
# the settings.json file or cron.json for cron jobs. This will look into the current
# directory, the running directory and the root directory of the app.
# Returns null if no file is found.
# @param [String] filename The base filename (with extension) of the config file.
# @return [String] The full path to the config file if one was found, or null.
getFilePath: (filename) ->
originalFilename = "./" + filename.toString()
# Check if file exists.
hasFile = fs.existsSync filename
return filename if hasFile
# Try current path...
filename = path.resolve __dirname, originalFilename
hasFile = fs.existsSync filename
return filename if hasFile
# Try parent path...
filename = path.resolve __dirname, "../", originalFilename
hasFile = fs.existsSync filename
return filename if hasFile
# If file does not exist on local path, try application root path.
filename = path.resolve path.dirname(require.main.filename), originalFilename
hasFile = fs.existsSync filename
return filename if hasFile
# Nothing found, so return null.
return null
# Returns a list of valid server IP addresses. If `firstOnly` is true it will
# return only the very first IP address found.
# @param [Boolean] firstOnly Optional, default is false which returns an array with all valid IPs, true returns a String will first valid IP.
# @return The server IPv4 address, or null.
getServerIP: (firstOnly) ->
ifaces = os.networkInterfaces()
result = []
# Parse network interfaces and try getting the server IPv4 address.
for i of ifaces
ifaces[i].forEach (details) ->
if details.family is "IPv4" and not details.internal
result.push details.address
# Return only first IP or all of them?
if firstOnly
return result[0]
else
return result
# Return an object with general information about the server.
# @return [Object] Results with process pid, platform, memory, uptime and IP.
getServerInfo: =>
result = {}
# Save parsed OS info to the result object.
result.uptime = moment.duration(process.uptime(), "s").humanize()
result.hostname = os.hostname()
result.title = path.basename process.title
result.platform = os.platform() + " " + os.arch() + " " + os.release()
result.memoryTotal = (os.totalmem() / 1024 / 1024).toFixed(0) + " MB"
result.memoryUsage = 100 - (os.freemem() / os.totalmem() * 100).toFixed(0)
result.ips = @getServerIP()
result.process = {pid: process.pid, memoryUsage: (process.memoryUsage().rss / 1024 / 1024).toFixed(0) + " MB"}
result.cpuCores = os.cpus().length
# Calculate average CPU load.
lastCpuLoad = @getCpuLoad() if not lastCpuLoad?
currentCpuLoad = @getCpuLoad()
idleDifference = currentCpuLoad.idle - lastCpuLoad.idle
totalDifference = currentCpuLoad.total - lastCpuLoad.total
result.loadAvg = 100 - ~~(100 * idleDifference / totalDifference)
return result
# Get current CPU load (used mainly by getServerInfo).
getCpuLoad: ->
totalIdle = 0
totalTick = 0
cpus = os.cpus()
i = 0
len = cpus.length
while i < len
cpu = cpus[i]
totalTick += value for t, value of cpu.times
totalIdle += cpu.times.idle
i++
return {idle: totalIdle / cpus.length, total: totalTick / cpus.length}
# CLIENT INFO UTILS
# --------------------------------------------------------------------------
# Get the client or browser IP. Works for http and socket requests, even when behind a proxy.
# @param [Object] reqOrSocket The request or socket object.
# @return [String] The client IP address, or null.
getClientIP: (reqOrSocket) ->
return null if not reqOrSocket?
# Try getting the xforwarded header first.
if reqOrSocket.header?
xfor = reqOrSocket.header "X-Forwarded-For"
if xfor? and xfor isnt ""
return xfor.split(",")[0]
# Get remote address.
if reqOrSocket.connection?
return reqOrSocket.connection.remoteAddress
else
return reqOrSocket.remoteAddress
# Get the client's device. This identifier string is based on the user agent.
# @param [Object] req The request object.
# @return [String] The client's device.
getClientDevice: (req) ->
ua = req.headers["user-agent"]
# Find mobile devices.
return "mobile-wp-8" if ua.indexOf("Windows Phone 8") > 0
return "mobile-wp-7" if ua.indexOf("Windows Phone 7") > 0
return "mobile-wp" if ua.indexOf("Windows Phone") > 0
return "mobile-iphone-5" if ua.indexOf("iPhone5") > 0
return "mobile-iphone-4" if ua.indexOf("iPhone4") > 0
return "mobile-iphone" if ua.indexOf("iPhone") > 0
return "mobile-android-5" if ua.indexOf("Android 5") > 0
return "mobile-android-4" if ua.indexOf("Android 4") > 0
return "mobile-android" if ua.indexOf("Android") > 0
# Find desktop browsers.
return "desktop-chrome" if ua.indexOf("Chrome/") > 0
return "desktop-firefox" if ua.indexOf("Firefox/") > 0
return "desktop-safari" if ua.indexOf("Safari/") > 0
return "desktop-opera" if ua.indexOf("Opera/") > 0
return "desktop-ie-11" if ua.indexOf("MSIE 11") > 0
return "desktop-ie-10" if ua.indexOf("MSIE 10") > 0
return "desktop-ie-9" if ua.indexOf("MSIE 9") > 0
return "desktop-ie" if ua.indexOf("MSIE") > 0
# Return default desktop value if no specific devices were found on user agent.
return "desktop"
# IO AND DATAUTILS
# --------------------------------------------------------------------------
# Copy the `src` file to the `target`, both must be the full file path.
# @param [String] src The full source file path.
# @param [String] target The full target file path.
copyFileSync: (src, target) =>
srcContents = fs.readFileSync src
fs.writeFileSync target, srcContents
# Make sure the "target" directory exists by recursively iterating through its parents
# and creating the directories. Returns nothing if all good or error.
mkdirRecursive: (target) =>
callback = (p, made) ->
made = null if not made
p = path.resolve p
try
fs.mkdirSync p
catch ex
if ex.code is "ENOENT"
made = callback path.dirname(p), made
callback p, made
else
try
stat = fs.statSync p
catch ex1
throw ex
if not stat.isDirectory()
throw ex
return made
return callback target
# Minify the passed JSON value. Removes comments, unecessary white spaces etc.
# @param [String] source The JSON text to be minified.
# @param [Boolean] asString If true, return as string instead of JSON object.
# @return [String] The minified JSON, or an empty string if there's an error.
minifyJson: (source, asString) ->
source = JSON.stringify source if typeof source is "object"
index = 0
length = source.length
result = ""
symbol = undefined
position = undefined
# Main iterator.
while index < length
symbol = source.charAt index
switch symbol
# Ignore whitespace tokens. According to ES 5.1 section 15.12.1.1,
# whitespace tokens include tabs, carriage returns, line feeds, and
# space characters.
when "\t", "\r"
, "\n"
, " "
index += 1
# Ignore line and block comments.
when "/"
symbol = source.charAt(index += 1)
switch symbol
# Line comments.
when "/"
position = source.indexOf("\n", index)
# Check for CR-style line endings.
position = source.indexOf("\r", index) if position < 0
index = (if position > -1 then position else length)
# Block comments.
when "*"
position = source.indexOf("*/", index)
if position > -1
# Advance the scanner's position past the end of the comment.
index = position += 2
break
throw SyntaxError "Unterminated block comment."
else
throw SyntaxError "Invalid comment."
# Parse strings separately to ensure that any whitespace characters and
# JavaScript-style comments within them are preserved.
when "\""
position = index
while index < length
symbol = source.charAt(index += 1)
if symbol is "\\"
# Skip past escaped characters.
index += 1
else break if symbol is "\""
if source.charAt(index) is "\""
result += source.slice(position, index += 1)
break
throw SyntaxError "Unterminated string."
# Preserve all other characters.
else
result += symbol
index += 1
# Check if should return as string or JSON.
if asString
return result
else
return JSON.parse result
# Generates a RFC1422-compliant unique ID using random numbers.
uuid: =>
baseStr = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
generator = (c) ->
r = Math.random() * 16 | 0
v = if c is "x" then r else (r & 0x3|0x8)
v.toString 16
return baseStr.replace(/[xy]/g, generator)
# Singleton implementation
# --------------------------------------------------------------------------
Utils.getInstance = ->
return new Utils() if process.env is "test"
@instance = new Utils() if not @instance?
return @instance
module.exports = exports = Utils.getInstance()