Skip to content

Node.js 实现 gzip 和 br 压缩类

github https://github.com/xieerduos/nodejs-br-gzip-class

下面是一个扩展的 Node.js 类,该类支持文件和文件夹的压缩与解压。

如果未指定输出路径,则会在原文件或文件夹的同级目录下创建.br.gz文件。

javascript
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const { promisify } = require("util");
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);

class FileCompressor {
  constructor() {
    this.brotliOptions = {
      params: {
        [zlib.constants.BROTLI_PARAM_QUALITY]:
          zlib.constants.BROTLI_MAX_QUALITY,
      },
    };
    this.gzipOptions = {
      level: zlib.constants.Z_MAX_LEVEL,
    };
  }

  async compressFile(inputPath, outputPath, compressFunction) {
    return new Promise((resolve, reject) => {
      const output = fs.createWriteStream(outputPath);
      const compressionStream = compressFunction();

      fs.createReadStream(inputPath)
        .pipe(compressionStream)
        .pipe(output)
        .on("finish", () => resolve(`Compression completed: ${outputPath}`))
        .on("error", reject);
    });
  }

  async decompressFile(inputPath, outputPath, decompressFunction) {
    return new Promise((resolve, reject) => {
      const output = fs.createWriteStream(outputPath);
      const decompressionStream = decompressFunction();

      fs.createReadStream(inputPath)
        .pipe(decompressionStream)
        .pipe(output)
        .on("finish", () => resolve(`Decompression completed: ${outputPath}`))
        .on("error", reject);
    });
  }

  async compressDirectory(inputPath, outputPath, compressFunction, extension) {
    const files = await readdir(inputPath);
    for (let file of files) {
      const fullInputPath = path.join(inputPath, file);
      const fullOutputPath = path.join(outputPath, `${file}${extension}`);
      const fileInfo = await stat(fullInputPath);

      if (fileInfo.isDirectory()) {
        await this.compressDirectory(
          fullInputPath,
          fullOutputPath,
          compressFunction,
          extension
        );
      } else {
        if (!fs.existsSync(fullOutputPath)) {
          await this.compressFile(
            fullInputPath,
            fullOutputPath,
            compressFunction
          );
        }
      }
    }
  }

  async compress(inputPath, outputPath, useBrotli = true) {
    const fileInfo = await stat(inputPath);
    const extension = useBrotli ? ".br" : ".gz";
    const compressFunction = useBrotli
      ? () => zlib.createBrotliCompress(this.brotliOptions)
      : () => zlib.createGzip(this.gzipOptions);

    if (!outputPath) {
      outputPath = inputPath + extension;
    }

    if (fileInfo.isDirectory()) {
      await this.compressDirectory(
        inputPath,
        outputPath,
        compressFunction,
        extension
      );
    } else {
      if (fs.existsSync(outputPath)) {
        fs.unlinkSync(outputPath);
      }
      await this.compressFile(inputPath, outputPath, compressFunction);
    }
  }

  async decompress(inputPath, outputPath, useBrotli = true) {
    const fileInfo = await stat(inputPath);
    const decompressFunction = useBrotli
      ? () => zlib.createBrotliDecompress()
      : () => zlib.createGunzip();

    if (!outputPath) {
      // Remove the extension (.br or .gz) for the decompressed file's path
      outputPath = inputPath.replace(
        new RegExp(`${useBrotli ? ".br$" : ".gz$"}`),
        ""
      );
    }

    if (fileInfo.isDirectory()) {
      throw new Error(
        "Decompressing directories is not supported directly. Please decompress individual files."
      );
    } else {
      await this.decompressFile(inputPath, outputPath, decompressFunction);
    }
  }
}

module.exports = FileCompressor;

使用说明:

  1. 创建FileCompressor类的实例。
  2. 使用实例的compress方法进行压缩,它接受三个参数:inputPath(输入文件或文件夹的路径),outputPath(可选,输出文件或文件夹的路径),useBrotli(布尔值,表示是否使用 Brotli 压缩,默认为 true)。

示例代码:

javascript
// @ts-ignore
const FileCompressor = require("./FileCompressor.js");
const compressor = new FileCompressor();
const fs = require("fs");
const path = require("path");

main();

async function main() {
  const filePath = path.join(__dirname, "test.jpg");
  if (!fs.existsSync(filePath)) {
    console.log(`filePath: ${filePath} 路径不存在`);
    return;
  }

  if (fs.existsSync(filePath + ".br")) {
    fs.unlinkSync(filePath + ".br");
  }
  if (fs.existsSync("./test2.jpg")) {
    fs.unlinkSync("./test2.jpg");
  }

  await new Promise((resolve) => setTimeout(resolve, 1000));

  // br 压缩
  await compressor.compress(filePath).then(console.log).catch(console.error);
  // gzip 压缩
  await compressor
    .compress(filePath, undefined, false)
    .then(console.log)
    .catch(console.error);
  // 解压
  await compressor
    .decompress("test.jpg.br", "test2.jpg")
    .then(console.log)
    .catch(console.error);
}

更多示例

js
const compressor = new FileCompressor();

// 压缩单个文件,自动在同一位置创建 .br 文件
compressor
  .compress("path/to/input.txt")
  .then(console.log) // 输出成功消息
  .catch(console.error); // 捕获并显示错误

// 压缩单个文件,使用Gzip而非Brotli
compressor
  .compress("path/to/input.txt", undefined, false)
  .then(console.log) // 输出成功消息
  .catch(console.error); // 捕获并显示错误

// 压缩文件夹,自动在同一位置创建带 .br 扩展的文件夹
compressor
  .compress("path/to/inputFolder")
  .then(console.log) // 输出成功消息
  .catch(console.error); // 捕获并显示错误

// 压缩文件夹,使用Gzip压缩
compressor
  .compress("path/to/inputFolder", undefined, false)
  .then(console.log) // 输出成功消息
  .catch(console.error); // 捕获并显示错误