hostparty
Version:
Programmatic and CLI editing for OS hosts file
1,099 lines (868 loc) • 39.2 kB
JavaScript
var api = ((extend)=>{
"use strict";
const constants = require('./constants');
const protectedEntries = {
ips: constants.PROTECTED_ENTRIES.IPS,
hosts: constants.PROTECTED_ENTRIES.HOSTS
};
var _ = require('lodash'),
os = require('os'),
util = require('util'),
path = require('path'),
utils = require('./utils'),
pkg = require('../package.json'),
fs = require('fs').promises,
options = {
// group hosts by ip
group: constants.DEFAULT_OPTIONS.GROUP_HOSTS,
// override validation
force: constants.DEFAULT_OPTIONS.FORCE_CHANGES,
// dry-run mode - preview changes without writing
dryRun: constants.DEFAULT_OPTIONS.DRY_RUN,
// auto-backup before changes
autoBackup: constants.DEFAULT_OPTIONS.AUTO_BACKUP,
// maximum number of backups to keep
maxBackups: constants.DEFAULT_OPTIONS.MAX_BACKUPS
},
/**
* setup
*
* default options
*/
setup = (setup)=>{
// Reset to defaults first, then apply new options
options = _.extend({
group: constants.DEFAULT_OPTIONS.GROUP_HOSTS,
force: constants.DEFAULT_OPTIONS.FORCE_CHANGES,
dryRun: constants.DEFAULT_OPTIONS.DRY_RUN,
autoBackup: constants.DEFAULT_OPTIONS.AUTO_BACKUP,
maxBackups: constants.DEFAULT_OPTIONS.MAX_BACKUPS
}, setup || {});
//
return api;
},
/**
* adds hostname entries to an ip address
*
* @method add
* @param {string} ip the ip address to add hostnames to
* @param {array} hostNames array of hostnames to add
* @return {promise} resolves when complete
*/
add = (ip, hostNames)=>{
ip = (ip || '').trim();
hostNames = _.compact(_.isArray(hostNames) ? hostNames : [hostNames]);
return new Promise((resolve, reject)=>{
if (!ip.length && !hostNames.length) {
return reject('Neither a hostname or IP was supplied.');
}
if (!utils.validateIP(ip)) {
return reject(util.format('Invalid IP address [%s] was supplied.', ip));
}
hostNames.forEach((host)=>{
if (!utils.validateHost(host)) {
return reject(util.format('Invalid hostname [%s] was supplied.', host));
}
});
loadHosts()
.then((hosts)=>{
let row = hosts[ip];
if (!row) {
row = hostNames;
} else {
row = row.concat(hostNames);
}
hosts[ip] = _.uniq(row);
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would add: %s -> %s', hostNames.join(', '), ip);
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* removeIP
*
* removes entries by ip address
*
* @param {array} ips array of ip addresses to remove
* @return {promise} resolves when complete
*/
removeIP = (ips)=>{
ips = _.isArray(ips) ? ips : [ips];
return new Promise((resolve, reject)=>{
if (!ips.length) {
return reject('No IP addresses provided');
}
ips.forEach((ip)=>{
if (!utils.validateIP(ip)) {
return reject(util.format('Invalid IP address [%s] was supplied.', ip));
}
});
ips.forEach((ip)=>{
if (protectedEntries.ips.indexOf(ip) > -1) {
if (!options.force) {
return reject(util.format('%s is protected by the OS and cannot be removed. Use --force to override this.', ip));
}
}
});
loadHosts()
.then((hosts)=>{
ips.forEach((ip)=>{
if (hosts[ip]) {
delete hosts[ip];
} else {
return reject(util.format('IP %s not found in hosts file.', ip));
}
});
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would remove IP(s): %s', ips.join(', '));
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* disable
*
* disables entries by ip address (comments them out)
*
* @param {array} ips array of ip addresses to disable
* @return {promise} resolves when complete
*/
disable = (ips)=>{
ips = _.isArray(ips) ? ips : [ips];
return new Promise((resolve, reject)=>{
if (!ips.length) {
return reject('No IP address provided');
}
ips.forEach((ip)=>{
if (!utils.validateIP(ip)) {
return reject(util.format('Invalid IP address [%s] was supplied.', ip));
}
});
ips.forEach((ip)=>{
if (protectedEntries.ips.indexOf(ip) > -1) {
if (!options.force) {
return reject(util.format('%s is protected by the OS and cannot be removed. Use --force to override this.', ip));
}
}
});
loadHosts()
.then((hosts)=>{
// console.log(hosts);
ips.forEach((ip)=>{
// console.log(hosts[ip], ip);
var commentBlock = util.format('# %s', ip);
if (hosts[ip]) {
hosts[commentBlock] = hosts[ip];
delete hosts[ip];
} else {
return reject(util.format('IP %s not in hosts file.', ip));
}
});
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would disable IP(s): %s', ips.join(', '));
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* enable
*
* re-enables a previously disabled entry
*
* @param {array} ips array of ip addresses to enable
* @return {promise} resolves when complete
*/
enable = (ips)=>{
ips = _.isArray(ips) ? ips : [ips];
return new Promise((resolve, reject)=>{
if (!ips.length) {
return reject('No IP addresses provided');
}
ips.forEach((ip)=>{
if (!utils.validateIP(ip)) {
return reject(util.format('Invalid IP address [%s] was supplied.', ip));
}
});
loadHosts()
.then((hosts)=>{
ips.forEach((ip)=>{
var commentBlock = util.format('# %s', ip);
if (hosts[commentBlock]) {
hosts[ip] = _.clone(hosts[commentBlock]);
delete hosts[commentBlock];
} else {
return reject(util.format('IP %s not in file.', ip));
}
// console.log(commentBlock, hosts[commentBlock]);
});
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would enable IP(s): %s', ips.join(', '));
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* renameHost
*
* renames a hostname while retaining its IP binding
*
* @param {string} oldHostName the existing hostname to rename
* @param {string} newHostName the new hostname
* @return {promise} resolves when complete
*/
renameHost = (oldHostName, newHostName)=>{
oldHostName = (oldHostName || '').trim().toLowerCase();
newHostName = (newHostName || '').trim().toLowerCase();
return new Promise((resolve, reject)=>{
if (!oldHostName.length) {
return reject('No existing hostname provided.');
}
if (!newHostName.length) {
return reject('No new hostname provided.');
}
if (!utils.validateHost(oldHostName)) {
return reject(util.format('Invalid hostname [%s] was supplied.', oldHostName));
}
if (!utils.validateHost(newHostName)) {
return reject(util.format('Invalid hostname [%s] was supplied.', newHostName));
}
// check if old hostname is protected
const oldIsProtected = protectedEntries.hosts.some(
(protectedHost) => protectedHost.toLowerCase() === oldHostName.toLowerCase()
);
if (oldIsProtected && !options.force) {
return reject(util.format('%s is a protected hostname and cannot be renamed. Use --force to override this.', oldHostName));
}
loadHosts()
.then((hosts)=>{
let found = false;
// find the IP that has the old hostname and replace it
_.each(hosts, (hostList, ip)=>{
const index = _.findIndex(hostList, (host)=>{
return host.toLowerCase() === oldHostName;
});
if (index > -1) {
found = true;
hostList[index] = newHostName;
}
});
if (!found) {
return reject(util.format('Hostname [%s] not found in hosts file.', oldHostName));
}
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would rename: %s -> %s', oldHostName, newHostName);
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* removeHost
*
* removes specific hostnames from all ip mappings
*
* @param {array} hostNames array of hostnames to remove
* @return {promise} resolves when complete
*/
removeHost = (hostNames)=>{
hostNames = _.isArray(hostNames) ? hostNames : [hostNames];
return new Promise((resolve, reject)=>{
hostNames.forEach((host)=>{
if (!utils.validateHost(host)) {
return reject(util.format("Invalid host [%s] supplied", host));
}
});
// check for protected hostnames
hostNames.forEach((host)=>{
const isProtected = protectedEntries.hosts.some(
(protectedHost) => protectedHost.toLowerCase() === host.toLowerCase()
);
if (isProtected && !options.force) {
return reject(util.format('%s is a protected hostname and cannot be removed. Use --force to override this.', host));
}
});
loadHosts()
.then((hosts)=>{
// loop supplied hostnames
_.each(hostNames, (hostName)=>{
// loop the hosts against the ip
_.each(hosts, (hostList, ip)=>{
// purge the matching
_.remove(hostList, (host)=>{
return host.toLowerCase().trim() === hostName.toLowerCase().trim();
});
// if the purge leaves the ip hostnbame bindings empty, bin it off
if (!hostList.length) {
try {
delete hosts[ip];
} catch (e) {}
}
});
});
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would remove hostname(s): %s', hostNames.join(', '));
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* moveHostname
*
* moves a specific hostname from its current IP to a new IP
*
* @method moveHostname
* @param {string} hostName the hostname to move
* @param {string} toIP the destination IP address
* @return {promise} resolves when complete
*/
moveHostname = (hostName, toIP)=>{
hostName = (hostName || '').trim().toLowerCase();
toIP = (toIP || '').trim();
return new Promise((resolve, reject)=>{
if (!hostName.length) {
return reject('No hostname provided.');
}
if (!toIP.length) {
return reject('No destination IP provided.');
}
if (!utils.validateHost(hostName)) {
return reject(util.format('Invalid hostname [%s] was supplied.', hostName));
}
if (!utils.validateIP(toIP)) {
return reject(util.format('Invalid destination IP [%s] was supplied.', toIP));
}
// Check if hostname is protected
const isProtected = protectedEntries.hosts.some(
(protectedHost) => protectedHost.toLowerCase() === hostName.toLowerCase()
);
if (isProtected && !options.force) {
return reject(util.format('%s is a protected hostname and cannot be moved. Use --force to override this.', hostName));
}
loadHosts()
.then((hosts)=>{
let found = false;
let fromIP = null;
// Find the IP that currently has this hostname
_.each(hosts, (hostList, ip)=>{
const index = _.findIndex(hostList, (host)=>{
return host.toLowerCase() === hostName;
});
if (index > -1) {
found = true;
fromIP = ip;
// Remove from current IP
hostList.splice(index, 1);
// If this leaves the IP empty, remove it
if (!hostList.length) {
delete hosts[ip];
}
}
});
if (!found) {
return reject(util.format('Hostname [%s] not found in hosts file.', hostName));
}
// Check if already at destination
if (fromIP === toIP) {
return reject(util.format('Hostname [%s] is already at IP %s.', hostName, toIP));
}
// Add to destination IP
if (hosts[toIP]) {
hosts[toIP].push(hostName);
hosts[toIP] = _.uniq(hosts[toIP]);
} else {
hosts[toIP] = [hostName];
}
return hosts;
})
.then((hosts)=>{
const dryRunMsg = util.format('Would move hostname %s to %s', hostName, toIP);
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* replaceIP
*
* migrates all hostnames from one IP to another
*
* @method replaceIP
* @param {string} fromIP the source IP address
* @param {string} toIP the destination IP address
* @param {boolean} keepSource if true, keeps the source IP (copy mode)
* @return {promise} resolves when complete
*/
replaceIP = (fromIP, toIP, keepSource = false)=>{
fromIP = (fromIP || '').trim();
toIP = (toIP || '').trim();
return new Promise((resolve, reject)=>{
if (!fromIP.length) {
return reject('No source IP address provided.');
}
if (!toIP.length) {
return reject('No destination IP address provided.');
}
if (!utils.validateIP(fromIP)) {
return reject(util.format('Invalid source IP address [%s] was supplied.', fromIP));
}
if (!utils.validateIP(toIP)) {
return reject(util.format('Invalid destination IP address [%s] was supplied.', toIP));
}
if (fromIP === toIP) {
return reject('Source and destination IP addresses are the same.');
}
// Check if source IP is protected
if (protectedEntries.ips.indexOf(fromIP) > -1) {
if (!options.force) {
return reject(util.format('%s is protected by the OS and cannot be modified. Use --force to override this.', fromIP));
}
}
loadHosts()
.then((hosts)=>{
if (!hosts[fromIP]) {
return reject(util.format('Source IP %s not found in hosts file.', fromIP));
}
// Get hostnames from source
const sourceHosts = hosts[fromIP];
// Merge with destination (if it exists)
if (hosts[toIP]) {
hosts[toIP] = _.uniq(hosts[toIP].concat(sourceHosts));
} else {
hosts[toIP] = sourceHosts;
}
// Remove source unless keepSource is true
if (!keepSource) {
delete hosts[fromIP];
}
return hosts;
})
.then((hosts)=>{
const action = keepSource ? 'copy' : 'move';
const dryRunMsg = util.format('Would %s hostnames from %s to %s', action, fromIP, toIP);
return saveToFile(hosts, dryRunMsg).then(resolve);
})
.catch(reject);
});
},
/**
* searchByIP
*
* returns all hostnames mapped to a given IP address
*
* @method searchByIP
* @param {string} ip the IP address to search for
* @return {promise} resolves with array of hostnames or null if not found
*/
searchByIP = (ip)=>{
ip = (ip || '').trim();
return new Promise((resolve, reject)=>{
if (!ip.length) {
return reject('No IP address provided.');
}
if (!utils.validateIP(ip)) {
return reject(util.format('Invalid IP address [%s] was supplied.', ip));
}
loadHosts()
.then((hosts)=>{
if (hosts[ip]) {
resolve({
ip: ip,
hostnames: hosts[ip]
});
} else {
resolve(null);
}
})
.catch(reject);
});
},
/**
* list
*
* returns all entries in the hosts file
*
* @method list
* @param {string} filter optional filter to match hostnames
* @return {promise} resolves with hosts object
*/
list = (filter)=>{
return loadHosts(filter);
},
/**
* getStats
*
* returns statistics about the hosts file
*
* @method getStats
* @return {promise} resolves with stats object
*/
getStats = ()=>{
return loadHosts()
.then((hosts)=>{
let activeIPs = 0;
let disabledIPs = 0;
let totalHostnames = 0;
let uniqueHostnames = new Set();
_.each(hosts, (hostList, ip)=>{
if (ip.startsWith('# ')) {
disabledIPs++;
} else {
activeIPs++;
}
totalHostnames += hostList.length;
hostList.forEach((h) => uniqueHostnames.add(h.toLowerCase()));
});
return {
activeIPs: activeIPs,
disabledIPs: disabledIPs,
totalIPs: activeIPs + disabledIPs,
totalHostnames: totalHostnames,
uniqueHostnames: uniqueHostnames.size
};
});
},
/**
* createBackup
*
* creates a backup of the hosts file
*
* @method createBackup
* @return {promise} resolves with the backup file path
*/
createBackup = ()=>{
return new Promise((resolve, reject)=>{
const backupDir = utils.getBackupDir();
const backupPath = utils.generateBackupPath();
// Ensure backup directory exists
fs.mkdir(backupDir, { recursive: true })
.then(()=>{
return getHostFilePath();
})
.then((hostPath)=>{
return fs.copyFile(hostPath, backupPath);
})
.then(()=>{
// Prune old backups if needed
return pruneBackups().then(()=> resolve(backupPath));
})
.catch(reject);
});
},
/**
* listBackups
*
* lists all available backup files
*
* @method listBackups
* @return {promise} resolves with array of backup info objects
*/
listBackups = ()=>{
return new Promise((resolve, reject)=>{
const backupDir = utils.getBackupDir();
fs.readdir(backupDir)
.then((files)=>{
const backups = files
.filter((f) => f.startsWith('hosts.backup.'))
.map((f) => ({
filename: f,
path: path.join(backupDir, f),
timestamp: f.replace('hosts.backup.', '').replace(/-/g, (m, i) => i < 10 ? '-' : (i < 13 ? 'T' : (i < 16 ? ':' : '.')))
}))
.sort((a, b) => b.filename.localeCompare(a.filename)); // newest first
resolve(backups);
})
.catch((e)=>{
if (e.code === 'ENOENT') {
resolve([]); // No backup directory yet
} else {
reject(e);
}
});
});
},
/**
* restore
*
* restores the hosts file from a backup
*
* @method restore
* @param {string} backupPath optional path to specific backup (defaults to latest)
* @return {promise} resolves with the restored backup path
*/
restore = (backupPath)=>{
return new Promise((resolve, reject)=>{
const getBackup = backupPath
? Promise.resolve(backupPath)
: listBackups().then((backups)=>{
if (!backups.length) {
return Promise.reject(constants.MESSAGES.NO_BACKUPS_FOUND);
}
return backups[0].path; // Latest backup
});
getBackup
.then((backup)=>{
backupPath = backup;
return getHostFilePath();
})
.then((hostPath)=>{
return fs.copyFile(backupPath, hostPath);
})
.then(()=>{
resolve(backupPath);
})
.catch(reject);
});
},
/**
* pruneBackups
*
* removes old backups beyond the maximum limit
*
* @method pruneBackups
* @return {promise} resolves with number of backups pruned
*/
pruneBackups = ()=>{
return new Promise((resolve, reject)=>{
listBackups()
.then((backups)=>{
if (backups.length <= options.maxBackups) {
return resolve(0);
}
const toDelete = backups.slice(options.maxBackups);
const deletePromises = toDelete.map((b) => fs.unlink(b.path));
Promise.all(deletePromises)
.then(()=>{
resolve(toDelete.length);
})
.catch(reject);
})
.catch(reject);
});
},
/**
* gets the platform-specific path to the hosts file
*
* operating system version(s) location
* unix, unix-like, posix /etc/hosts
* microsoft windows 3.1 %windir%\hosts
* 95, 98, me %windir%\hosts
* nt, 2000, xp,[5] 2003, vista,
* 2008, 7, 2012, 8, 10 %systemroot%\system32\drivers\etc\hosts
* mac os x 10.0–10.1.5 (added through netinfo or niload)
* mac os x 10.2 and newer /etc/hosts (a symbolic link to /private/etc/hosts)
*
* supported architectures: 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', 'x64', and 'x86'
*
* @method getHostFilePath
* @return {promise} resolves with the file path
*/
getHostFilePath = ()=>{
var filePath;
// path was set by user via options
if (options.path) {
filePath = options.path;
} else {
// calculate the path
switch (os.platform()) {
case constants.PLATFORMS.LINUX:
case constants.PLATFORMS.DARWIN:
case constants.PLATFORMS.FREEBSD:
case constants.PLATFORMS.OPENBSD:
//
filePath = constants.PATHS.UNIX_HOSTS;
break;
case constants.PLATFORMS.WIN32:
//
filePath = process.env.windir || process.env.WINDIR;
if (filePath) {
filePath = path.normalize(util.format('%s%s', filePath, constants.PATHS.WINDOWS_HOSTS_SUFFIX));
}
break;
default:
}
}
// console.log('Resolved OS "%s" target path to %s', os.platform(), path);
// now resolve it
return fs.realpath(filePath);
},
/**
* loadHosts
*
* @method loadHosts
*
* @return object of entries in the host file
*/
loadHosts = (filter)=>{
return getHostFilePath()
.then((path)=>{
return fs
.readFile(path)
.then((file)=>{
let hosts = {
// comments: []
},
expression = /((^\#\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
file.toString().split("\n").map((line)=>{
// Check for disabled entries (commented IPs like "# 192.168.1.1 hostname")
if (line.indexOf('#') === 0) {
// Try to parse as disabled entry
const disabledMatch = line.match(/^#\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(.+)$/);
if (disabledMatch) {
const disabledIP = '# ' + disabledMatch[1];
const disabledHosts = disabledMatch[2].trim().split(/\s+/);
if (hosts[disabledIP]) {
hosts[disabledIP] = hosts[disabledIP].concat(disabledHosts);
} else {
hosts[disabledIP] = disabledHosts;
}
}
return;
}
// split the string to get the ip and hosts
line = line .replace(/(\s)+/g, ' ')
.split(' ');
if (!line.length || line.length < 2) {
} else {
// duplicate ip, so merge them
if (line[0].length) {
let ip = hosts[line[0]];
if (ip) {
ip = ip.concat(_.compact(line.slice(1)));
} else {
ip = _.compact(line.slice(1));
}
// only return matching hosts
if (filter) {
var matching = _.filter(ip, (ip)=>{
return ip.indexOf(filter) > -1;
});
if (matching.length) {
ip = matching;
} else {
return;
}
}
hosts[line[0]] = _.uniq(ip);
}
}
});
return hosts;
})
// format the ips
.then((hosts)=>{
return utils.sortEntries(hosts);
});
})
.catch((e)=>{
console.error(e);
});
},
/**
* saveToFile
*
* @param {object} hosts - the hosts object to save
* @param {string} dryRunMessage - optional message describing what would change (for dry-run output)
*/
saveToFile = (hosts, dryRunMessage)=>{
// sort the dictionary of entries
hosts = utils.sortEntries(hosts);
return new Promise((resolve, reject)=>{
getHostFilePath()
.then((filePath)=>{
let contents = [];
_.each(hosts, (hosts, ip)=>{
// group the hosts by IP
if (options.group) {
contents.push(util.format('%s %s', ip, hosts.join(' ')));
} else {
// don't group - 1 line per IP
hosts.forEach((host)=>{
contents.push(util.format('%s %s', ip, host));
});
}
});
// Dry-run mode: return preview instead of writing
if (options.dryRun) {
return resolve({
dryRun: true,
message: dryRunMessage,
preview: contents.join("\n")
});
}
// Auto-backup before writing (if enabled)
const backupPromise = options.autoBackup
? createBackup().catch(() => {/* Ignore backup errors */})
: Promise.resolve();
return backupPromise.then(()=>{
return fs
.writeFile(filePath, util.format("%s%s", contents.join("\n"), "\n"))
.then(resolve)
.catch((e)=>{
var message = 'Error writing file',
elevated = (os.platform().match(constants.REGEX.WINDOWS_PLATFORM) ? constants.USER_ROLES.WINDOWS_ADMIN : constants.USER_ROLES.UNIX_ROOT);
// access denied?
switch (e.code) {
case constants.ERROR_CODES.ACCESS_DENIED:
message = util.format('Write permission denied on %s. Try running as %s.', filePath, elevated);
break;
}
reject(message);
});
});
}).catch(reject);
});
},
/**
* @deprecated Use removeIP() instead
*/
remove = (...args)=>{
console.warn('hostparty: remove() is deprecated, use removeIP() instead');
return removeIP(...args);
},
/**
* @deprecated Use removeHost() instead
*/
purge = (...args)=>{
console.warn('hostparty: purge() is deprecated, use removeHost() instead');
return removeHost(...args);
};
return {
setup: setup,
add: add,
removeIP: removeIP,
removeHost: removeHost,
renameHost: renameHost,
moveHostname: moveHostname,
replaceIP: replaceIP,
disable: disable,
enable: enable,
searchByIP: searchByIP,
list: list,
getStats: getStats,
createBackup: createBackup,
listBackups: listBackups,
restore: restore,
pruneBackups: pruneBackups,
remove: remove,
purge: purge
};
})();
module.exports = api;