Source: index.js

const crypto = require('crypto')
const fs = require('fs/promises')
const path = require('path')
const util = require('util')

const workingDir = process.cwd()

/**
 * @typedef {Object} AutoHashConfigWithFile - configuration of AutoHash
 * @property {string} c - config file path
 * @property {string} config - config file path
 */

/**
 * @typedef {Object} AutoHashConfigWithObject - configuration of AutoHash
 * @property {Object[]} files - file list to be hashed
 * @property {string} files[].file - file path
 * @property {string=} files[].name - key to use in hash object
 * @property {{file: string=}=} output - output file path of hashes
 * @property {number=} len - length of MD5 to be used
 * @property {boolean=} rename - rename original file to originalFilename.hash.ext
 * @property {boolean=} copy - create a copy of original file in originalFilename.hash.ext
 */

/**
 * @typedef {AutoHashConfigWithFile|AutoHashConfigWithObject} AutoHashConfig - configuration
 */

/**
 * calculate MD5 of the file buffer.
 * @param {Object} params
 * @param {Buffer} params.buffer - file buffer
 * @param {number=} params.length - hash length
 * @return {string} MD5 hash
 * @private
 */
function fileMD5({
  buffer,
  length,
}) {
  const fsHash = crypto.createHash('md5')
  fsHash.update(buffer)
  let hash = fsHash.digest('hex')
  if (length) {
    hash = hash.substring(0, length)
  }
  return hash
}

/**
 * generate new file path in /original/file/path/originalFilename.hash.ext
 * @param {string} filePath - original file path
 * @param {string} hash - file hash
 * @return {string} - file path with hash in file name
 * @private
 */
function cpFilePath(filePath, hash) {
  const pathObj = path.parse(filePath)
  if (pathObj.base) {
    const base = pathObj.base.split('.')
    base.splice(-1, 0, hash)
    pathObj.base = base.join('.')
  } else {
    pathObj.name += `.${hash}`
  }
  return path.format(pathObj)
}

/**
 * rename original file to orignalFilename.hash.ext
 * @param {string} filePath 
 * @param {string} hash 
 * @return {Promise}
 * @private
 */
function renameFile(filePath, hash) {
  const newPath = cpFilePath(filePath, hash)
  return fs.rename(filePath, newPath)
}

/**
 * create a copy of original file in same path in originalFilename.hash.ext
 * @param {string} filePath 
 * @param {string} hash  
 * @return {Promise}
 * @private
 */
function copyFile(filePath, hash) {
  const newPath = cpFilePath(filePath, hash)
  return fs.copyFile(filePath, newPath)
}

/**
 * load specific config file
 * @param {string} configPath 
 * @private
 */
async function loadConfig(configPath) {
  const configContent = await fs.readFile(path.resolve(workingDir, configPath))
  return JSON.parse(configContent.toString())
}

/**
 * 1. load auto-hash config(config file or parameter)
 * 2. generate hash value of each file
 * 2.1 (optional) rename file or create a copy of it
 * 2.2 assign hashes to an object
 * 3. (optional) output hashes to file
 * 4. return hashes
 * @param {AutoHashConfig} argv - AutoHash configuration
 * @return {Promise<Object.<string, string>>} - hashes
 */
async function autoHash(argv = {}) {
  /**
   * @type {AutoHashConfig}
   */
  let config
  if (argv.c || argv.config) {
    const configFilePath = argv.c || argv.config
    config = await loadConfig(configFilePath)
  } else if (Array.isArray(argv.files)) {
    config = argv
  } else {
    config = await loadConfig('./auto-hash.config.json')
  }
  if (!(Array.isArray(config.files) && config.files.length)) {
    throw new Error('Missing file list')
  }
  const hashes = {}
  await Promise.all(config.files.map(async fileObj => {
    let filePath
    let fileHash
    if (typeof fileObj === 'object') {
      if (!fileObj.file) {
        return
      }
      filePath = path.resolve(workingDir, fileObj.file)
      const file = await fs.readFile(filePath)
      fileHash = fileMD5({
        buffer: file,
        length: config.len,
      })
      if (fileObj.name) {
        hashes[fileObj.name] = fileHash
      } else {
        const fileInfo = path.parse(filePath)
        hashes[fileInfo.name] = fileHash
      }
    } else if (typeof fileObj === 'string') {
      filePath = path.resolve(workingDir, fileObj)
      const file = await fs.readFile(filePath)
      fileHash = fileMD5({
        buffer: file,
        length: config.len,
      })
      const fileInfo = path.parse(filePath)
      hashes[fileInfo.name] = fileHash
    }
    if (config.rename) {
      return renameFile(filePath, fileHash)
    } else if (config.copy) {
      return copyFile(filePath, fileHash)
    }
  }))
  if (config.output && config.output.file) {
    const fileContent = `module.exports = ${util.inspect(hashes)}`
    await fs.writeFile(config.output.file, fileContent)
  }
  return hashes
}

/**
 * AutoHash
 * @exports autoHash
 */
module.exports = autoHash