dir-archiver
Version:
Compress a whole directory (including subdirectories) into a zip file, with options to exclude specific files, or directories.
321 lines (320 loc) • 13.2 kB
JavaScript
'use strict';
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const archiver_1 = __importDefault(require("archiver"));
class DirArchiver {
/**
* The constructor.
* @param directoryPath - the path of the folder to archive.
* @param zipPath - The path of the zip file to create.
* @param includeBaseDirectory - Includes a base directory at the root of the archive. For example, if the root folder of your project is named "your-project", setting includeBaseDirectory to true will create an archive that includes this base directory. If this option is set to false the archive created will unzip its content to the current directory.
* @param excludes - The name of the files and foldes to exclude.
*/
constructor(directoryPath, zipPath, includeBaseDirectory = false, excludes = [], followSymlinks = false) {
this.directoryPath = path.resolve(directoryPath);
this.zipPath = path.resolve(zipPath);
this.includeBaseDirectory = includeBaseDirectory;
this.followSymlinks = followSymlinks;
this.baseDirectory = path.basename(this.directoryPath);
this.visitedDirectories = new Set();
this.caseInsensitiveExcludes = process.platform === 'win32';
// Contains the excluded files and folders.
this.excludedPaths = new Set();
this.excludedNames = new Set();
for (const excludeRaw of excludes) {
if (typeof excludeRaw !== 'string') {
continue;
}
const trimmedRaw = excludeRaw.trim();
if (trimmedRaw.length === 0) {
continue;
}
let normalizedExclude = path.normalize(trimmedRaw.replace(/\\/g, path.sep));
if (normalizedExclude === '.' || normalizedExclude === path.sep) {
continue;
}
if (path.isAbsolute(normalizedExclude)) {
const relativeCandidate = path.relative(this.directoryPath, normalizedExclude);
const isInsideSource = relativeCandidate.length > 0
&& !relativeCandidate.startsWith('..')
&& !path.isAbsolute(relativeCandidate);
if (isInsideSource) {
normalizedExclude = path.normalize(relativeCandidate);
}
}
if (normalizedExclude.length === 0) {
continue;
}
const hasSeparator = normalizedExclude.includes('/')
|| normalizedExclude.includes('\\')
|| normalizedExclude.includes(path.sep);
const trimmedExclude = normalizedExclude.replace(/[\\/]+$/g, '');
if (trimmedExclude.length === 0 || trimmedExclude === '.') {
continue;
}
const normalizedValue = this.normalizeExcludeValue(trimmedExclude);
this.excludedPaths.add(normalizedValue);
if (!hasSeparator) {
this.excludedNames.add(normalizedValue);
}
}
const relativeZipPath = path.relative(this.directoryPath, this.zipPath);
const isZipInsideSource = relativeZipPath.length > 0
&& !relativeZipPath.startsWith('..')
&& !path.isAbsolute(relativeZipPath);
if (isZipInsideSource) {
const normalizedZipPath = path.normalize(relativeZipPath);
this.excludedPaths.add(this.normalizeExcludeValue(normalizedZipPath));
}
}
/**
* Recursively traverse the directory tree and append the files to the archive.
* @param directoryPath - The path of the directory being looped through.
*/
traverseDirectoryTree(directoryPath, archive) {
const directoriesToVisit = [directoryPath];
while (directoriesToVisit.length > 0) {
const nextDirectory = directoriesToVisit.pop();
if (!nextDirectory) {
continue;
}
if (this.followSymlinks) {
try {
const realPath = fs.realpathSync(nextDirectory);
if (this.visitedDirectories.has(realPath)) {
continue;
}
this.visitedDirectories.add(realPath);
}
catch {
continue;
}
}
const resolvedDirectoryPath = path.resolve(nextDirectory);
const entries = fs.readdirSync(resolvedDirectoryPath, { withFileTypes: true });
entries.sort((firstEntry, secondEntry) => {
if (firstEntry.name < secondEntry.name) {
return -1;
}
if (firstEntry.name > secondEntry.name) {
return 1;
}
return 0;
});
for (const entry of entries) {
const currentPath = path.join(resolvedDirectoryPath, entry.name);
if (currentPath === this.zipPath) {
continue;
}
const relativePath = path.relative(this.directoryPath, currentPath);
const normalizedRelativePath = path.normalize(relativePath);
const archiveRelativePath = normalizedRelativePath.replace(/\\/g, '/');
const baseName = path.basename(normalizedRelativePath);
const normalizedPathValue = this.normalizeExcludeValue(normalizedRelativePath);
const normalizedNameValue = this.normalizeExcludeValue(baseName);
if (this.excludedPaths.has(normalizedPathValue) || this.excludedNames.has(normalizedNameValue)) {
continue;
}
if (entry.isFile()) {
if (this.includeBaseDirectory) {
archive.file(currentPath, {
name: path.posix.join(this.baseDirectory, archiveRelativePath)
});
}
else {
archive.file(currentPath, {
name: archiveRelativePath
});
}
}
else if (entry.isDirectory()) {
directoriesToVisit.push(currentPath);
}
else if (entry.isSymbolicLink()) {
if (!this.followSymlinks) {
continue;
}
let stats;
try {
stats = fs.statSync(currentPath);
}
catch {
continue;
}
if (stats.isFile()) {
if (this.includeBaseDirectory) {
archive.file(currentPath, {
name: path.posix.join(this.baseDirectory, archiveRelativePath)
});
}
else {
archive.file(currentPath, {
name: archiveRelativePath
});
}
}
else if (stats.isDirectory()) {
directoriesToVisit.push(currentPath);
}
}
}
}
}
prettyBytes(bytes) {
if (bytes > 1000 && bytes < 1000000) {
const kiloBytes = Math.round(((bytes / 1000) + Number.EPSILON) * 100) / 100;
return `${kiloBytes} KB`;
}
if (bytes > 1000000 && bytes < 1000000000) {
const megaBytes = Math.round(((bytes / 1000000) + Number.EPSILON) * 100) / 100;
return `${megaBytes} MB`;
}
if (bytes > 1000000000) {
const gigaBytes = Math.round(((bytes / 1000000000) + Number.EPSILON) * 100) / 100;
return `${gigaBytes} GB`;
}
return `${bytes} bytes`;
}
normalizeExcludeValue(value) {
return this.caseInsensitiveExcludes ? value.toLowerCase() : value;
}
createZip() {
return new Promise((resolve, reject) => {
let output = null;
let archive = null;
let settled = false;
const safeResolve = (value) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
const safeReject = (err) => {
if (settled) {
return;
}
settled = true;
try {
if (archive) {
archive.abort();
}
}
catch {
// Ignore abort errors.
}
try {
if (output) {
output.destroy();
}
}
catch {
// Ignore destroy errors.
}
try {
if (fs.existsSync(this.zipPath)) {
fs.unlinkSync(this.zipPath);
}
}
catch {
// Ignore cleanup errors.
}
const normalizedError = err instanceof Error ? err : new Error(String(err));
reject(normalizedError);
};
// Remove the destination zip if it exists.
// see : https://github.com/Ismail-elkorchi/dir-archiver/issues/5
try {
if (fs.existsSync(this.zipPath)) {
fs.unlinkSync(this.zipPath);
}
}
catch (err) {
safeReject(err);
return;
}
// Create a file to stream archive data to.
output = fs.createWriteStream(this.zipPath);
archive = (0, archiver_1.default)('zip', {
zlib: { level: 9 }
});
// Catch warnings during archiving.
archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
// log warning
console.log(err);
}
else {
safeReject(err);
}
});
// Catch errors during archiving.
archive.on('error', (err) => {
safeReject(err);
});
output.on('error', (err) => {
safeReject(err);
});
// Listen for all archive data to be written.
output.on('close', () => {
if (settled) {
return;
}
console.log(`Created ${this.zipPath} of ${this.prettyBytes(archive.pointer())}`);
safeResolve(this.zipPath);
});
// Pipe archive data to the file.
archive.pipe(output);
// Recursively traverse the directory tree and append the files to the archive.
this.visitedDirectories.clear();
try {
this.traverseDirectoryTree(this.directoryPath, archive);
}
catch (err) {
safeReject(err);
return;
}
// Finalize the archive.
void Promise.resolve(archive.finalize()).catch((err) => {
safeReject(err);
});
});
}
}
module.exports = DirArchiver;