zip-lib
Version:
A zip and unzip library for Node.js.
364 lines (363 loc) • 13.4 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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Zip = void 0;
const node_fs_1 = require("node:fs");
const fs = __importStar(require("node:fs/promises"));
const path = __importStar(require("node:path"));
const yazl = __importStar(require("yazl"));
const cancelable_1 = require("./cancelable");
const exfs = __importStar(require("./fs"));
/**
* Compress files or folders to a zip file.
*/
class Zip extends cancelable_1.Cancelable {
/**
*
*/
constructor(options) {
super();
this.options = options;
this.token = null;
this.activeArchive = null;
this.zipFiles = [];
this.zipFolders = [];
}
/**
* Adds a file from the file system at `realPath` to the zip file as `metadataPath`.
* @param file
* @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`.
* A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..".
*/
addFile(file, metadataPath) {
let mPath = metadataPath;
if (!mPath) {
mPath = path.basename(file);
}
this.zipFiles.push({
path: file,
metadataPath: mPath,
});
}
/**
* Adds a folder from the file system at `realPath` to the zip file as `metadataPath`.
* @param folder
* @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`.
* A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..".
*/
addFolder(folder, metadataPath) {
this.zipFolders.push({
path: folder,
metadataPath,
});
}
async archive(zipFile) {
const token = new cancelable_1.CancellationToken();
this.token = token;
const zip = await this.prepareArchive(zipFile);
const activeArchive = {
zip,
mode: zipFile ? "file" : "buffer",
};
this.activeArchive = activeArchive;
try {
return await new Promise((resolve, reject) => {
let disposeCancel = () => {
// noop
};
const settle = this.createPromiseSettler((value) => {
disposeCancel();
resolve(value);
}, (error) => {
disposeCancel();
reject(error);
});
disposeCancel = token.onCancelled(() => {
this.stop(this.canceledError(), activeArchive);
settle.reject(this.canceledError());
});
this.bindArchiveOutput(zip, zipFile, token, settle, activeArchive);
this.addQueuedEntries(zip, token)
.catch((error) => {
settle.reject(this.wrapError(error, token.isCancelled));
})
.finally(() => {
zip.end();
});
});
}
finally {
if (this.token === token) {
this.token = null;
}
if (this.activeArchive === activeArchive) {
this.activeArchive = null;
}
}
}
async prepareArchive(zipFile) {
if (zipFile) {
await exfs.ensureFolder(path.dirname(zipFile));
}
// Re-instantiate yazl every time the archive method is called to ensure that files are not added repeatedly.
// This will also make the Zip class reusable.
return new yazl.ZipFile();
}
bindArchiveOutput(zip, zipFile, token, settle, activeArchive) {
if (zipFile) {
void this.archiveToFile(zip, zipFile, token, settle, activeArchive);
return;
}
void this.archiveToBuffer(zip, token, settle);
}
async archiveToFile(zip, zipFile, token, settle, activeArchive) {
const zipStream = (0, node_fs_1.createWriteStream)(zipFile);
activeArchive.zipStream = zipStream;
const archivePromise = new Promise((resolve) => {
zip.once("error", (err) => {
settle.reject(this.wrapError(err, token.isCancelled));
resolve();
});
zipStream.once("error", (err) => {
settle.reject(this.wrapError(err, token.isCancelled));
resolve();
});
zipStream.once("close", () => {
if (token.isCancelled) {
settle.reject(this.canceledError());
}
else {
settle.resolve(void 0);
}
resolve();
});
});
zip.outputStream.pipe(zipStream);
await archivePromise;
}
async archiveToBuffer(zip, token, settle) {
const chunks = [];
const archivePromise = new Promise((resolve) => {
zip.once("error", (err) => {
settle.reject(this.wrapError(err, token.isCancelled));
resolve();
});
zip.outputStream.on("data", (chunk) => {
chunks.push(chunk);
});
zip.outputStream.once("end", () => {
settle.resolve(Buffer.concat(chunks));
resolve();
});
zip.outputStream.once("error", (err) => {
settle.reject(this.wrapError(err, token.isCancelled));
resolve();
});
});
await archivePromise;
}
createPromiseSettler(resolve, reject) {
let settled = false;
return {
resolve: (value) => {
if (settled) {
return;
}
settled = true;
resolve(value);
},
reject: (error) => {
if (settled) {
return;
}
settled = true;
reject(error);
},
};
}
async addQueuedEntries(zip, token) {
for (const file of this.zipFiles) {
const entry = await exfs.getFileEntry(file.path);
await this.addEntry(zip, entry, file, token);
}
if (this.zipFolders.length > 0) {
await this.walkDir(zip, this.zipFolders, token);
}
}
/**
* Cancel compression.
* If the cancel method is called after the archive is complete, nothing will happen.
*/
cancel() {
if (this.token) {
this.token.cancel();
this.token = null;
}
this.stop(this.canceledError());
}
async addEntry(zip, entry, file, token) {
if (entry.isSymbolicLink) {
if (this.followSymlink()) {
try {
const realPath = await exfs.realpath(file.path);
const actualStat = await fs.stat(realPath);
if (actualStat.isDirectory()) {
await this.walkDir(zip, [{ path: realPath, metadataPath: file.metadataPath }], token);
}
else {
const targetEntry = {
path: realPath,
isSymbolicLink: false,
type: "file",
mtime: actualStat.mtime,
mode: actualStat.mode,
};
await this.addFileStream(zip, targetEntry, file.metadataPath, token);
}
}
catch (_error) {
// Symlink is broken, ignore it
}
}
else {
await this.addSymlink(zip, entry, file.metadataPath);
}
}
else {
if (entry.type === "dir") {
zip.addEmptyDirectory(file.metadataPath, {
mtime: entry.mtime,
mode: entry.mode,
});
}
else {
await this.addFileStream(zip, entry, file.metadataPath, token);
}
}
}
addFileStream(zip, file, metadataPath, token) {
return new Promise((resolve, reject) => {
const fileStream = (0, node_fs_1.createReadStream)(file.path);
const disposeCancel = token.onCancelled(() => {
fileStream.destroy(this.canceledError());
});
fileStream.once("error", (err) => {
disposeCancel();
const wrappedError = this.wrapError(err, token.isCancelled);
this.stop(wrappedError);
reject(wrappedError);
});
fileStream.once("close", () => {
disposeCancel();
resolve();
});
// If the file attribute is known, add the entry using `addReadStream`,
// this can reduce the number of calls to the `fs.stat` method.
zip.addReadStream(fileStream, metadataPath, Object.assign(Object.assign({}, this.getYazlOption()), { mode: file.mode, mtime: file.mtime }));
});
}
async addSymlink(zip, file, metadataPath) {
const linkTarget = await fs.readlink(file.path);
zip.addBuffer(Buffer.from(linkTarget), metadataPath, Object.assign(Object.assign({}, this.getYazlOption()), { mtime: file.mtime, mode: file.mode }));
}
async walkDir(zip, folders, token) {
for (const folder of folders) {
if (token.isCancelled) {
return;
}
const entries = await exfs.readdirp(folder.path);
if (entries.length > 0) {
for (const entry of entries) {
if (token.isCancelled) {
return;
}
const relativePath = path.relative(folder.path, entry.path);
const metadataPath = folder.metadataPath
? path.join(folder.metadataPath, relativePath)
: relativePath;
await this.addEntry(zip, entry, { path: entry.path, metadataPath }, token);
}
}
else {
// If the folder is empty and the metadataPath has a value,
// an empty folder should be created based on the metadataPath
if (folder.metadataPath) {
zip.addEmptyDirectory(folder.metadataPath);
}
}
}
}
stop(err, activeArchive) {
const archive = activeArchive !== null && activeArchive !== void 0 ? activeArchive : this.activeArchive;
if (!archive) {
return;
}
if (archive.mode === "file" && archive.zipStream) {
archive.zip.outputStream.unpipe(archive.zipStream);
archive.zipStream.destroy(err);
}
if (archive.mode === "buffer") {
archive.zip.outputStream.destroy(err);
}
if (!activeArchive || this.activeArchive === activeArchive) {
this.activeArchive = null;
}
}
followSymlink() {
let followSymlink = false;
if (this.options && this.options.followSymlinks === true) {
followSymlink = true;
}
return followSymlink;
}
/**
* Retrieves the yazl options based on the current settings.
*
* @returns The yazl options with the specified compression level,
* or undefined if options or compressionLevel are not properly set.
*/
getYazlOption() {
if (!this.options) {
return undefined;
}
if (typeof this.options.compressionLevel !== "number") {
return undefined;
}
return {
compressionLevel: this.options.compressionLevel,
};
}
}
exports.Zip = Zip;