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