folder-hash
Advanced tools
Comparing version 3.3.3 to 4.0.0
99
cli.js
@@ -5,72 +5,73 @@ const fs = require('graceful-fs'); | ||
function program(cliArgs) { | ||
let args; | ||
try { | ||
args = parseArgs(cliArgs); | ||
} catch (ex) { | ||
error(ex); | ||
} | ||
let args; | ||
try { | ||
args = parseArgs(cliArgs); | ||
} catch (ex) { | ||
error(ex); | ||
} | ||
if (args.help) { | ||
printHelp(); | ||
process.exit(0); | ||
} | ||
if (args.help) { | ||
printHelp(); | ||
process.exit(0); | ||
} | ||
let config; | ||
if (args.config) { | ||
try { | ||
config = JSON.parse(fs.readFileSync(args.config, { encoding: 'utf-8' })); | ||
} catch (err) { | ||
console.error('Could not parse configuration from file ' + args.config); | ||
console.error('Maybe try a JSON config like this instead?\n'); | ||
console.error(JSON.stringify(lib.defaults, undefined, 2)); | ||
process.exit(1); | ||
} | ||
let config; | ||
if (args.config) { | ||
try { | ||
config = JSON.parse(fs.readFileSync(args.config, { encoding: 'utf-8' })); | ||
} catch (err) { | ||
console.error('Could not parse configuration from file ' + args.config); | ||
console.error('Maybe try a JSON config like this instead?\n'); | ||
console.error(JSON.stringify(lib.defaults, undefined, 2)); | ||
process.exit(1); | ||
} | ||
} | ||
return lib.hashElement(args.src || process.cwd(), config) | ||
.then(result => console.log(result.toString())) | ||
.catch(error); | ||
return lib | ||
.hashElement(args.src || process.cwd(), config) | ||
.then(result => console.log(result.toString())) | ||
.catch(error); | ||
} | ||
function parseArgs(args) { | ||
let help, config, src | ||
let help, config, src; | ||
for (let index = 0; index < args.length; index++) { | ||
switch (args[index]) { | ||
case '-h': | ||
case '--help': | ||
help = true; | ||
break; | ||
for (let index = 0; index < args.length; index++) { | ||
switch (args[index]) { | ||
case '-h': | ||
case '--help': | ||
help = true; | ||
break; | ||
case '-c': | ||
case '--config': | ||
if (!args[++index]) { | ||
throw new Error(`Need to supply a JSON config file after "${args[index]}"`); | ||
} | ||
config = args[index]; | ||
break; | ||
case '-c': | ||
case '--config': | ||
if (!args[++index]) { | ||
throw new Error(`Need to supply a JSON config file after "${args[index]}"`); | ||
} | ||
config = args[index]; | ||
break; | ||
default: | ||
if (!src) { | ||
src = args[index]; | ||
} else { | ||
console.log(`Ignoring param "${args[index]}"`); | ||
} | ||
break; | ||
default: | ||
if (!src) { | ||
src = args[index]; | ||
} else { | ||
console.log(`Ignoring param "${args[index]}"`); | ||
} | ||
break; | ||
} | ||
} | ||
return { help, config, src }; | ||
return { help, config, src }; | ||
} | ||
function error(err) { | ||
console.error('ERROR:', ex.message || ex.name || ex); | ||
process.exit(1); | ||
console.error('ERROR:', ex.message || ex.name || ex); | ||
process.exit(1); | ||
} | ||
function printHelp() { | ||
console.log('Use folder-hash on cli like this:'); | ||
console.log(' folder-hash [--config <json-file>] <file-or-folder>'); | ||
console.log('Use folder-hash on cli like this:'); | ||
console.log(' folder-hash [--config <json-file>] <file-or-folder>'); | ||
} | ||
module.exports = program; |
565
index.js
const crypto = require('crypto'), | ||
debug = require('debug'), | ||
minimatch = require('minimatch'), | ||
path = require('path'); | ||
debug = require('debug'), | ||
minimatch = require('minimatch'), | ||
path = require('path'); | ||
const defaultOptions = { | ||
algo: 'sha1', // see crypto.getHashes() for options | ||
encoding: 'base64', // 'base64', 'hex' or 'binary' | ||
files: { | ||
exclude: [], | ||
include: [], | ||
matchBasename: true, | ||
matchPath: false, | ||
ignoreBasename: false, | ||
ignoreRootName: false | ||
}, | ||
folders: { | ||
exclude: [], | ||
include: [], | ||
matchBasename: true, | ||
matchPath: false, | ||
ignoreBasename: false, | ||
ignoreRootName: false | ||
} | ||
algo: 'sha1', // see crypto.getHashes() for options | ||
encoding: 'base64', // 'base64', 'hex' or 'binary' | ||
files: { | ||
exclude: [], | ||
include: [], | ||
matchBasename: true, | ||
matchPath: false, | ||
ignoreBasename: false, | ||
ignoreRootName: false, | ||
}, | ||
folders: { | ||
exclude: [], | ||
include: [], | ||
matchBasename: true, | ||
matchPath: false, | ||
ignoreBasename: false, | ||
ignoreRootName: false, | ||
}, | ||
symbolicLinks: { | ||
include: true, | ||
ignoreBasename: false, | ||
ignoreTargetPath: true, | ||
ignoreTargetContent: false, | ||
ignoreTargetContentAfterError: false, | ||
}, | ||
}; | ||
@@ -29,281 +36,353 @@ | ||
const log = { | ||
match: debug('fhash:match'), | ||
params: (params) => { | ||
debug('fhash:parameters')(params); | ||
return params; | ||
} | ||
match: debug('fhash:match'), | ||
params: params => { | ||
debug('fhash:parameters')(params); | ||
return params; | ||
}, | ||
err: debug('fhash:err'), | ||
symlink: debug('fhash:symlink'), | ||
}; | ||
function prep(fs, Promise) { | ||
function hashElement(name, dir, options, callback) { | ||
callback = arguments[arguments.length - 1]; | ||
function prep(fs) { | ||
function hashElement(name, dir, options, callback) { | ||
callback = arguments[arguments.length - 1]; | ||
return parseParameters(arguments) | ||
.then(({ basename, dir, options }) => { | ||
// this is only used for the root level | ||
options.skipMatching = true; | ||
return hashElementPromise(basename, dir, options, true); | ||
}) | ||
.then(result => { | ||
if (isFunction(callback)) { | ||
return callback(undefined, result); | ||
} else { | ||
return result; | ||
} | ||
}) | ||
.catch(reason => { | ||
if (isFunction(callback)) { | ||
return callback(reason); | ||
} else { | ||
throw reason; | ||
} | ||
}); | ||
} | ||
return parseParameters(arguments) | ||
.then(({ basename, dir, options }) => { | ||
// this is only used for the root level | ||
options.skipMatching = true; | ||
return fs.promises | ||
.lstat(path.join(dir, basename)) | ||
.then(stats => { | ||
stats.name = basename; | ||
return stats; | ||
}) | ||
.then(stats => hashElementPromise(stats, dir, options, true)); | ||
}) | ||
.then(result => { | ||
if (isFunction(callback)) { | ||
return callback(undefined, result); | ||
} else { | ||
return result; | ||
} | ||
}) | ||
.catch(reason => { | ||
log.err('Fatal error:', reason); | ||
if (isFunction(callback)) { | ||
return callback(reason); | ||
} else { | ||
throw reason; | ||
} | ||
}); | ||
} | ||
function hashElementPromise(basename, dirname, options, isRootElement = false) { | ||
return stat(path.join(dirname, basename)).then(stats => { | ||
if (stats.isDirectory()) { | ||
return hashFolderPromise(basename, dirname, options, isRootElement); | ||
} else if (stats.isFile()) { | ||
return hashFilePromise(basename, dirname, options, isRootElement); | ||
} else { | ||
return { | ||
name: basename, | ||
hash: 'unknown element type' | ||
}; | ||
} | ||
}); | ||
/** | ||
* @param {fs.Stats} stats folder element, can also be of type fs.Dirent | ||
* @param {string} dirname | ||
* @param {Options} options | ||
* @param {boolean} isRootElement | ||
*/ | ||
function hashElementPromise(stats, dirname, options, isRootElement = false) { | ||
const name = stats.name; | ||
if (stats.isDirectory()) { | ||
return hashFolderPromise(name, dirname, options, isRootElement); | ||
} else if (stats.isFile()) { | ||
return hashFilePromise(name, dirname, options, isRootElement); | ||
} else if (stats.isSymbolicLink()) { | ||
return hashSymLinkPromise(name, dirname, options, isRootElement); | ||
} else { | ||
log.err('hashElementPromise cannot handle ', stats); | ||
return { name, hash: 'Error: unknown element type' }; | ||
} | ||
} | ||
function stat(filepath) { | ||
return new Promise((resolve, reject) => { | ||
fs.stat(filepath, (err, stats) => { | ||
if (err) { | ||
return reject(err); | ||
} else { | ||
return resolve(stats); | ||
} | ||
}); | ||
}); | ||
function hashFolderPromise(name, dir, options, isRootElement = false) { | ||
const folderPath = path.join(dir, name); | ||
let ignoreBasenameOnce = options.ignoreBasenameOnce; | ||
delete options.ignoreBasenameOnce; | ||
if (options.skipMatching) { | ||
// this is currently only used for the root folder | ||
log.match(`skipped '${folderPath}'`); | ||
delete options.skipMatching; | ||
} else if (ignore(name, folderPath, options.folders)) { | ||
return undefined; | ||
} | ||
function hashFolderPromise(name, dir, options, isRootElement = false) { | ||
const folderPath = path.join(dir, name); | ||
return fs.promises.readdir(folderPath, { withFileTypes: true }).then(files => { | ||
const children = files.sort().map(child => { | ||
return hashElementPromise(child, folderPath, options); | ||
}); | ||
if (options.skipMatching) { | ||
// this is currently only used for the root folder | ||
log.match(`skipped '${folderPath}'`); | ||
delete options.skipMatching; | ||
} else if (ignore(name, folderPath, options.folders)) { | ||
return undefined; | ||
} | ||
return Promise.all(children).then(children => { | ||
if (ignoreBasenameOnce) options.ignoreBasenameOnce = true; | ||
const hash = new HashedFolder(name, children.filter(notUndefined), options, isRootElement); | ||
return hash; | ||
}); | ||
}); | ||
} | ||
return readdir(folderPath).then(files => { | ||
const children = files.map(child => { | ||
return hashElementPromise(child, folderPath, options); | ||
}); | ||
function hashFilePromise(name, dir, options, isRootElement = false) { | ||
const filePath = path.join(dir, name); | ||
return Promise.all(children).then(children => { | ||
const hash = new HashedFolder(name, children.filter(notUndefined), options, isRootElement); | ||
return hash; | ||
}); | ||
}); | ||
if (options.skipMatching) { | ||
// this is currently only used for the root folder | ||
log.match(`skipped '${filePath}'`); | ||
delete options.skipMatching; | ||
} else if (ignore(name, filePath, options.files)) { | ||
return undefined; | ||
} | ||
function readdir(folderPath) { | ||
return new Promise((resolve, reject) => { | ||
fs.readdir(folderPath, (err, files) => { | ||
if (err) { | ||
console.error(err); | ||
return reject(err); | ||
} else { | ||
return resolve(files.sort()); | ||
} | ||
}); | ||
}); | ||
} | ||
return new Promise((resolve, reject) => { | ||
try { | ||
const hash = crypto.createHash(options.algo); | ||
if ( | ||
options.files.ignoreBasename || | ||
options.ignoreBasenameOnce || | ||
(isRootElement && options.files.ignoreRootName) | ||
) { | ||
delete options.ignoreBasenameOnce; | ||
log.match(`omitted name of ${filePath} from hash`); | ||
} else { | ||
hash.update(name); | ||
} | ||
function hashFilePromise(name, dir, options, isRootElement = false) { | ||
const filePath = path.join(dir, name); | ||
const f = fs.createReadStream(filePath); | ||
f.pipe(hash, { end: false }); | ||
if (options.skipMatching) { | ||
// this is currently only used for the root folder | ||
log.match(`skipped '${filePath}'`); | ||
delete options.skipMatching; | ||
} else if (ignore(name, filePath, options.files)) { | ||
return undefined; | ||
} | ||
f.on('end', () => { | ||
const hashedFile = new HashedFile(name, hash, options.encoding); | ||
return resolve(hashedFile); | ||
}); | ||
} catch (ex) { | ||
return reject(ex); | ||
} | ||
}); | ||
} | ||
return new Promise((resolve, reject) => { | ||
try { | ||
const hash = crypto.createHash(options.algo); | ||
if (options.files.ignoreBasename || | ||
(isRootElement && options.files.ignoreRootName)) | ||
{ | ||
log.match(`omitted name of ${filePath} from hash`) | ||
} else { | ||
hash.write(name); | ||
} | ||
async function hashSymLinkPromise(name, dir, options, isRootElement = false) { | ||
const target = await fs.promises.readlink(path.join(dir, name)); | ||
log.symlink(`handling symbolic link ${name} -> ${target}`); | ||
if (options.symbolicLinks.include) { | ||
if (options.symbolicLinks.ignoreTargetContent) { | ||
return symLinkIgnoreTargetContent(name, target, options, isRootElement); | ||
} else { | ||
return symLinkResolve(name, dir, target, options, isRootElement); | ||
} | ||
} else { | ||
log.symlink('skipping symbolic link'); | ||
return Promise.resolve(undefined); | ||
} | ||
} | ||
const f = fs.createReadStream(filePath); | ||
f.pipe(hash, { end: false }); | ||
function symLinkIgnoreTargetContent(name, target, options, isRootElement) { | ||
delete options.skipMatching; // only used for the root level | ||
log.symlink('ignoring symbolic link target content'); | ||
const hash = crypto.createHash(options.algo); | ||
if (!options.symbolicLinks.ignoreBasename && !(isRootElement && options.files.ignoreRootName)) { | ||
log.symlink('hash basename'); | ||
hash.update(name); | ||
} | ||
if (!options.symbolicLinks.ignoreTargetPath) { | ||
log.symlink('hash targetpath'); | ||
hash.update(target); | ||
} | ||
return Promise.resolve(new HashedFile(name, hash, options.encoding)); | ||
} | ||
f.on('end', () => { | ||
const hashedFile = new HashedFile(name, hash, options.encoding); | ||
return resolve(hashedFile); | ||
}); | ||
} catch (ex) { | ||
return reject(ex); | ||
} | ||
}); | ||
async function symLinkResolve(name, dir, target, options, isRootElement) { | ||
delete options.skipMatching; // only used for the root level | ||
if (options.symbolicLinks.ignoreBasename) { | ||
options.ignoreBasenameOnce = true; | ||
} | ||
function ignore(name, path, rules) { | ||
if (rules.exclude) { | ||
if (rules.matchBasename && rules.exclude(name)) { | ||
log.match(`exclude basename '${path}'`); | ||
return true; | ||
} else if (rules.matchPath && rules.exclude(path)) { | ||
log.match(`exclude path '${path}'`); | ||
return true; | ||
} | ||
} else if (rules.include) { | ||
if (rules.matchBasename && rules.include(name)) { | ||
log.match(`include basename '${path}'`); | ||
return false; | ||
} else if (rules.matchPath && rules.include(path)) { | ||
log.match(`include path '${path}'`); | ||
return false; | ||
} else { | ||
return true; | ||
} | ||
try { | ||
const stats = await fs.promises.stat(path.join(dir, name)); | ||
stats.name = name; | ||
const temp = await hashElementPromise(stats, dir, options, isRootElement); | ||
if (!options.symbolicLinks.ignoreTargetPath) { | ||
const hash = crypto.createHash(options.algo); | ||
hash.update(temp.hash); | ||
log.symlink('hash targetpath'); | ||
hash.update(target); | ||
temp.hash = hash.digest(options.encoding); | ||
} | ||
return temp; | ||
} catch (err) { | ||
if (options.symbolicLinks.ignoreTargetContentAfterError) { | ||
log.symlink(`Ignoring error "${err.code}" when hashing symbolic link ${name}`, err); | ||
const hash = crypto.createHash(options.algo); | ||
if ( | ||
!options.symbolicLinks.ignoreBasename && | ||
!(isRootElement && options.files.ignoreRootName) | ||
) { | ||
hash.update(name); | ||
} | ||
if (!options.symbolicLinks.ignoreTargetPath) { | ||
hash.update(target); | ||
} | ||
return new HashedFile(name, hash, options.encoding); | ||
} else { | ||
log.symlink(`Error "${err.code}": When hashing symbolic link ${name}`, err); | ||
throw err; | ||
} | ||
} | ||
} | ||
log.match(`unmatched '${path}'`); | ||
function ignore(name, path, rules) { | ||
if (rules.exclude) { | ||
if (rules.matchBasename && rules.exclude(name)) { | ||
log.match(`exclude basename '${name}'`); | ||
return true; | ||
} else if (rules.matchPath && rules.exclude(path)) { | ||
log.match(`exclude path '${path}'`); | ||
return true; | ||
} | ||
} | ||
if (rules.include) { | ||
if (rules.matchBasename && rules.include(name)) { | ||
log.match(`include basename '${name}'`); | ||
return false; | ||
} else if (rules.matchPath && rules.include(path)) { | ||
log.match(`include path '${path}'`); | ||
return false; | ||
} else { | ||
log.match(`include rule failed for path '${path}'`); | ||
return true; | ||
} | ||
} | ||
const HashedFolder = function HashedFolder(name, children, options, isRootElement = false) { | ||
this.name = name; | ||
this.children = children; | ||
log.match(`Will not ignore unmatched '${path}'`); | ||
return false; | ||
} | ||
const hash = crypto.createHash(options.algo); | ||
if (options.folders.ignoreBasename || | ||
(isRootElement && options.folders.ignoreRootName)) | ||
{ | ||
log.match(`omitted name of folder ${name} from hash`) | ||
} else { | ||
hash.write(name); | ||
} | ||
children.forEach(child => { | ||
if (child.hash) { | ||
hash.write(child.hash); | ||
} | ||
}); | ||
return hashElement; | ||
} | ||
this.hash = hash.digest(options.encoding); | ||
}; | ||
function parseParameters(args) { | ||
let basename = args[0], | ||
dir = args[1], | ||
options_ = args[2]; | ||
HashedFolder.prototype.toString = function (padding = '') { | ||
const first = `${padding}{ name: '${this.name}', hash: '${this.hash}',\n`; | ||
padding += ' '; | ||
if (!isString(basename)) { | ||
return Promise.reject(new TypeError('First argument must be a string')); | ||
} | ||
return `${first}${padding}children: ${this.childrenToString(padding)}}`; | ||
}; | ||
if (!isString(dir)) { | ||
dir = path.dirname(basename); | ||
basename = path.basename(basename); | ||
options_ = args[1]; | ||
} | ||
HashedFolder.prototype.childrenToString = function (padding = '') { | ||
if (this.children.length === 0) { | ||
return '[]'; | ||
} else { | ||
const nextPadding = padding + ' '; | ||
const children = this.children | ||
.map(child => child.toString(nextPadding)) | ||
.join('\n'); | ||
return `[\n${children}\n${padding}]`; | ||
} | ||
}; | ||
// parse options (fallback default options) | ||
if (!isObject(options_)) options_ = {}; | ||
const options = { | ||
algo: options_.algo || defaultOptions.algo, | ||
encoding: options_.encoding || defaultOptions.encoding, | ||
files: Object.assign({}, defaultOptions.files, options_.files), | ||
folders: Object.assign({}, defaultOptions.folders, options_.folders), | ||
match: Object.assign({}, defaultOptions.match, options_.match), | ||
symbolicLinks: Object.assign({}, defaultOptions.symbolicLinks, options_.symbolicLinks), | ||
}; | ||
const HashedFile = function HashedFile(name, hash, encoding) { | ||
this.name = name; | ||
this.hash = hash.digest(encoding); | ||
}; | ||
// transform match globs to Regex | ||
options.files.exclude = reduceGlobPatterns(options.files.exclude); | ||
options.files.include = reduceGlobPatterns(options.files.include); | ||
options.folders.exclude = reduceGlobPatterns(options.folders.exclude); | ||
options.folders.include = reduceGlobPatterns(options.folders.include); | ||
HashedFile.prototype.toString = function (padding = '') { | ||
return padding + '{ name: \'' + this.name + '\', hash: \'' + this.hash + '\' }'; | ||
}; | ||
return hashElement; | ||
return Promise.resolve(log.params({ basename, dir, options })); | ||
} | ||
function parseParameters(args) { | ||
let basename = args[0], | ||
dir = args[1], | ||
options_ = args[2]; | ||
const HashedFolder = function HashedFolder(name, children, options, isRootElement = false) { | ||
this.name = name; | ||
this.children = children; | ||
if (!isString(basename)) { | ||
return Promise.reject(new TypeError('First argument must be a string')); | ||
const hash = crypto.createHash(options.algo); | ||
if ( | ||
options.folders.ignoreBasename || | ||
options.ignoreBasenameOnce || | ||
(isRootElement && options.folders.ignoreRootName) | ||
) { | ||
delete options.ignoreBasenameOnce; | ||
log.match(`omitted name of folder ${name} from hash`); | ||
} else { | ||
hash.update(name); | ||
} | ||
children.forEach(child => { | ||
if (child.hash) { | ||
hash.update(child.hash); | ||
} | ||
}); | ||
if (!isString(dir)) { | ||
dir = path.dirname(basename); | ||
basename = path.basename(basename); | ||
options_ = args[1]; | ||
} | ||
this.hash = hash.digest(options.encoding); | ||
}; | ||
// parse options (fallback default options) | ||
if (!isObject(options_)) options_ = {}; | ||
const options = { | ||
algo: options_.algo || defaultOptions.algo, | ||
encoding: options_.encoding || defaultOptions.encoding, | ||
files: Object.assign({}, defaultOptions.files, options_.files), | ||
folders: Object.assign({}, defaultOptions.folders, options_.folders), | ||
match: Object.assign({}, defaultOptions.match, options_.match) | ||
}; | ||
HashedFolder.prototype.toString = function (padding = '') { | ||
const first = `${padding}{ name: '${this.name}', hash: '${this.hash}',\n`; | ||
padding += ' '; | ||
// transform match globs to Regex | ||
options.files.exclude = reduceGlobPatterns(options.files.exclude); | ||
options.files.include = reduceGlobPatterns(options.files.include); | ||
options.folders.exclude = reduceGlobPatterns(options.folders.exclude); | ||
options.folders.include = reduceGlobPatterns(options.folders.include); | ||
return `${first}${padding}children: ${this.childrenToString(padding)}}`; | ||
}; | ||
return Promise.resolve(log.params({ basename, dir, options })); | ||
} | ||
HashedFolder.prototype.childrenToString = function (padding = '') { | ||
if (this.children.length === 0) { | ||
return '[]'; | ||
} else { | ||
const nextPadding = padding + ' '; | ||
const children = this.children.map(child => child.toString(nextPadding)).join('\n'); | ||
return `[\n${children}\n${padding}]`; | ||
} | ||
}; | ||
const HashedFile = function HashedFile(name, hash, encoding) { | ||
this.name = name; | ||
this.hash = hash.digest(encoding); | ||
}; | ||
HashedFile.prototype.toString = function (padding = '') { | ||
return padding + "{ name: '" + this.name + "', hash: '" + this.hash + "' }"; | ||
}; | ||
function isFunction(any) { | ||
return typeof any === 'function'; | ||
return typeof any === 'function'; | ||
} | ||
function isString(str) { | ||
return typeof str === 'string' || str instanceof String; | ||
return typeof str === 'string' || str instanceof String; | ||
} | ||
function isObject(obj) { | ||
return obj !== null && typeof obj === 'object'; | ||
return obj !== null && typeof obj === 'object'; | ||
} | ||
function notUndefined(obj) { | ||
return typeof obj !== 'undefined'; | ||
return typeof obj !== 'undefined'; | ||
} | ||
function reduceGlobPatterns(globs) { | ||
if (isFunction(globs)) { | ||
return globs; | ||
} | ||
else if (!globs || !Array.isArray(globs) || globs.length === 0) { | ||
return undefined; | ||
} else { | ||
// combine globs into one single RegEx | ||
const regex = new RegExp(globs.reduce((acc, exclude) => { | ||
return acc + '|' + minimatch.makeRe(exclude).source; | ||
}, '').substr(1)); | ||
return param => regex.test(param); | ||
} | ||
if (isFunction(globs)) { | ||
return globs; | ||
} else if (!globs || !Array.isArray(globs) || globs.length === 0) { | ||
return undefined; | ||
} else { | ||
// combine globs into one single RegEx | ||
const regex = new RegExp( | ||
globs | ||
.reduce((acc, exclude) => { | ||
return acc + '|' + minimatch.makeRe(exclude).source; | ||
}, '') | ||
.substr(1), | ||
); | ||
return param => regex.test(param); | ||
} | ||
} | ||
module.exports = { | ||
defaults: defaultOptions, | ||
hashElement: prep(require("graceful-fs"), Promise), | ||
// exposed for testing | ||
prep: prep, | ||
parseParameters: parseParameters | ||
defaults: defaultOptions, | ||
hashElement: prep(require('graceful-fs')), | ||
// exposed for testing | ||
prep, | ||
parseParameters, | ||
}; |
{ | ||
"name": "folder-hash", | ||
"version": "3.3.3", | ||
"version": "4.0.0", | ||
"description": "Create a hash checksum over a folder and its content - its children and their content", | ||
@@ -13,2 +13,3 @@ "main": "index.js", | ||
"cover": "nyc mocha test", | ||
"format": "prettier --write *.js examples/ test/", | ||
"doc": "./node_modules/.bin/jsdoc index.js -R README.md -d doc" | ||
@@ -45,11 +46,13 @@ }, | ||
"chai-as-promised": "^7.1.1", | ||
"clone": "^2.1.2", | ||
"ignore": "^5.1.2", | ||
"jsdoc": "3.6.4", | ||
"jsdoc": "3.6.6", | ||
"memfs": "^3.0.4", | ||
"mocha": "^8.0.1", | ||
"nyc": "^15.0.0" | ||
"nyc": "^15.0.0", | ||
"prettier": "~2.1.1" | ||
}, | ||
"engines": { | ||
"node": ">=6.0.0" | ||
"node": ">=10.10.0" | ||
} | ||
} |
198
README.md
@@ -5,11 +5,12 @@ Create a hash checksum over a folder or a file. | ||
Each file returns a name and a hash, and each folder returns additionally an array of children (file or folder elements). | ||
Each file returns a name and a hash, and each folder returns additionally an array of children (file or folder elements). | ||
## Usage | ||
First, install folder-hash with `npm install --save folder-hash` or `yarn add folder-hash`. | ||
## Usage | ||
First, install folder-hash with `npm install --save folder-hash` or `yarn add folder-hash`. | ||
### Simple example | ||
To see differences to the last version of this package, I would create hashes over all *.js* and *.json* files. But ignore everything inside folders starting wiht a dot, and also from the folders *node_modules*, *test_coverage*. The structure of the options object is documented <a href="#options">on this page.</a> | ||
This example is also stored in [./examples/readme-example1.js](/examples/readme-example1.js). | ||
To see differences to the last version of this package, I would create hashes over all _.js_ and _.json_ files. But ignore everything inside folders starting with a dot, and also from the folders _node_modules_, _test_coverage_. The structure of the options object is documented <a href="#options">below.</a> | ||
This example is also stored in [./examples/readme-example1.js](/examples/readme-example1.js). | ||
@@ -20,4 +21,4 @@ ```js | ||
const options = { | ||
folders: { exclude: ['.*', 'node_modules', 'test_coverage'] }, | ||
files: { include: ['*.js', '*.json'] } | ||
folders: { exclude: ['.*', 'node_modules', 'test_coverage'] }, | ||
files: { include: ['*.js', '*.json'] }, | ||
}; | ||
@@ -27,11 +28,12 @@ | ||
hashElement('.', options) | ||
.then(hash => { | ||
console.log(hash.toString()); | ||
}) | ||
.catch(error => { | ||
return console.error('hashing failed:', error); | ||
}); | ||
.then(hash => { | ||
console.log(hash.toString()); | ||
}) | ||
.catch(error => { | ||
return console.error('hashing failed:', error); | ||
}); | ||
``` | ||
The returned information looks for example like this: | ||
``` | ||
@@ -57,21 +59,24 @@ Creating a hash over the current folder: | ||
``` | ||
And the structure may be traversed to e.g. create incremental backups. | ||
It is also possible to only match the full path and not the basename. The same configuration could look like this: | ||
_You should be aware that *nix and Windows behave differently, so please use caution._ | ||
_You should be aware that \*nix and Windows behave differently, so please use caution._ | ||
```js | ||
const options = { | ||
folders: { | ||
exclude: ['.*', '**.*', '**node_modules', '**test_coverage'], | ||
matchBasename: false, matchPath: true | ||
}, | ||
files: { | ||
//include: ['**.js', '**.json' ], // Windows | ||
include: ['*.js', '**/*.js', '*.json', '**/*.json'], // *nix | ||
matchBasename: false, matchPath: true | ||
} | ||
folders: { | ||
exclude: ['.*', '**.*', '**node_modules', '**test_coverage'], | ||
matchBasename: false, | ||
matchPath: true, | ||
}, | ||
files: { | ||
//include: ['**.js', '**.json' ], // Windows | ||
include: ['*.js', '**/*.js', '*.json', '**/*.json'], // *nix | ||
matchBasename: false, | ||
matchPath: true, | ||
}, | ||
}; | ||
``` | ||
### Parameters for the hashElement function | ||
@@ -134,3 +139,5 @@ | ||
## Options | ||
### Default values | ||
```js | ||
@@ -154,2 +161,11 @@ { | ||
ignoreRootName: false | ||
}, | ||
symbolicLinks: { | ||
symbolicLinks: { | ||
include: true, | ||
ignoreBasename: false, | ||
ignoreTargetPath: true, | ||
ignoreTargetContent: false, | ||
ignoreTargetContentAfterError: false, | ||
} | ||
} | ||
@@ -220,2 +236,14 @@ } | ||
</tr> | ||
<tr> | ||
<td>symLinks</td> | ||
<td> | ||
<span>Object</span> | ||
</td> | ||
<td> | ||
<optional><br> | ||
</td> | ||
<td colspan="2"> | ||
<a href="#symlink-options">Symlink options (see below)</a> | ||
</td> | ||
</tr> | ||
</tbody> | ||
@@ -225,2 +253,3 @@ </table> | ||
#### Rules object properties | ||
<table> | ||
@@ -318,8 +347,80 @@ <thead> | ||
### Symlink options | ||
Configure, how symbolic links should be hashed. | ||
To understand how the options can be combined to create a specific behavior, look into [test/symbolic-links.js](https://github.com/marc136/node-folder-hash/blob/master/test/symbolic-links.js). | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Name</th> | ||
<th>Type</th> | ||
<th>Default</th> | ||
<th>Description</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td>include</td> | ||
<td> | ||
<span>bool</span> | ||
</td> | ||
<td> | ||
true | ||
</td> | ||
<td>If false, symbolic links are not handled at all. A folder with three symbolic links inside will have no children entries.</td> | ||
</tr> | ||
<tr> | ||
<td>ignoreBasename</td> | ||
<td> | ||
<span>bool</span> | ||
</td> | ||
<td> | ||
false | ||
</td> | ||
<td>Set to true to calculate the hash without the basename element</td> | ||
</tr> | ||
<tr> | ||
<td>ignoreTargetPath</td> | ||
<td> | ||
<span>bool</span> | ||
</td> | ||
<td> | ||
true | ||
</td> | ||
<td>If false, the resolved link target is added to the hash (uses <a href="https://devdocs.io/node/fs#fs_fs_readlink_path_options_callback">fs.readlink</a>)</td> | ||
</tr> | ||
<tr> | ||
<td>ignoreTargetContent</td> | ||
<td> | ||
<span>bool</span> | ||
</td> | ||
<td> | ||
false | ||
</td> | ||
<td>If true, will only assess the basename and target path (as configured in the other options)</td> | ||
</tr> | ||
<tr> | ||
<td>ignoreTargetContentAfterError</td> | ||
<td> | ||
<span>bool</span> | ||
</td> | ||
<td> | ||
false | ||
</td> | ||
<td>If true, will ignore all errors while trying to hash symbolic links and only assess the basename and target path (as configured in other options).<br />E.g. a missing target (<i>ENOENT</i>) or access permissions (<i>EPERM</i>).</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
## Command line usage | ||
After installing it globally via | ||
``` | ||
$ npm install -g folder-hash | ||
``` | ||
You can use it like this: | ||
``` | ||
@@ -337,2 +438,3 @@ # local folder | ||
You can also use a local version of folder-hash like this: | ||
``` | ||
@@ -345,4 +447,7 @@ $ npx folder-hash --help | ||
## Examples | ||
### Other examples using promises | ||
See file *./examples/readme-with-promises.js* | ||
See file _./examples/readme-with-promises.js_ | ||
```js | ||
@@ -384,4 +489,5 @@ const path = require('path'); | ||
### Other examples using error-first callbacks | ||
See *./examples/readme-with-callbacks.js* | ||
See _./examples/readme-with-callbacks.js_ | ||
```js | ||
@@ -393,7 +499,7 @@ const path = require('path'); | ||
hashElement('test', path.join(__dirname, '..'), (error, hash) => { | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "../test":', hash.toString(), '\n'); | ||
} | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "../test":', hash.toString(), '\n'); | ||
} | ||
}); | ||
@@ -403,8 +509,8 @@ | ||
hashElement(__dirname, (error, hash) => { | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "' + __dirname + '":'); | ||
console.log(hash.toString(), '\n'); | ||
} | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "' + __dirname + '":'); | ||
console.log(hash.toString(), '\n'); | ||
} | ||
}); | ||
@@ -415,18 +521,18 @@ | ||
hashElement(__dirname, options, (error, hash) => { | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "' + __dirname + '":'); | ||
console.log(hash.toString()); | ||
} | ||
if (error) { | ||
return console.error('hashing failed:', error); | ||
} else { | ||
console.log('Result for folder "' + __dirname + '":'); | ||
console.log(hash.toString()); | ||
} | ||
}); | ||
``` | ||
## Behavior | ||
## Behavior | ||
The behavior is documented and verified in the unit tests. Execute `npm test` or `mocha test`, and have a look at the _test_ subfolder. | ||
You can also have a look at the [CircleCI report. ![CircleCI](https://circleci.com/gh/marc136/node-folder-hash/tree/master.svg?style=svg)](https://circleci.com/gh/marc136/node-folder-hash/tree/master) | ||
### Creating hashes over files (with default options) | ||
### Creating hashes over files (with default options) | ||
**The hashes are the same if:** | ||
@@ -444,2 +550,3 @@ | ||
### Creating hashes over folders (with default options) | ||
Content means in this case a folder's children - both the files and the subfolders with their children. | ||
@@ -459,2 +566,3 @@ | ||
## License | ||
MIT, see LICENSE.txt |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
31475
409
552
9