387 lines
9.5 KiB
JavaScript
387 lines
9.5 KiB
JavaScript
|
import fs from 'fs'
|
||
|
import path from 'path'
|
||
|
import util from 'util'
|
||
|
import crypto from 'crypto'
|
||
|
import Memory from './memory.mjs'
|
||
|
|
||
|
const fsPromise = fs.promises
|
||
|
|
||
|
//
|
||
|
// ### function File (options)
|
||
|
// #### @options {Object} Options for this instance
|
||
|
// Constructor function for the File nconf store, a simple abstraction
|
||
|
// around the Memory store that can persist configuration to disk.
|
||
|
//
|
||
|
const File = function (orgOpts) {
|
||
|
let options = orgOpts
|
||
|
if (typeof(options) === 'string') {
|
||
|
options = { file: options }
|
||
|
}
|
||
|
if (!options || !options.file) {
|
||
|
throw new Error('Missing required option `file`')
|
||
|
}
|
||
|
|
||
|
Memory.call(this, options)
|
||
|
|
||
|
this.file = options.file
|
||
|
// this.dir = options.dir || process.cwd()
|
||
|
this.format = options.format || JSON
|
||
|
this.secure = options.secure
|
||
|
// this.spacing = options.json_spacing || options.spacing || 2
|
||
|
|
||
|
if (this.secure) {
|
||
|
this.secure = typeof(this.secure === 'string')
|
||
|
? { secret: this.secure.toString() }
|
||
|
: this.secure
|
||
|
|
||
|
this.secure.alg = this.secure.alg || 'aes-256-ctr'
|
||
|
// if (this.secure.secretPath) {
|
||
|
// this.secure.secret = fs.readFileSync(this.secure.secretPath, 'utf8')
|
||
|
// }
|
||
|
|
||
|
if (!this.secure.secret) {
|
||
|
throw new Error('secure.secret option is required')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if (options.search) {
|
||
|
// this.search(this.dir)
|
||
|
// }
|
||
|
}
|
||
|
|
||
|
// Inherit from the Memory store
|
||
|
util.inherits(File, Memory)
|
||
|
|
||
|
File.prototype.load = function () {
|
||
|
this.store = {}
|
||
|
|
||
|
let fileData
|
||
|
try {
|
||
|
fileData = fs.readFileSync(this.file, 'utf8')
|
||
|
}
|
||
|
catch (ex) {
|
||
|
throw new Error('Error opening ' + this.file + ': ' + ex.message)
|
||
|
}
|
||
|
|
||
|
// Deals with file that include BOM
|
||
|
if (fileData.charAt(0) === '\uFEFF') {
|
||
|
fileData = fileData.substr(1)
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.store = this.parse(fileData)
|
||
|
}
|
||
|
catch (ex) {
|
||
|
throw new Error("Error parsing your configuration file: [" + this.file + ']: ' + ex.message)
|
||
|
}
|
||
|
|
||
|
return this.store
|
||
|
}
|
||
|
|
||
|
File.prototype.loadAsync = function () {
|
||
|
this.store = {}
|
||
|
|
||
|
return fsPromise.readFile(this.file, 'utf8')
|
||
|
.then(fileData => {
|
||
|
let data = fileData
|
||
|
if (data.charAt(0) === '\uFEFF') {
|
||
|
data = data.substr(1)
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.store = this.parse(data)
|
||
|
} catch (err) {
|
||
|
return Promise.reject(new Error("Error parsing your configuration file: [" + this.file + ']: ' + err.message))
|
||
|
}
|
||
|
|
||
|
return this.store
|
||
|
}, err => {
|
||
|
return Promise.reject(new Error('Error opening ' + this.file + ': ' + err.message))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// ### function parse (contents)
|
||
|
// Returns a decrypted version of the contents IFF
|
||
|
// `this.secure` is enabled.
|
||
|
//
|
||
|
File.prototype.parse = function (contents) {
|
||
|
let parsed = this.format.parse(contents)
|
||
|
|
||
|
if (this.secure) {
|
||
|
parsed = Object.keys(parsed).reduce((acc, key) => {
|
||
|
let value = parsed[key]
|
||
|
if (!value.iv) {
|
||
|
throw new Error(`Encrypted file ${this.file} is outdated (encrypted without iv). Please re-encrypt your file.`)
|
||
|
}
|
||
|
let decipher = crypto.createDecipheriv(value.alg, this.secure.secret, Buffer.from(value.iv, 'hex'))
|
||
|
let plaintext = decipher.update(value.value, 'hex', 'utf8')
|
||
|
plaintext += decipher.final('utf8')
|
||
|
acc[key] = this.format.parse(plaintext)
|
||
|
return acc
|
||
|
}, {})
|
||
|
}
|
||
|
|
||
|
return parsed
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function save (path)
|
||
|
// #### @path {string} The path to the file where we save the configuration to
|
||
|
// Saves the current configuration object to disk at `this.file`
|
||
|
//
|
||
|
File.prototype.save = function (orgPath) {
|
||
|
let path = orgPath
|
||
|
if (!path) {
|
||
|
path = this.file
|
||
|
}
|
||
|
fs.writeFileSync(path, this.stringify())
|
||
|
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function save (path)
|
||
|
// #### @path {string} The path to the file where we save the configuration to
|
||
|
// Saves the current configuration object to disk at `this.file`
|
||
|
//
|
||
|
File.prototype.saveAsync = function(orgPath) {
|
||
|
let path = orgPath
|
||
|
if (!path) {
|
||
|
path = this.file
|
||
|
}
|
||
|
|
||
|
return fsPromise.writeFile(path, this.stringify()).then(() => {
|
||
|
return this
|
||
|
})
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function stringify ()
|
||
|
// Returns an encrypted version of the contents IIF
|
||
|
// `this.secure` is enabled
|
||
|
//
|
||
|
File.prototype.stringify = function () {
|
||
|
let data = this.store
|
||
|
|
||
|
if (this.secure) {
|
||
|
data = Object.keys(data).reduce((acc, key) => {
|
||
|
let value = this.format.stringify(data[key])
|
||
|
let iv = crypto.randomBytes(16)
|
||
|
let cipher = crypto.createCipheriv(this.secure.alg, this.secure.secret, iv)
|
||
|
let ciphertext = cipher.update(value, 'utf8', 'hex')
|
||
|
ciphertext += cipher.final('hex')
|
||
|
acc[key] = { alg: this.secure.alg, value: ciphertext, iv: iv.toString('hex') }
|
||
|
return acc
|
||
|
}, {})
|
||
|
}
|
||
|
|
||
|
return this.format.stringify(data, null, this.spacing)
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
|
||
|
//
|
||
|
// ### function save (value, callback)
|
||
|
// #### @value {Object} _Ignored_ Left here for consistency
|
||
|
// #### @callback {function} Continuation to respond to when complete.
|
||
|
// Saves the current configuration object to disk at `this.file`
|
||
|
// using the format specified by `this.format`.
|
||
|
//
|
||
|
File.prototype.save = function (value, callback) {
|
||
|
this.saveToFile(this.file, value, callback)
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function saveToFile (path, value, callback)
|
||
|
// #### @path {string} The path to the file where we save the configuration to
|
||
|
// #### @format {Object} Optional formatter, default behing the one of the store
|
||
|
// #### @callback {function} Continuation to respond to when complete.
|
||
|
// Saves the current configuration object to disk at `this.file`
|
||
|
// using the format specified by `this.format`.
|
||
|
//
|
||
|
File.prototype.saveToFile = function (path, format, callback) {
|
||
|
if (!callback) {
|
||
|
callback = format
|
||
|
format = this.format
|
||
|
}
|
||
|
|
||
|
fs.writeFile(path, this.stringify(format), callback)
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function saveSync (value, callback)
|
||
|
// Saves the current configuration object to disk at `this.file`
|
||
|
// using the format specified by `this.format` synchronously.
|
||
|
//
|
||
|
File.prototype.saveSync = function () {
|
||
|
fs.writeFileSync(this.file, this.stringify())
|
||
|
return this.store
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function load (callback)
|
||
|
// #### @callback {function} Continuation to respond to when complete.
|
||
|
// Responds with an Object representing all keys associated in this instance.
|
||
|
//
|
||
|
File.prototype.load = function (callback) {
|
||
|
let self = this
|
||
|
|
||
|
exists(self.file, function (exists) {
|
||
|
if (!exists) {
|
||
|
return callback(null, {})
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Else, the path exists, read it from disk
|
||
|
//
|
||
|
fs.readFile(self.file, function (err, data) {
|
||
|
if (err) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Deals with string that include BOM
|
||
|
let stringData = data.toString()
|
||
|
if (stringData.charAt(0) === '\uFEFF') {
|
||
|
stringData = stringData.substr(1)
|
||
|
}
|
||
|
|
||
|
self.store = self.parse(stringData)
|
||
|
}
|
||
|
catch (ex) {
|
||
|
return callback(new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message))
|
||
|
}
|
||
|
|
||
|
callback(null, self.store)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// ### function loadSync (callback)
|
||
|
// Attempts to load the data stored in `this.file` synchronously
|
||
|
// and responds appropriately.
|
||
|
//
|
||
|
File.prototype.loadSync = function () {
|
||
|
if (!existsSync(this.file)) {
|
||
|
this.store = {}
|
||
|
return this.store
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Else, the path exists, read it from disk
|
||
|
//
|
||
|
try {
|
||
|
// Deals with file that include BOM
|
||
|
let fileData = fs.readFileSync(this.file, 'utf8')
|
||
|
if (fileData.charAt(0) === '\uFEFF') {
|
||
|
fileData = fileData.substr(1)
|
||
|
}
|
||
|
|
||
|
this.store = this.parse(fileData)
|
||
|
}
|
||
|
catch (ex) {
|
||
|
throw new Error("Error parsing your configuration file: [" + this.file + ']: ' + ex.message)
|
||
|
}
|
||
|
|
||
|
return this.store
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// ### function search (base)
|
||
|
// #### @base {string} Base directory (or file) to begin searching for the target file.
|
||
|
// Attempts to find `this.file` by iteratively searching up the
|
||
|
// directory structure
|
||
|
//
|
||
|
File.prototype.search = function (base) {
|
||
|
let looking = true,
|
||
|
fullpath,
|
||
|
previous,
|
||
|
stats
|
||
|
|
||
|
base = base || process.cwd()
|
||
|
|
||
|
if (this.file[0] === '/') {
|
||
|
//
|
||
|
// If filename for this instance is a fully qualified path
|
||
|
// (i.e. it starts with a `'/'`) then check if it exists
|
||
|
//
|
||
|
try {
|
||
|
stats = fs.statSync(fs.realpathSync(this.file))
|
||
|
if (stats.isFile()) {
|
||
|
fullpath = this.file
|
||
|
looking = false
|
||
|
}
|
||
|
}
|
||
|
catch (ex) {
|
||
|
//
|
||
|
// Ignore errors
|
||
|
//
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (looking && base) {
|
||
|
//
|
||
|
// Attempt to stat the realpath located at `base`
|
||
|
// if the directory does not exist then return false.
|
||
|
//
|
||
|
try {
|
||
|
let stat = fs.statSync(fs.realpathSync(base))
|
||
|
looking = stat.isDirectory()
|
||
|
}
|
||
|
catch (ex) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
while (looking) {
|
||
|
//
|
||
|
// Iteratively look up the directory structure from `base`
|
||
|
//
|
||
|
try {
|
||
|
stats = fs.statSync(fs.realpathSync(fullpath = path.join(base, this.file)))
|
||
|
looking = stats.isDirectory()
|
||
|
}
|
||
|
catch (ex) {
|
||
|
previous = base
|
||
|
base = path.dirname(base)
|
||
|
|
||
|
if (previous === base) {
|
||
|
//
|
||
|
// If we've reached the top of the directory structure then simply use
|
||
|
// the default file path.
|
||
|
//
|
||
|
try {
|
||
|
stats = fs.statSync(fs.realpathSync(fullpath = path.join(this.dir, this.file)))
|
||
|
if (stats.isDirectory()) {
|
||
|
fullpath = undefined
|
||
|
}
|
||
|
}
|
||
|
catch (ex) {
|
||
|
//
|
||
|
// Ignore errors
|
||
|
//
|
||
|
}
|
||
|
|
||
|
looking = false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Set the file for this instance to the fullpath
|
||
|
// that we have found during the search. In the event that
|
||
|
// the search was unsuccessful use the original value for `this.file`.
|
||
|
//
|
||
|
this.file = fullpath || this.file
|
||
|
|
||
|
return fullpath
|
||
|
}
|
||
|
|
||
|
*/
|
||
|
|
||
|
export default File
|