zip-a-folder
Version:
Compress a complete folder or a glob list into a zip/tgz/br/7z file
1,433 lines (1,421 loc) • 48.5 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
import * as zlib from "node:zlib";
import * as fs$1 from "fs";
import * as path$1 from "path";
import * as zlib$1 from "zlib";
import LZMA from "lzma";
import { globSync } from "tinyglobby";
//#region lib/7z/Native7z.ts
/**
* 7z archive format constants
*/
const SIGNATURE = Buffer.from([
55,
122,
188,
175,
39,
28
]);
const VERSION_MAJOR = 0;
const VERSION_MINOR = 4;
const kEnd = 0;
const kHeader = 1;
const kMainStreamsInfo = 4;
const kFilesInfo = 5;
const kPackInfo = 6;
const kUnpackInfo = 7;
const kSubStreamsInfo = 8;
const kSize = 9;
const kFolder = 11;
const kCodersUnpackSize = 12;
const kNumUnpackStream = 13;
const kNames = 17;
const kMTime = 20;
const kAttributes = 21;
const LZMA_CODEC_ID = Buffer.from([
3,
1,
1
]);
/**
* CRC-32 calculation (same as used in ZIP)
*/
const CRC32_TABLE$1 = [];
for (let i = 0; i < 256; i++) {
let crc = i;
for (let j = 0; j < 8; j++) crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
CRC32_TABLE$1[i] = crc >>> 0;
}
function crc32$1(data) {
let crc = 4294967295;
for (let i = 0; i < data.length; i++) crc = CRC32_TABLE$1[(crc ^ data[i]) & 255] ^ crc >>> 8;
return (crc ^ 4294967295) >>> 0;
}
/**
* Simple 7z number encoding (used for headers)
*/
/* c8 ignore start - large number encoding only triggered with files >127 bytes compressed */
function encode7zNumber(value) {
if (value < 128) return Buffer.from([value]);
const buf = Buffer.alloc(9);
buf[0] = 255;
buf.writeBigUInt64LE(BigInt(value), 1);
return buf;
}
/* c8 ignore stop */
/**
* Write Windows FILETIME (100-nanosecond intervals since 1601-01-01)
*/
function dateToFiletime(date) {
const EPOCH_DIFF = BigInt(0xa9730b66800);
return (BigInt(date.getTime()) + EPOCH_DIFF) * BigInt(1e4);
}
/**
* Native 7z archive writer using pure JavaScript LZMA compression.
*/
var Native7z = class {
/**
* @param compressionLevel LZMA compression level 1-9 (default: 5)
*/
constructor(compressionLevel = 5) {
this.files = [];
this.compressionLevel = Math.max(1, Math.min(9, compressionLevel));
}
/**
* Add a file to the archive from filesystem.
*/
async addFile(entry) {
if (entry.isDirectory) return;
const data = await fs$1.promises.readFile(entry.fsPath);
const compressedData = await this.compressLzma(data);
this.files.push({
entry,
data,
compressedData
});
}
/**
* Compress data using LZMA.
*/
compressLzma(data) {
return new Promise((resolve, reject) => {
LZMA.compress(data, this.compressionLevel, (result, error) => {
/* c8 ignore start - LZMA error handling is hard to trigger in tests */
if (error || result instanceof Error) {
reject(error || result);
return;
}
/* c8 ignore stop */
const unsigned = result.map((b) => b < 0 ? b + 256 : b);
resolve(Buffer.from(unsigned));
});
});
}
/**
* Finalize and write the archive to a stream.
*/
async writeToStream(outStream) {
if (this.files.length === 0) throw new Error("Cannot create empty 7z archive");
const archiveData = await this.buildArchive();
return new Promise((resolve, reject) => {
outStream.on("error", reject);
outStream.on("finish", resolve);
outStream.write(archiveData, (err) => {
/* c8 ignore start */
if (err) reject(err);
else outStream.end?.();
/* c8 ignore stop */
});
});
}
/**
* Build the complete 7z archive.
*/
async buildArchive() {
const packedData = Buffer.concat(this.files.map((f) => f.compressedData));
const header = this.buildHeader();
const compressedHeader = await this.compressLzma(header);
const nextHeaderOffset = BigInt(packedData.length);
const nextHeaderSize = BigInt(compressedHeader.length);
const nextHeaderCRC = crc32$1(compressedHeader);
const startHeader = Buffer.alloc(20);
startHeader.writeBigUInt64LE(nextHeaderOffset, 0);
startHeader.writeBigUInt64LE(nextHeaderSize, 8);
startHeader.writeUInt32LE(nextHeaderCRC, 16);
const startHeaderCRC = crc32$1(startHeader);
const signatureHeader = Buffer.alloc(32);
SIGNATURE.copy(signatureHeader, 0);
signatureHeader[6] = VERSION_MAJOR;
signatureHeader[7] = VERSION_MINOR;
signatureHeader.writeUInt32LE(startHeaderCRC, 8);
startHeader.copy(signatureHeader, 12);
return Buffer.concat([
signatureHeader,
packedData,
compressedHeader
]);
}
/**
* Build the 7z header with file metadata.
*/
buildHeader() {
const parts = [];
parts.push(Buffer.from([kHeader]));
parts.push(this.buildMainStreamsInfo());
parts.push(this.buildFilesInfo());
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build MainStreamsInfo section.
*/
buildMainStreamsInfo() {
const parts = [];
parts.push(Buffer.from([kMainStreamsInfo]));
parts.push(this.buildPackInfo());
parts.push(this.buildUnpackInfo());
parts.push(this.buildSubStreamsInfo());
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build PackInfo section (packed/compressed sizes).
*/
buildPackInfo() {
const parts = [];
parts.push(Buffer.from([kPackInfo]));
parts.push(encode7zNumber(0));
parts.push(encode7zNumber(this.files.length));
parts.push(Buffer.from([kSize]));
for (const file of this.files) parts.push(encode7zNumber(file.compressedData.length));
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build UnpackInfo section (codec and uncompressed sizes).
*/
buildUnpackInfo() {
const parts = [];
parts.push(Buffer.from([kUnpackInfo]));
parts.push(Buffer.from([kFolder]));
parts.push(encode7zNumber(this.files.length));
parts.push(Buffer.from([0]));
for (const _file of this.files) {
parts.push(Buffer.from([1]));
const coderFlags = LZMA_CODEC_ID.length;
parts.push(Buffer.from([coderFlags]));
parts.push(LZMA_CODEC_ID);
}
parts.push(Buffer.from([kCodersUnpackSize]));
for (const file of this.files) parts.push(encode7zNumber(file.data.length));
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build SubStreamsInfo section.
*/
buildSubStreamsInfo() {
const parts = [];
parts.push(Buffer.from([kSubStreamsInfo]));
parts.push(Buffer.from([kNumUnpackStream]));
for (let i = 0; i < this.files.length; i++) parts.push(encode7zNumber(1));
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build FilesInfo section (file names, attributes, times).
*/
buildFilesInfo() {
const parts = [];
parts.push(Buffer.from([kFilesInfo]));
parts.push(encode7zNumber(this.files.length));
parts.push(this.buildFileNames());
parts.push(this.buildMTimes());
parts.push(this.buildAttributes());
parts.push(Buffer.from([kEnd]));
return Buffer.concat(parts);
}
/**
* Build file names section (UTF-16LE encoded).
*/
buildFileNames() {
const parts = [];
parts.push(Buffer.from([kNames]));
const namesData = [];
for (const file of this.files) {
const name = file.entry.relativePath.replace(/\//g, "\\");
const nameBuffer = Buffer.from(name, "utf16le");
namesData.push(nameBuffer);
namesData.push(Buffer.from([0, 0]));
}
const allNames = Buffer.concat(namesData);
parts.push(encode7zNumber(allNames.length + 1));
parts.push(Buffer.from([0]));
parts.push(allNames);
return Buffer.concat(parts);
}
/**
* Build modification times section.
*/
buildMTimes() {
const parts = [];
parts.push(Buffer.from([kMTime]));
const dataSize = 2 + 8 * this.files.length;
parts.push(encode7zNumber(dataSize));
parts.push(Buffer.from([1]));
parts.push(Buffer.from([0]));
for (const file of this.files) {
const buf = Buffer.alloc(8);
buf.writeBigUInt64LE(dateToFiletime(file.entry.stat.mtime));
parts.push(buf);
}
return Buffer.concat(parts);
}
/**
* Build Windows attributes section.
*/
buildAttributes() {
const parts = [];
parts.push(Buffer.from([kAttributes]));
const dataSize = 2 + 4 * this.files.length;
parts.push(encode7zNumber(dataSize));
parts.push(Buffer.from([1]));
parts.push(Buffer.from([0]));
for (const file of this.files) {
const buf = Buffer.alloc(4);
/* c8 ignore next */
const attr = file.entry.isDirectory ? 16 : 32;
buf.writeUInt32LE(attr);
parts.push(buf);
}
return Buffer.concat(parts);
}
};
//#endregion
//#region lib/core/FileCollector.ts
/**
* Recursively collect entries (files + directories) under a directory,
* using a worker pool limited by statConcurrency.
*
* - The root directory itself is NOT returned as an entry.
* - Directory entries have relativePath ending with "/".
* - Entries matching any pattern in `exclude` are omitted (along with their children).
*/
async function collectEntriesFromDirectory(rootDir, statConcurrency = 4, exclude) {
const root = path$1.resolve(rootDir);
const entries = [];
const relPaths = globSync("**/*", {
cwd: root,
onlyFiles: false,
dot: true,
ignore: exclude
});
statConcurrency = Math.max(1, statConcurrency | 0);
let index = 0;
const worker = async () => {
while (true) {
const i = index++;
if (i >= relPaths.length) break;
const rel = relPaths[i];
const fsPath = path$1.join(root, rel);
const stat = await fs$1.promises.lstat(fsPath);
if (stat.isSymbolicLink()) continue;
if (stat.isDirectory()) {
/* v8 ignore next */
const relDirPath = rel.endsWith("/") ? rel : rel + "/";
entries.push({
fsPath,
relativePath: relDirPath.replace(/\\/g, "/"),
isDirectory: true,
stat
});
} else if (stat.isFile()) entries.push({
fsPath,
relativePath: rel.replace(/\\/g, "/"),
isDirectory: false,
stat
});
}
};
const workers = Array.from({ length: statConcurrency }, () => worker());
await Promise.all(workers);
return entries;
}
/**
* Collect file entries from one or more glob patterns, relative to a given cwd.
*
* - Uses the "tinyglobby" package.
* - Only files are returned (no explicit directory entries).
* - Returns [] when there is no match (caller will throw "No glob match found").
*/
async function collectGlobEntries(patterns, cwd, statConcurrency = 4, exclude) {
const patternList = patterns.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
const matchedRelPaths = /* @__PURE__ */ new Set();
for (const pattern of patternList) try {
const matches = globSync(pattern, {
cwd,
onlyFiles: true,
dot: false,
ignore: exclude
});
for (const rel of matches) matchedRelPaths.add(rel.replace(/\\/g, "/"));
} catch {}
const relPaths = Array.from(matchedRelPaths);
if (relPaths.length === 0) return [];
const entries = [];
let index = 0;
statConcurrency = Math.max(1, statConcurrency | 0);
const worker = async () => {
while (true) {
const i = index++;
if (i >= relPaths.length) break;
const rel = relPaths[i];
const abs = path$1.resolve(cwd, rel);
const stat = await fs$1.promises.stat(abs);
/* v8 ignore next 3 */
if (!stat.isFile()) continue;
entries.push({
fsPath: abs,
relativePath: rel,
isDirectory: false,
stat
});
}
};
const workers = Array.from({ length: statConcurrency }, () => worker());
await Promise.all(workers);
return entries;
}
//#endregion
//#region lib/core/utils.ts
/**
* Precomputed CRC32 table for efficient checksum calculation.
*/
const CRC32_TABLE = (() => {
const table = /* @__PURE__ */ new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) if (c & 1) c = 3988292384 ^ c >>> 1;
else c = c >>> 1;
table[i] = c >>> 0;
}
return table;
})();
/**
* Compute CRC32 checksum for a buffer.
*/
function crc32(buf) {
let crc = 4294967295;
for (let i = 0; i < buf.length; i++) {
const byte = buf[i];
crc = CRC32_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
}
return (crc ^ 4294967295) >>> 0;
}
/**
* Convert a Date to DOS time (HH:MM:SS/2).
* @param date Date to convert.
* @param useLocal When true use local time, otherwise UTC.
*/
function dateToDosTime(date, useLocal) {
const secs = Math.floor((useLocal ? date.getSeconds() : date.getUTCSeconds()) / 2);
const mins = useLocal ? date.getMinutes() : date.getUTCMinutes();
return (useLocal ? date.getHours() : date.getUTCHours()) << 11 | mins << 5 | secs;
}
/**
* Convert a Date to DOS date.
* @param date Date to convert.
* @param useLocal When true use local time, otherwise UTC.
*/
function dateToDosDate(date, useLocal) {
const year = useLocal ? date.getFullYear() : date.getUTCFullYear();
const month = (useLocal ? date.getMonth() : date.getUTCMonth()) + 1;
const day = useLocal ? date.getDate() : date.getUTCDate();
return (year < 1980 ? 0 : year - 1980) << 9 | month << 5 | day;
}
/**
* Write an ASCII string into a buffer with a fixed maximum length.
*/
function writeString(buf, str, offset, length) {
const bytes = Buffer.from(str, "utf8");
bytes.copy(buf, offset, 0, Math.min(bytes.length, length));
}
/**
* Write an octal number into a buffer as ASCII text.
*/
function writeOctal(buf, value, offset, length) {
writeString(buf, value.toString(8).padStart(length - 1, "0"), offset, length - 1);
buf[offset + length - 1] = 0;
}
/**
* Write a 64-bit unsigned integer (up to 2^53-1) to a buffer in little-endian order.
*/
function writeUInt64LE(buf, value, offset) {
const low = value >>> 0;
const high = Math.floor(value / 4294967296) >>> 0;
buf.writeUInt32LE(low, offset);
buf.writeUInt32LE(high, offset + 4);
}
/**
* Helper to write data to a Node writable stream and await completion.
*/
function writeToStream(stream, data) {
return new Promise((resolve, reject) => {
/* v8 ignore next 4 */
const onError = (err) => {
stream.removeListener("error", onError);
reject(err);
};
stream.once("error", onError);
stream.write(data, (err) => {
stream.removeListener("error", onError);
/* v8 ignore next 3 */
if (err) reject(err);
else resolve();
});
});
}
/**
* Determine if a string "looks like" it contains glob characters.
*/
function looksLikeGlob(source) {
return /[*?[\]{},]/.test(source);
}
//#endregion
//#region lib/tar/NativeTar.ts
/**
* Create a 512-byte TAR header block for a file or directory.
*/
function createTarHeader(name, size, mtimeSeconds, mode, isDirectory) {
const buf = Buffer.alloc(512, 0);
writeString(buf, name, 0, 100);
writeOctal(buf, mode & 4095, 100, 8);
writeOctal(buf, 0, 108, 8);
writeOctal(buf, 0, 116, 8);
writeOctal(buf, size, 124, 12);
writeOctal(buf, Math.floor(mtimeSeconds), 136, 12);
for (let i = 148; i < 156; i++) buf[i] = 32;
buf[156] = (isDirectory ? "5" : "0").charCodeAt(0);
writeString(buf, "ustar", 257, 6);
writeString(buf, "00", 263, 2);
let sum = 0;
for (let i = 0; i < 512; i++) sum += buf[i];
writeOctal(buf, sum, 148, 8);
return buf;
}
/**
* Minimal TAR writer used to build tar streams for .tar / .tgz files.
*/
var NativeTar = class {
constructor(stream) {
this.stream = stream;
}
/**
* Add a directory entry (no data, only header).
*/
async addDirectory(entry) {
let name = entry.relativePath.replace(/\\/g, "/");
if (!name.endsWith("/")) name += "/";
const header = createTarHeader(name, 0, entry.stat.mtime.getTime() / 1e3, entry.stat.mode, true);
await writeToStream(this.stream, header);
}
/**
* Add a file entry (header + file data + padding to 512 bytes).
*/
async addFile(entry) {
const name = entry.relativePath.replace(/\\/g, "/");
const data = await fs$1.promises.readFile(entry.fsPath);
const header = createTarHeader(name, data.length, entry.stat.mtime.getTime() / 1e3, entry.stat.mode, false);
await writeToStream(this.stream, header);
await writeToStream(this.stream, data);
const remainder = data.length % 512;
if (remainder !== 0) {
const padding = Buffer.alloc(512 - remainder, 0);
await writeToStream(this.stream, padding);
}
}
/**
* Finalize the TAR archive by writing two 512-byte zero blocks.
*/
async finalize() {
const block = Buffer.alloc(512, 0);
await writeToStream(this.stream, block);
await writeToStream(this.stream, block);
}
};
//#endregion
//#region lib/zip/NativeZip.ts
/**
* Low-level ZIP entry representation.
*/
var ZipEntry = class {
constructor(params) {
this.localHeaderOffset = 0;
this.name = params.name;
this.isDirectory = params.isDirectory;
this.date = params.date;
this.mode = params.mode;
this.crc32 = params.crc32;
this.compressedSize = params.compressedSize;
this.uncompressedSize = params.uncompressedSize;
this.compressionMethod = params.compressionMethod;
this.compressedData = params.compressedData;
}
};
/**
* Native ZIP writer using only Node's built-in modules.
*
* - Supports standard ZIP up to 4GB.
* - Automatically enables ZIP64 structures when needed (size/offset/entry-count overflow),
* or when forceZip64 is set.
* - Directories are stored as entries with trailing "/".
*/
var NativeZip = class {
/* istanbul ignore next */
constructor(options = {}) {
this.entries = [];
this.comment = options.comment;
this.useLocalTime = options.forceLocalTime === true;
this.forceZip64 = options.forceZip64 === true;
this.store = options.store === true;
this.zlibOptions = options.zlib;
}
/**
* Add a directory entry to the ZIP archive.
* @param archivePath Path inside the archive (with or without trailing "/").
* @param date Modification date.
* @param mode File mode (permissions).
*/
addDirectoryEntry(archivePath, date, mode) {
let name = archivePath.replace(/\\/g, "/");
if (!name.endsWith("/")) name += "/";
this.entries.push(new ZipEntry({
name,
isDirectory: true,
date,
mode,
crc32: 0,
compressedSize: 0,
uncompressedSize: 0,
compressionMethod: 0,
compressedData: Buffer.alloc(0)
}));
}
/**
* Add a file from the filesystem to the ZIP archive.
* @param filePath Physical file path on disk.
* @param archivePath Path inside the archive.
* @param date Modification date.
* @param mode File mode (permissions).
*/
async addFileFromFs(filePath, archivePath, date, mode) {
const name = archivePath.replace(/\\/g, "/");
const data = await fs$1.promises.readFile(filePath);
const sum = crc32(data);
let compressed;
let method;
if (this.store) {
compressed = data;
method = 0;
} else {
compressed = zlib$1.deflateRawSync(data, this.zlibOptions);
method = 8;
}
this.entries.push(new ZipEntry({
name,
isDirectory: false,
date,
mode,
crc32: sum,
compressedSize: compressed.length,
uncompressedSize: data.length,
compressionMethod: method,
compressedData: compressed
}));
}
/**
* Write the ZIP archive to a writable stream.
* Handles ZIP64 automatically when required.
*/
writeToStream(stream) {
return new Promise((resolve, reject) => {
stream.once("error", reject);
try {
let offset = 0;
const centralDirParts = [];
const commentBuffer = this.comment ? Buffer.from(this.comment, "utf8") : Buffer.alloc(0);
let useZip64 = this.forceZip64;
for (const entry of this.entries) {
const nameBuffer = Buffer.from(entry.name, "utf8");
const dosTime = dateToDosTime(entry.date, this.useLocalTime);
const dosDate = dateToDosDate(entry.date, this.useLocalTime);
const needsZip64Sizes = entry.compressedSize >= 4294967295 || entry.uncompressedSize >= 4294967295 || this.forceZip64;
const needsZip64Offset = offset >= 4294967295 || this.forceZip64;
if (needsZip64Sizes || needsZip64Offset) useZip64 = true;
let localExtra = Buffer.alloc(0);
if (needsZip64Sizes) {
const extra = Buffer.alloc(20);
let q = 0;
extra.writeUInt16LE(1, q);
q += 2;
extra.writeUInt16LE(16, q);
q += 2;
writeUInt64LE(extra, entry.uncompressedSize, q);
q += 8;
writeUInt64LE(extra, entry.compressedSize, q);
q += 8;
localExtra = extra;
}
const localHeader = Buffer.alloc(30);
let p = 0;
localHeader.writeUInt32LE(67324752, p);
p += 4;
localHeader.writeUInt16LE(useZip64 ? 45 : 20, p);
p += 2;
localHeader.writeUInt16LE(0, p);
p += 2;
localHeader.writeUInt16LE(entry.compressionMethod, p);
p += 2;
localHeader.writeUInt16LE(dosTime, p);
p += 2;
localHeader.writeUInt16LE(dosDate, p);
p += 2;
localHeader.writeUInt32LE(entry.crc32 >>> 0, p);
p += 4;
if (needsZip64Sizes) {
localHeader.writeUInt32LE(4294967295, p);
p += 4;
localHeader.writeUInt32LE(4294967295, p);
p += 4;
} else {
localHeader.writeUInt32LE(entry.compressedSize >>> 0, p);
p += 4;
localHeader.writeUInt32LE(entry.uncompressedSize >>> 0, p);
p += 4;
}
localHeader.writeUInt16LE(nameBuffer.length, p);
p += 2;
localHeader.writeUInt16LE(localExtra.length, p);
p += 2;
entry.localHeaderOffset = offset;
stream.write(localHeader);
stream.write(nameBuffer);
if (localExtra.length > 0) stream.write(localExtra);
offset += localHeader.length + nameBuffer.length + localExtra.length;
if (entry.compressedData.length > 0) {
stream.write(entry.compressedData);
offset += entry.compressedData.length;
}
const centralHeader = Buffer.alloc(46);
p = 0;
const centralNeedsZip64Sizes = needsZip64Sizes;
const centralNeedsZip64Offset = needsZip64Offset;
const centralNeedsZip64 = centralNeedsZip64Sizes || centralNeedsZip64Offset;
centralHeader.writeUInt32LE(33639248, p);
p += 4;
centralHeader.writeUInt16LE(768 | (useZip64 ? 45 : 20), p);
p += 2;
centralHeader.writeUInt16LE(useZip64 ? 45 : 20, p);
p += 2;
centralHeader.writeUInt16LE(0, p);
p += 2;
centralHeader.writeUInt16LE(entry.compressionMethod, p);
p += 2;
centralHeader.writeUInt16LE(dosTime, p);
p += 2;
centralHeader.writeUInt16LE(dosDate, p);
p += 2;
centralHeader.writeUInt32LE(entry.crc32 >>> 0, p);
p += 4;
if (centralNeedsZip64Sizes) {
centralHeader.writeUInt32LE(4294967295, p);
p += 4;
centralHeader.writeUInt32LE(4294967295, p);
p += 4;
} else {
centralHeader.writeUInt32LE(entry.compressedSize >>> 0, p);
p += 4;
centralHeader.writeUInt32LE(entry.uncompressedSize >>> 0, p);
p += 4;
}
centralHeader.writeUInt16LE(nameBuffer.length, p);
p += 2;
let centralExtra = Buffer.alloc(0);
if (centralNeedsZip64) {
const extraSize = 24;
const extra = Buffer.alloc(28);
let q = 0;
extra.writeUInt16LE(1, q);
q += 2;
extra.writeUInt16LE(extraSize, q);
q += 2;
writeUInt64LE(extra, entry.uncompressedSize, q);
q += 8;
writeUInt64LE(extra, entry.compressedSize, q);
q += 8;
writeUInt64LE(extra, entry.localHeaderOffset, q);
q += 8;
centralExtra = extra;
}
centralHeader.writeUInt16LE(centralExtra.length, p);
p += 2;
centralHeader.writeUInt16LE(0, p);
p += 2;
centralHeader.writeUInt16LE(0, p);
p += 2;
centralHeader.writeUInt16LE(0, p);
p += 2;
const extAttr = entry.mode << 16 | (entry.isDirectory ? 16 : 0);
centralHeader.writeUInt32LE(extAttr >>> 0, p);
p += 4;
if (centralNeedsZip64Offset) {
centralHeader.writeUInt32LE(4294967295, p);
p += 4;
} else {
centralHeader.writeUInt32LE(entry.localHeaderOffset >>> 0, p);
p += 4;
}
centralDirParts.push(centralHeader, nameBuffer);
if (centralExtra.length > 0) centralDirParts.push(centralExtra);
}
const startOfCentralDir = offset;
const centralDirectoryBuffer = Buffer.concat(centralDirParts);
const sizeOfCentralDir = centralDirectoryBuffer.length;
const totalEntries = this.entries.length;
/* v8 ignore next 3 */
if (startOfCentralDir >= 4294967295 || sizeOfCentralDir >= 4294967295 || totalEntries >= 65535) useZip64 = true;
stream.write(centralDirectoryBuffer);
const offsetAfterCentralDir = startOfCentralDir + sizeOfCentralDir;
if (useZip64) {
const zip64Eocd = Buffer.alloc(56);
let pz = 0;
zip64Eocd.writeUInt32LE(101075792, pz);
pz += 4;
writeUInt64LE(zip64Eocd, 44, pz);
pz += 8;
zip64Eocd.writeUInt16LE(45, pz);
pz += 2;
zip64Eocd.writeUInt16LE(45, pz);
pz += 2;
zip64Eocd.writeUInt32LE(0, pz);
pz += 4;
zip64Eocd.writeUInt32LE(0, pz);
pz += 4;
writeUInt64LE(zip64Eocd, totalEntries, pz);
pz += 8;
writeUInt64LE(zip64Eocd, totalEntries, pz);
pz += 8;
writeUInt64LE(zip64Eocd, sizeOfCentralDir, pz);
pz += 8;
writeUInt64LE(zip64Eocd, startOfCentralDir, pz);
pz += 8;
stream.write(zip64Eocd);
const zip64Locator = Buffer.alloc(20);
let pl = 0;
zip64Locator.writeUInt32LE(117853008, pl);
pl += 4;
zip64Locator.writeUInt32LE(0, pl);
pl += 4;
writeUInt64LE(zip64Locator, offsetAfterCentralDir, pl);
pl += 8;
zip64Locator.writeUInt32LE(1, pl);
pl += 4;
stream.write(zip64Locator);
}
const eocd = Buffer.alloc(22);
let p3 = 0;
eocd.writeUInt32LE(101010256, p3);
p3 += 4;
eocd.writeUInt16LE(0, p3);
p3 += 2;
eocd.writeUInt16LE(0, p3);
p3 += 2;
eocd.writeUInt16LE(Math.min(totalEntries, 65535), p3);
p3 += 2;
eocd.writeUInt16LE(Math.min(totalEntries, 65535), p3);
p3 += 2;
eocd.writeUInt32LE(Math.min(sizeOfCentralDir, 4294967295), p3);
p3 += 4;
eocd.writeUInt32LE(Math.min(startOfCentralDir, 4294967295), p3);
p3 += 4;
eocd.writeUInt16LE(commentBuffer.length, p3);
p3 += 2;
stream.write(eocd);
if (commentBuffer.length > 0) stream.write(commentBuffer);
stream.end?.(() => resolve());
} catch (err) {
reject(err);
}
});
}
};
//#endregion
//#region lib/ZipAFolder.ts
/**
* High-level facade class that provides ZIP/TAR creation helpers.
*
* Public API expected by tests:
* - ZipAFolder.zip(...)
* - ZipAFolder.tar(...)
* - zip(...)
* - tar(...)
*/
var ZipAFolder = class {
/**
* Create a ZIP archive from a directory or glob.
*
* @param source Directory path OR glob pattern.
* @param targetFilePath Path to target zip file. May be empty/undefined if customWriteStream is provided.
* @param options ZIP options.
*/
static async zip(source, targetFilePath, options = {}) {
const customWS = options.customWriteStream;
const hasTargetPath = !!(targetFilePath && targetFilePath.length > 0);
if (!hasTargetPath && !customWS) throw new Error("You must either provide a target file path or a custom write stream to write to.");
const statConcurrency = options.statConcurrency ?? 4;
const zipStore = options.store === true || options.compression === "uncompressed";
const zlibOptions = { ...options.zlib || {} };
if (!zipStore && options.compression !== void 0) switch (options.compression) {
case "medium":
if (zlibOptions.level === void 0) zlibOptions.level = zlib$1.constants.Z_DEFAULT_COMPRESSION;
break;
case "high":
if (zlibOptions.level === void 0) zlibOptions.level = zlib$1.constants.Z_BEST_COMPRESSION;
break;
/* v8 ignore next 3 */
default: break;
}
const zipper = new NativeZip({
comment: options.comment,
forceLocalTime: options.forceLocalTime,
forceZip64: options.forceZip64,
namePrependSlash: options.namePrependSlash,
store: zipStore,
zlib: zlibOptions
});
const cwd = process.cwd();
const isGlob = looksLikeGlob(source);
let entries = [];
if (isGlob) {
entries = await collectGlobEntries(source, cwd, statConcurrency, options.exclude);
if (entries.length === 0) throw new Error("No glob match found");
} else {
const sourceDir = path$1.resolve(source);
if (!(await fs$1.promises.stat(sourceDir)).isDirectory()) throw new Error("Source must be a directory when no glob pattern is used.");
if (hasTargetPath) {
const targetAbs = path$1.resolve(targetFilePath);
const targetDir = path$1.dirname(targetAbs);
const normalizedSourceDir = path$1.resolve(sourceDir);
if (targetDir === normalizedSourceDir || targetDir.startsWith(normalizedSourceDir + path$1.sep)) throw new Error("Source and target folder must be different.");
}
entries = await collectEntriesFromDirectory(sourceDir, statConcurrency, options.exclude);
if (options.destPath) {
const prefix = options.destPath.replace(/\\/g, "/").replace(/\/+$/, "");
if (prefix.length > 0) entries = entries.map((e) => ({
...e,
relativePath: prefix + "/" + e.relativePath.replace(/\\/g, "/").replace(/^\/+/, "")
}));
}
}
if (options.namePrependSlash) entries = entries.map((e) => {
/* v8 ignore next */
const rp = e.relativePath.startsWith("/") ? e.relativePath : "/" + e.relativePath;
return {
...e,
relativePath: rp
};
});
for (const e of entries.filter((x) => x.isDirectory)) zipper.addDirectoryEntry(e.relativePath, e.stat.mtime, e.stat.mode);
for (const e of entries.filter((x) => !x.isDirectory)) await zipper.addFileFromFs(e.fsPath, e.relativePath, e.stat.mtime, e.stat.mode);
if (!customWS && hasTargetPath) {
const parentDir = path$1.dirname(path$1.resolve(targetFilePath));
await fs$1.promises.stat(parentDir);
}
const outStream = customWS ?? fs$1.createWriteStream(targetFilePath);
await zipper.writeToStream(outStream);
}
/**
* Create a TAR (optionally gzipped) archive from a directory or glob.
*
* @param source Directory path OR glob pattern.
* @param targetFilePath Path to target tar/tgz file. May be empty/undefined if customWriteStream is provided.
* @param options TAR/gzip options.
*/
static async tar(source, targetFilePath, options = {}) {
const customWS = options.customWriteStream;
const hasTargetPath = !!(targetFilePath && targetFilePath.length > 0);
if (!hasTargetPath && !customWS) throw new Error("You must either provide a target file path or a custom write stream to write to.");
const statConcurrency = options.statConcurrency ?? 4;
const cwd = process.cwd();
const isGlob = looksLikeGlob(source);
let entries = [];
if (isGlob) {
entries = await collectGlobEntries(source, cwd, statConcurrency, options.exclude);
if (entries.length === 0) throw new Error("No glob match found");
} else {
const sourceDir = path$1.resolve(source);
if (!(await fs$1.promises.stat(sourceDir)).isDirectory()) throw new Error("Source must be a directory when no glob pattern is used.");
if (hasTargetPath) {
const targetAbs = path$1.resolve(targetFilePath);
const targetDir = path$1.dirname(targetAbs);
const normalizedSourceDir = path$1.resolve(sourceDir);
if (targetDir === normalizedSourceDir || targetDir.startsWith(normalizedSourceDir + path$1.sep)) throw new Error("Source and target folder must be different.");
}
entries = await collectEntriesFromDirectory(sourceDir, statConcurrency, options.exclude);
}
let compressionType = "gzip";
if (options.compression === "uncompressed") compressionType = "none";
else if (options.compressionType !== void 0) compressionType = options.compressionType;
else if (options.gzip !== void 0) compressionType = options.gzip ? "gzip" : "none";
const gzipOptions = { ...options.gzipOptions || {} };
if (compressionType === "gzip" && options.compression !== void 0) switch (options.compression) {
case "medium":
if (gzipOptions.level === void 0) gzipOptions.level = zlib$1.constants.Z_DEFAULT_COMPRESSION;
break;
case "high":
if (gzipOptions.level === void 0) gzipOptions.level = zlib$1.constants.Z_BEST_COMPRESSION;
break;
/* v8 ignore next 2 */
default: break;
}
const brotliOptions = { ...options.brotliOptions || {} };
if (compressionType === "brotli" && options.compression !== void 0) {
if (!brotliOptions.params) brotliOptions.params = {};
if (brotliOptions.params[zlib$1.constants.BROTLI_PARAM_QUALITY] === void 0) switch (options.compression) {
case "medium":
brotliOptions.params[zlib$1.constants.BROTLI_PARAM_QUALITY] = 6;
break;
case "high":
brotliOptions.params[zlib$1.constants.BROTLI_PARAM_QUALITY] = zlib$1.constants.BROTLI_MAX_QUALITY;
break;
/* v8 ignore next 2 */
default: break;
}
}
if (!customWS && hasTargetPath) {
const parentDir = path$1.dirname(path$1.resolve(targetFilePath));
await fs$1.promises.stat(parentDir);
}
const finalOut = customWS ?? fs$1.createWriteStream(targetFilePath);
let tarDestination;
if (compressionType === "gzip") {
const gzipStream = zlib$1.createGzip(gzipOptions);
gzipStream.pipe(finalOut);
tarDestination = gzipStream;
} else if (compressionType === "brotli") {
const brotliStream = zlib$1.createBrotliCompress(brotliOptions);
brotliStream.pipe(finalOut);
tarDestination = brotliStream;
} else tarDestination = finalOut;
const tarWriter = new NativeTar(tarDestination);
for (const e of entries.filter((x) => x.isDirectory)) await tarWriter.addDirectory(e);
for (const e of entries.filter((x) => !x.isDirectory)) await tarWriter.addFile(e);
await tarWriter.finalize();
await new Promise((resolve, reject) => {
const finishTarget = compressionType !== "none" ? finalOut : tarDestination;
/* v8 ignore next 4 */
const onError = (err) => {
cleanup();
reject(err);
};
const onFinish = () => {
cleanup();
resolve();
};
const cleanup = () => {
finishTarget.removeListener("error", onError);
finishTarget.removeListener("finish", onFinish);
};
finishTarget.once("error", onError);
finishTarget.once("finish", onFinish);
tarDestination.end?.();
});
}
/**
* Create a 7z archive from a directory or glob using LZMA compression.
*
* @param source Directory path OR glob pattern.
* @param targetFilePath Path to target .7z file. May be empty/undefined if customWriteStream is provided.
* @param options 7z/LZMA options.
*/
static async sevenZip(source, targetFilePath, options = {}) {
const customWS = options.customWriteStream;
const hasTargetPath = !!(targetFilePath && targetFilePath.length > 0);
if (!hasTargetPath && !customWS) throw new Error("You must either provide a target file path or a custom write stream to write to.");
const statConcurrency = options.statConcurrency ?? 4;
const cwd = process.cwd();
const isGlob = looksLikeGlob(source);
let entries = [];
if (isGlob) {
entries = await collectGlobEntries(source, cwd, statConcurrency, options.exclude);
if (entries.length === 0) throw new Error("No glob match found");
} else {
const sourceDir = path$1.resolve(source);
if (!(await fs$1.promises.stat(sourceDir)).isDirectory()) throw new Error("Source must be a directory when no glob pattern is used.");
if (hasTargetPath) {
const targetAbs = path$1.resolve(targetFilePath);
const targetDir = path$1.dirname(targetAbs);
const normalizedSourceDir = path$1.resolve(sourceDir);
if (targetDir === normalizedSourceDir || targetDir.startsWith(normalizedSourceDir + path$1.sep)) throw new Error("Source and target folder must be different.");
}
entries = await collectEntriesFromDirectory(sourceDir, statConcurrency, options.exclude);
}
let compressionLevel = options.compressionLevel ?? 5;
if (options.compression !== void 0) switch (options.compression) {
case "uncompressed":
compressionLevel = 1;
break;
case "medium":
compressionLevel = 5;
break;
case "high":
compressionLevel = 9;
break;
}
if (!customWS && hasTargetPath) {
const parentDir = path$1.dirname(path$1.resolve(targetFilePath));
await fs$1.promises.stat(parentDir);
}
const outStream = customWS ?? fs$1.createWriteStream(targetFilePath);
const sevenZWriter = new Native7z(compressionLevel);
for (const e of entries) await sevenZWriter.addFile(e);
await sevenZWriter.writeToStream(outStream);
}
};
/**
* Convenience function: zip(...) directly.
*/
function zip(source, targetFilePath, options) {
return ZipAFolder.zip(source, targetFilePath, options ?? {});
}
/**
* Convenience function: tar(...) directly.
*/
function tar(source, targetFilePath, options) {
return ZipAFolder.tar(source, targetFilePath, options ?? {});
}
/**
* Convenience function: sevenZip(...) directly.
* Creates a 7z archive using LZMA compression.
*/
function sevenZip(source, targetFilePath, options) {
return ZipAFolder.sevenZip(source, targetFilePath, options ?? {});
}
//#endregion
//#region bin/zip-a-folder.ts
const VERSION = "6.0.1";
const colors = {
reset: "\x1B[0m",
bold: "\x1B[1m",
dim: "\x1B[2m",
green: "\x1B[32m",
yellow: "\x1B[33m",
blue: "\x1B[34m",
cyan: "\x1B[36m",
red: "\x1B[31m",
magenta: "\x1B[35m"
};
const spinnerFrames = [
"⠋",
"⠙",
"⠹",
"⠸",
"⠼",
"⠴",
"⠦",
"⠧",
"⠇",
"⠏"
];
function printHelp() {
console.log(`
${colors.bold}${colors.cyan}zip-a-folder${colors.reset} - Compress folders into ZIP/TAR/TGZ/BR/7z archives
${colors.bold}USAGE:${colors.reset}
zip-a-folder <source> <target> [options]
${colors.bold}ARGUMENTS:${colors.reset}
${colors.green}<source>${colors.reset} Source folder path or glob pattern
${colors.green}<target>${colors.reset} Target archive path (extension determines format)
${colors.bold}SUPPORTED FORMATS:${colors.reset}
${colors.yellow}.zip${colors.reset} ZIP archive (deflate compression)
${colors.yellow}.tar${colors.reset} TAR archive (uncompressed)
${colors.yellow}.tgz${colors.reset} TAR archive with gzip compression
${colors.yellow}.tar.gz${colors.reset} TAR archive with gzip compression
${colors.yellow}.tar.br${colors.reset} TAR archive with brotli compression
${colors.yellow}.7z${colors.reset} 7z archive with LZMA compression
${colors.bold}OPTIONS:${colors.reset}
${colors.cyan}-h, --help${colors.reset} Show this help message
${colors.cyan}-V, --version${colors.reset} Show version number
${colors.cyan}-v, --verbose${colors.reset} Show detailed progress (files being processed)
${colors.cyan}-q, --quiet${colors.reset} Suppress all output except errors
${colors.cyan}-c, --compression${colors.reset} <level>
Compression preset: high, medium, uncompressed
(default: high)
${colors.cyan}-l, --level${colors.reset} <number> Compression level (1-9, format-specific)
${colors.cyan}-e, --exclude${colors.reset} <pattern> Glob pattern to exclude (can be used multiple times)
${colors.cyan}-d, --dest-path${colors.reset} <path> Destination path prefix inside archive
${colors.bold}EXAMPLES:${colors.reset}
${colors.dim}# Create a ZIP archive${colors.reset}
zip-a-folder ./my-folder ./archive.zip
${colors.dim}# Create a gzipped TAR with verbose output${colors.reset}
zip-a-folder ./src ./backup.tgz -v
${colors.dim}# Create a 7z archive with maximum compression${colors.reset}
zip-a-folder ./project ./project.7z -c high
${colors.dim}# Create archive excluding node_modules${colors.reset}
zip-a-folder ./app ./app.zip -e "node_modules/**" -e "**/*.log"
${colors.dim}# Create brotli-compressed TAR${colors.reset}
zip-a-folder ./data ./data.tar.br -v
${colors.dim}# Use glob patterns${colors.reset}
zip-a-folder "src/**/*.ts" ./source.zip
${colors.bold}OUTPUT:${colors.reset}
By default, shows a summary with compression ratio.
Use ${colors.cyan}-v${colors.reset} for detailed file-by-file progress.
Use ${colors.cyan}-q${colors.reset} to suppress all output.
`);
}
function printVersion() {
console.log(`zip-a-folder v${VERSION}`);
}
function parseArgs(args) {
const options = {
verbose: false,
quiet: false,
compression: "high",
exclude: [],
help: false,
version: false
};
let source;
let target;
const positional = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") options.help = true;
else if (arg === "-V" || arg === "--version") options.version = true;
else if (arg === "-v" || arg === "--verbose") options.verbose = true;
else if (arg === "-q" || arg === "--quiet") options.quiet = true;
else if (arg === "-c" || arg === "--compression") {
const level = args[++i];
if (level === "high" || level === "medium" || level === "uncompressed") options.compression = level;
else {
console.error(`${colors.red}Error:${colors.reset} Invalid compression level: ${level}`);
console.error("Valid options: high, medium, uncompressed");
process.exit(1);
}
} else if (arg === "-l" || arg === "--level") {
const level = Number.parseInt(args[++i], 10);
if (Number.isNaN(level) || level < 1 || level > 9) {
console.error(`${colors.red}Error:${colors.reset} Invalid compression level: must be 1-9`);
process.exit(1);
}
options.compressionLevel = level;
} else if (arg === "-e" || arg === "--exclude") options.exclude.push(args[++i]);
else if (arg === "-d" || arg === "--dest-path") options.destPath = args[++i];
else if (!arg.startsWith("-")) positional.push(arg);
else {
console.error(`${colors.red}Error:${colors.reset} Unknown option: ${arg}`);
console.error("Use --help to see available options");
process.exit(1);
}
}
if (positional.length >= 1) source = positional[0];
if (positional.length >= 2) target = positional[1];
return {
source,
target,
options
};
}
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = [
"B",
"KB",
"MB",
"GB"
];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
}
function getArchiveType(targetPath) {
const lowerPath = targetPath.toLowerCase();
if (lowerPath.endsWith(".zip")) return "zip";
if (lowerPath.endsWith(".tar.br")) return "tar.br";
if (lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz")) return "tgz";
if (lowerPath.endsWith(".tar")) return "tar";
if (lowerPath.endsWith(".7z")) return "7z";
return null;
}
async function getSourceSize(sourcePath) {
let totalSize = 0;
async function walkDir(dir) {
try {
const stat = await fs.promises.stat(dir);
if (stat.isFile()) {
totalSize += stat.size;
return;
}
const entries = await fs.promises.readdir(dir);
for (const entry of entries) await walkDir(path.join(dir, entry));
} catch {}
}
if (sourcePath.includes("*") || sourcePath.includes("?")) return 0;
await walkDir(sourcePath);
return totalSize;
}
async function countFiles(sourcePath) {
let count = 0;
async function walkDir(dir) {
try {
if ((await fs.promises.stat(dir)).isFile()) {
count++;
return;
}
const entries = await fs.promises.readdir(dir);
for (const entry of entries) await walkDir(path.join(dir, entry));
} catch {}
}
if (!sourcePath.includes("*") && !sourcePath.includes("?")) await walkDir(sourcePath);
return count;
}
var ProgressReporter = class {
constructor(verbose, quiet, totalFiles) {
this.spinnerIndex = 0;
this.filesProcessed = 0;
this.totalFiles = 0;
this.verbose = verbose;
this.quiet = quiet;
this.totalFiles = totalFiles;
this.startTime = Date.now();
}
start(archiveType, source, target) {
if (this.quiet) return;
console.log(`\n${colors.bold}${colors.cyan}📦 zip-a-folder${colors.reset}`);
console.log(`${colors.dim}───────────────────────────────────────${colors.reset}`);
console.log(`${colors.green}Source:${colors.reset} ${source}`);
console.log(`${colors.green}Target:${colors.reset} ${target}`);
console.log(`${colors.green}Format:${colors.reset} ${archiveType.toUpperCase()}`);
if (this.totalFiles > 0) console.log(`${colors.green}Files:${colors.reset} ${this.totalFiles}`);
console.log(`${colors.dim}───────────────────────────────────────${colors.reset}`);
if (!this.verbose) {
process.stdout.write(`\n${colors.cyan}${spinnerFrames[0]}${colors.reset} Compressing...`);
this.interval = setInterval(() => {
this.spinnerIndex = (this.spinnerIndex + 1) % spinnerFrames.length;
process.stdout.write(`\r${colors.cyan}${spinnerFrames[this.spinnerIndex]}${colors.reset} Compressing...`);
}, 80);
} else console.log(`\n${colors.bold}Processing files:${colors.reset}`);
}
file(relativePath, size) {
this.filesProcessed++;
if (this.verbose && !this.quiet) {
const sizeStr = formatBytes(size).padStart(10);
console.log(` ${colors.dim}${sizeStr}${colors.reset} ${relativePath}`);
}
}
stop() {
if (this.interval) {
clearInterval(this.interval);
process.stdout.write("\r" + " ".repeat(30) + "\r");
}
}
summary(sourceSize, targetSize) {
if (this.quiet) return;
const duration = ((Date.now() - this.startTime) / 1e3).toFixed(2);
const ratio = sourceSize > 0 ? ((1 - targetSize / sourceSize) * 100).toFixed(1) : "N/A";
console.log(`\n${colors.dim}───────────────────────────────────────${colors.reset}`);
console.log(`${colors.bold}${colors.green}✓ Compression complete!${colors.reset}`);
console.log(`${colors.dim}───────────────────────────────────────${colors.reset}`);
if (sourceSize > 0) {
console.log(`${colors.yellow}Original size:${colors.reset} ${formatBytes(sourceSize)}`);
console.log(`${colors.yellow}Compressed size:${colors.reset} ${formatBytes(targetSize)}`);
console.log(`${colors.yellow}Compression:${colors.reset} ${colors.green}${ratio}%${colors.reset} reduction`);
} else console.log(`${colors.yellow}Archive size:${colors.reset} ${formatBytes(targetSize)}`);
console.log(`${colors.yellow}Time:${colors.reset} ${duration}s`);
if (this.filesProcessed > 0) console.log(`${colors.yellow}Files:${colors.reset} ${this.filesProcessed}`);
console.log();
}
error(message) {
this.stop();
console.error(`\n${colors.red}✗ Error:${colors.reset} ${message}\n`);
}
};
async function main() {
const { source, target, options } = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
process.exit(0);
}
if (options.version) {
printVersion();
process.exit(0);
}
if (!source || !target) {
console.error(`${colors.red}Error:${colors.reset} Missing required arguments`);
console.error("Usage: zip-a-folder <source> <target> [options]");
console.error("Use --help for more information");
process.exit(1);
}
const archiveType = getArchiveType(target);
if (!archiveType) {
console.error(`${colors.red}Error:${colors.reset} Unsupported archive format`);
console.error("Supported formats: .zip, .tar, .tgz, .tar.gz, .tar.br, .7z");
process.exit(1);
}
const sourceSize = await getSourceSize(source);
const fileCount = await countFiles(source);
const progress = new ProgressReporter(options.verbose, options.quiet, fileCount);
try {
progress.start(archiveType, source, target);
const baseOptions = {
compression: options.compression,
exclude: options.exclude.length > 0 ? options.exclude : void 0,
destPath: options.destPath
};
switch (archiveType) {
case "zip":
await zip(source, target, {
...baseOptions,
zlib: options.compressionLevel ? { level: options.compressionLevel } : void 0
});
break;
case "tar":
await tar(source, target, {
...baseOptions,
compressionType: "none"
});
break;
case "tgz":
await tar(source, target, {
...baseOptions,
compressionType: "gzip",
gzipOptions: options.compressionLevel ? { level: options.compressionLevel } : void 0
});
break;
case "tar.br":
await tar(source, target, {
...baseOptions,
compressionType: "brotli",
brotliOptions: options.compressionLevel ? { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: options.compressionLevel } } : void 0
});
break;
case "7z":
await sevenZip(source, target, {
...baseOptions,
compressionLevel: options.compressionLevel
});
break;
}
progress.stop();
const targetStat = await fs.promises.stat(target);
progress.summary(sourceSize, targetStat.size);
} catch (error) {
progress.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
main();
//#endregion
export {};