backup.js
Rama master · réplica de lectura del repositorio original.
// backup to zipfile
module.exports.zipDirectory = async function (dirPath, outputZip, password = '', compression = 5) {
try {
const { ZipWriterStream } = require("@zip.js/zip.js");
const { readdir, stat } = require("fs/promises");
const path = require("path");
const { createReadStream, createWriteStream } = require("fs");
const { Readable } = require("stream");
const { pipeline } = require("stream/promises");
const zipStream = new ZipWriterStream({
...(password ? { password, encryptionStrength: 3 } : {}), // 3 = AES-256
level: compression
// msDosCompatible: true // to write EntryMetaData#externalFileAttributes in MS-DOS format for folder entries. Fixes Dir entries issue
});
async function addFiles() {
const entries = await readdir(dirPath, { recursive: true, withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(entry.parentPath, entry.name);
const relPath = path.relative(dirPath, fullPath).replaceAll(path.sep, path.posix.sep);
if (entry.isDirectory()) {
await zipStream.zipWriter.add(relPath + '/', null);
} else {
await Readable.toWeb(createReadStream(fullPath)).pipeTo(zipStream.writable(relPath));
}
// console.log ("zip addfile: " + entry);
}
}
await Promise.all([
addFiles().then(() => zipStream.close()),
pipeline(Readable.fromWeb(zipStream.readable), createWriteStream(outputZip)),
]);
return { res: true, mes: 'Zip created succesfully' };
} catch (e) {
// console.error(e);
return { res: false, mes: e.message };
}
}
// Move the folders zipExtract kept at the root of destPath to where they belong
// Try rename (move) first, else copy/remove
module.exports.moveExtractedFolders = async function (destPath, destinations) {
const path = require('path');
const fsp = require('fs/promises');
const { existsSync } = require('fs');
const moved = [], failed = [];
for (const folderName in destinations) {
const srcPath = path.join(destPath, folderName);
const folderDest = destinations[folderName];
if (!existsSync(srcPath)) continue;
if (typeof folderDest != 'string') { failed.push(folderName + ': no destination, left in ' + srcPath); continue; }
try {
await fsp.mkdir(path.dirname(folderDest), { recursive: true });
try {
await fsp.rename(srcPath, folderDest);
} catch (e) {
if (['EXDEV', 'ENOTEMPTY', 'EEXIST', 'EPERM', 'EACCES'].indexOf(e.code) == -1) throw e;
await fsp.cp(srcPath, folderDest, { recursive: true, force: true });
await fsp.rm(srcPath, { recursive: true, force: true });
}
moved.push(folderName + ' -> ' + folderDest);
} catch (e) {
failed.push(folderName + ' -> ' + folderDest + ': ' + e.message);
}
}
return { res: (failed.length == 0), moved: moved, mes: failed.length ? ('Unable to move: ' + failed.join('; ')) : ('Moved ' + moved.length + ' folder(s)') };
}
// restore zipfile
// First check archive on illegal paths (updirs and absolute paths), then extract
module.exports.zipExtract = async function (zipPath, destPath, removePath = '', password = "") {
try {
const { ZipReader, Uint8ArrayReader } = require("@zip.js/zip.js");
const { createWriteStream } = require("fs");
const { readFile, mkdir } = require("fs/promises");
const path = require("path");
const { Readable } = require("stream");
const { pipeline } = require("stream/promises");
const zipReader = new ZipReader(new Uint8ArrayReader(await readFile(zipPath)));
const entries = await zipReader.getEntries();
const resolveDestPath = path.resolve(destPath);
// Archive validation: Abort the restore on any bad path before extraction
const targets = []; // { entry, fullPath } cache the paths while we're at it
for (const entry of entries) {
// skip symlinks in unix-mode zips (upper 16 bits hold st_mode); not present in dos/win zips
if (((entry.externalFileAttributes >>> 16) & 0xF000) === 0xA000) { continue; }
// anchored prefix strip — must match what the extraction pass writes
let name = entry.filename;
if (Array.isArray(removePath)) {
const parts = name.split(/[/\\]/);
if ((parts.length > 1) && (removePath.indexOf(parts[0]) == -1)) { name = parts.slice(1).join('/'); }
} else if (removePath && name.startsWith(removePath)) { name = name.slice(removePath.length); }
// reject any parent-dir token outright (split on both separators; segment match, not substring)
if (name.split(/[/\\]/).includes('..')) {
throw new Error('Aborting, nothing restored. Illegal path (parent traversal) in zip, entry: ' + entry.filename);
}
// reject absolute paths (posix root, or Windows drive / UNC)
if (path.isAbsolute(name) || /^[a-zA-Z]:/.test(name) || name.startsWith('\\')) {
throw new Error('Aborting, nothing restored. Illegal absolute path in zip, entry: ' + entry.filename);
}
// backstop: resolved target must stay inside destPath
const fullPath = path.resolve(destPath, name);
if (fullPath !== resolveDestPath && !fullPath.startsWith(resolveDestPath + path.sep)) {
throw new Error('Illegal path in zip entry: ' + entry.filename);
}
targets.push({ entry, fullPath });
}
// Validated, extract!
for (const { entry, fullPath } of targets) {
if (entry.directory) {
await mkdir(fullPath, { recursive: true });
} else {
await mkdir(path.dirname(fullPath), { recursive: true });
const { writable, readable } = new TransformStream();
await Promise.all([
entry.getData(writable, { password }),
pipeline(Readable.fromWeb(readable), createWriteStream(fullPath)),
]);
}
}
await zipReader.close();
return { res: true, mes: 'Extraction successful' };
} catch (e) {
// console.error(e.message);
return { res: false, mes: e.message };
}
}