UtilsFile.mjs

import { createReadStream, createWriteStream } from "node:fs";
import path from "node:path";
import fsPromise from "node:fs/promises";
import crypto from "node:crypto";
import archiver from "archiver";
import unzipper from "unzipper";
import { filesize } from "filesize";
import { ensureDir } from "fs-extra/esm";

/**
 * Utility functions for working with files.
 * @module File
 */

/**
 * Generates a safe file name by replacing special characters with underscores.
 * Consecutive underscores are also reduced to a single underscore.
 *
 * @param {string} fileName - The original file name to be sanitized.
 * @returns {string} - The sanitized file name with special characters replaced by underscores and consecutive underscores reduced to a single underscore.
 *
 * @example
 * import { getSafeFileName } from "nsuite";
 * const safeFileName = getSafeFileName("测试有空格 和特殊符号 &.pdf");
 */
export function getSafeFileName(fileName) {
  return fileName
    .replace(/[+\s??!@#¥%…&*()=·~!$^()/<>,;':"[\]{}]/g, "_")
    .replace(/__/g, "_");
}

/**
 * Zips a single file and returns a promise that resolves when the zip operation is complete.
 *
 * @param {Object} options - The options for the zip operation.
 * @param {string} options.pathInputFile - The path to the input file to be zipped.
 * @param {string} options.pathOutputFile - The path to the output zip file.
 * @returns {Promise<number>} - A promise that resolves with final zip file size in Bytes when the zip operation is complete.
 *
 * @example
 * import { zipFile } from "nsuite";
 * await zipFile({
 *   pathInputFile: "./package.json",
 *   pathOutputFile: "./package.json.zip",
 * });
 */
export async function zipFile(options) {
  const { pathInputFile, pathOutputFile } = options;
  const outputDir = path.dirname(pathOutputFile);
  await ensureDir(outputDir);
  const outputStream = createWriteStream(pathOutputFile);
  const archive = archiver("zip", {
    zlib: { level: 9 },
  });
  archive.pipe(outputStream);
  archive.append(createReadStream(pathInputFile), {
    name: path.basename(pathInputFile),
  });
  return new Promise((resolve, reject) => {
    outputStream.on("close", () => {
      resolve(archive.pointer());
    });
    outputStream.on("error", (err) => {
      reject(err);
    });
    archive.on("error", (err) => {
      reject(err);
    });
    archive.finalize();
  });
}

/**
 * Zips a folder and returns a promise that resolves when the zip operation is complete.
 *
 * @param {Object} options - The options for the zip operation.
 * @param {string} options.pathFolder - The path to the folder to be zipped.
 * @param {string} options.pathOutputFile - The path to the output zip file.
 * @returns {Promise<number>} - A promise that resolves with final zip file size in Bytes when the zip operation is complete.
 *
 * @example
 * import { zipFolder } from "nsuite";
 * await zipFolder({
 *   pathFolder: "./dist",
 *   pathOutputFile: "./dist.zip",
 * });
 */
export async function zipFolder(options) {
  const { pathFolder, pathOutputFile } = options;
  const outputDir = path.dirname(pathOutputFile);
  await ensureDir(outputDir);
  const outputStream = createWriteStream(pathOutputFile);
  const archive = archiver("zip", {
    zlib: { level: 9 },
  });
  archive.pipe(outputStream);
  archive.directory(pathFolder, false);
  return await new Promise((resolve, reject) => {
    outputStream.on("close", () => {
      resolve(archive.pointer());
    });
    outputStream.on("error", (err) => {
      reject(err);
    });
    archive.on("error", (/** @type {any} */ err) => {
      reject(err);
    });
    archive.finalize();
  });
}

/**
 * Unzips a file and returns a promise that resolves when the unzip operation is complete.
 *
 * @param {Object} options - The options for the unzip operation.
 * @param {string} options.pathFile - The path to the zip file to be unzipped.
 * @param {string} options.pathOutput - The path to the output directory.
 * @returns {Promise<void>} - A promise that resolves when the unzip operation is complete.
 *
 * @example
 * import { unzipFile } from "nsuite";
 *
 * await unzipFile({
 *   pathFile: pathDistZip,
 *   pathOutput: pathOutputDirectory,
 * });
 */
export async function unzipFile({ pathFile, pathOutput }) {
  const directory = await unzipper.Open.file(pathFile);
  await directory.extract({ path: pathOutput });
}

/**
 * @typedef {import('filesize').FilesizeOptions} CustomFilesizeOptions
 * @property {string} [output] - output format, only 'string' is supported
 */

/**
 * converts file size in bytes to human-readable string
 * @param {number | string} size
 * @param {CustomFilesizeOptions} [options]
 * @returns {string}
 *
 * @example
 * import { getReadableFileSize } from "nsuite";
 *
 * getReadableFileSize(0); // "0 B"
 *
 * // 1024-based, with { standard: "jedec" }
 * getReadableFileSize(1024, { standard: "jedec" }); // "1 KB"
 * getReadableFileSize(1024 * 1024, { standard: "jedec" }); // "1 MB"
 * getReadableFileSize(1024 * 1024 * 1024, { standard: "jedec" }); // "1 GB"
 *
 * // 1000-based, default
 * getReadableFileSize(1000); // "1 kB"
 * getReadableFileSize(1001); // "1 kB"
 * getReadableFileSize(1010); // "1.01 kB"
 * getReadableFileSize(1100); // 1.1 kB"
 * getReadableFileSize(1024); // "1.02 kB"
 * getReadableFileSize(1024 * 1000); // "1.02 MB"
 * // 1024 * 1024 = 1048576
 * getReadableFileSize(1024 * 1024); // "1.05 MB"
 * // 1024 * 1024 * 1024 = 1073741824
 * getReadableFileSize(1024 * 1024 * 1024); // "1.07 GB"
 */
export function getReadableFileSize(size, options) {
  return filesize(size, options);
}

/**
 * Calculates the MD5 hash of a file and returns a promise that resolves with the hash.
 *
 * @param {Object} options - The options for the hash operation.
 * @param {string} options.pathFile - The path to the file for which the hash is to be calculated.
 * @returns {Promise<string>} - A promise that resolves with the MD5 hash of the file.
 *
 * @example
 * import { getFileMd5 } from "nsuite";
 * const md5 = await getFileMd5({ pathFile: "./package.json" });
 */
export async function getFileMd5({ pathFile }) {
  const zipBuffer = await fsPromise.readFile(pathFile);
  const hash = crypto.createHash("md5");
  hash.update(zipBuffer);
  return hash.digest("hex");
}