Open thinkloop opened 7 years ago
The documentation confused me as well. zip.folder
creates a new folder within the archive, not the behavior I expected of adding all items from that path to the archive.
It appears zip-dir is built on top of jszip (using a much older version unfortunately) but provides the syntactic sugar I was hoping jszip provided.
I agree. If someone can solve this it would be most appreciated.
zip-dir worked well for me too, would be nice to add this 'sugar' into the main library
This option would be great if can add to the JSZip. Will this option add to JSZip?
I built something like that here: https://github.com/markandcurry/AngJSPHPZipArchive
I was looking for a way to:
I couldn't find anything that meets these criteria so I duct taped something together, perhaps it is of use to someone:
const fs = require('fs')
const path = require('path')
const JSZip = require('jszip-sync')
function getZippedFolderSync(dir) {
let allPaths = getFilePathsRecursiveSync(dir)
let zip = new JSZip()
let zipped = zip.sync(() => {
for (let filePath of allPaths) {
let addPath = path.relative(path.join(dir, ".."), filePath)
// let addPath = path.relative(dir, filePath) // use this instead if you don't want the source folder itself in the zip
let data = fs.readFileSync(filePath)
zip.file(addPath, data)
}
let data = null;
zip.generateAsync({type:"nodebuffer"}).then((content) => {
data = content;
});
return data;
})
return zipped;
}
// returns a flat array of absolute paths of all files recursively contained in the dir
function getFilePathsRecursiveSync(dir) {
var results = []
list = fs.readdirSync(dir)
var pending = list.length
if (!pending) return results
for (let file of list) {
file = path.resolve(dir, file)
let stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
res = getFilePathsRecursiveSync(file)
results = results.concat(res)
} else {
results.push(file)
}
if (!--pending) return results
}
return results
}
@MattijsKneppers this was not working for me since jszip-sync dont know the method sync, a bit strange i expected that but vscode tells me no and i dont see this into source files.
So i take your solution and change this a little bit to generate zip file from directory with jszip ( not jszip-async), yeah a bit hard coded now but for me this is enough :)
const resolve = require('path').resolve;
const fs = require('fs');
const JsZip = require('jszip');
const path = require('path');
class CreateZipService {
constructor() {
this.rootDirectory = process.cwd();
this.outDirectory = resolve(process.cwd(), 'dist');
}
/** create zip file for extension */
async createZipFile() {
// we know what directory we want
const sourceDir = resolve(this.rootDirectory, 'dist', 'serWebManagement');
let zip = new JsZip();
this.buildZipFromDirectory(sourceDir, zip, sourceDir);
/** generate zip file content */
const zipContent = await zip.generateAsync({
type: 'nodebuffer',
comment: 'ser-web-manangement',
compression: "DEFLATE",
compressionOptions: {
level: 9
}
});
/** create zip file */
fs.writeFileSync(resolve(this.outDirectory, `ser-web-management.zip`), zipContent);
}
// returns a flat array of absolute paths of all files recursively contained in the dir
buildZipFromDirectory(dir, zip, root) {
const list = fs.readdirSync(dir);
for (let file of list) {
file = path.resolve(dir, file)
let stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
this.buildZipFromDirectory(file, zip, root)
} else {
const filedata = fs.readFileSync(file);
zip.file(path.relative(root, file), filedata);
}
}
}
}
These examples don't take symlinks into account. Here is my typescript solution:
const getFilePathsRecursively = (dir: string): string[] => {
if (isBrowser()) {
throw new Error('getFilePathsRecursively is not supported in browser');
}
// returns a flat array of absolute paths of all files recursively contained in the dir
let results: string[] = [];
let list = fs.readdirSync(dir);
var pending = list.length;
if (!pending) return results;
for (let file of list) {
file = path.resolve(dir, file);
let stat = fs.lstatSync(file);
if (stat && stat.isDirectory()) {
results = results.concat(getFilePathsRecursively(file));
} else {
results.push(file);
}
if (!--pending) return results;
}
return results;
};
const getZipOfFolder = (dir: string): JSZip => {
if (isBrowser()) {
throw new Error('getZipOfFolder is not supported in browser');
}
// returns a JSZip instance filled with contents of dir.
let allPaths = getFilePathsRecursively(dir);
let zip = new JSZip();
for (let filePath of allPaths) {
// let addPath = path.relative(path.join(dir, '..'), filePath); // use this instead if you want the source folder itself in the zip
let addPath = path.relative(dir, filePath); // use this instead if you don't want the source folder itself in the zip
let data = fs.readFileSync(filePath);
let stat = fs.lstatSync(filePath);
let permissions = stat.mode;
if (stat.isSymbolicLink()) {
zip.file(addPath, fs.readlinkSync(filePath), {
unixPermissions: parseInt('120755', 8), // This permission can be more permissive than necessary for non-executables but we don't mind.
dir: stat.isDirectory()
});
} else {
zip.file(addPath, data, {
unixPermissions: permissions,
dir: stat.isDirectory()
});
}
}
return zip;
};
Make sure your platform is set to "UNIX" when final ZIP archive file is created. On Mac, this is not the default behavior and needs to be set manually!
I used code and inspiration from this thread to create a fully async and fully stream'd method for zipping a folder. Here are the main talking points:
zip.file(fileName, fs.createReadStream(...))
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const JSZip = require('jszip');
/**
* Returns a flat list of all files and subfolders for a directory (recursively).
* @param {string} dir
* @returns {Promise<string[]>}
*/
const getFilePathsRecursively = async (dir) => {
// returns a flat array of absolute paths of all files recursively contained in the dir
const list = await fsp.readdir(dir);
const statPromises = list.map(async (file) => {
const fullPath = path.resolve(dir, file);
const stat = await fsp.stat(fullPath);
if (stat && stat.isDirectory()) {
return getFilePathsRecursively(fullPath);
}
return fullPath;
});
return (await Promise.all(statPromises)).flat(Infinity);
};
/**
* Creates an in-memory zip stream from a folder in the file system
* @param {string} dir
* @returns {JSZip}
*/
const createZipFromFolder = async (dir) => {
const absRoot = path.resolve(dir);
const filePaths = await getFilePathsRecursively(dir);
return filePaths.reduce((z, filePath) => {
const relative = filePath.replace(absRoot, '');
// create folder trees manually :(
const zipFolder = path
.dirname(relative)
.split(path.sep)
.reduce((zf, dirName) => zf.folder(dirName), z);
zipFolder.file(path.basename(filePath), fs.createReadStream(filePath));
return z;
}, new JSZip());
};
/**
* Compresses a folder to the specified zip file.
* @param {string} srcDir
* @param {string} destFile
*/
const compressFolder = async (srcDir, destFile) => {
const start = Date.now();
try {
const zip = await createZipFromFolder(srcDir);
zip
.generateNodeStream({ streamFiles: true, compression: 'DEFLATE' })
.pipe(fs.createWriteStream(destFile))
.on('error', (err) => console.error('Error writing file', err.stack))
.on('finish', () => console.log('Zip written successfully:', Date.now() - start, 'ms'));
} catch (ex) {
console.error('Error creating zip', ex);
}
};
compressFolder('./docs', './docs.zip');
Nice @DesignByOnyx ! I am going to try your solution. Maybe improve it somehow to support symlinks as well? That would be pretty neat!
@diegoperini Does this support symlinks - or you mean the others don't but yours does.
Nice @DesignByOnyx ! I am going to try your solution. Maybe improve it somehow to support symlinks as well? That would be pretty neat!
@diegoperini Does this support symlinks - or you mean the others don't but yours does.
@ktivadar Main branch doesn't but my snippet does. I haven't tried other snippets in this issue.
If you are zipping from browser and want to keep the file structure:
let zippedFileName: string = ""; const zip = JSZip();
for (let file = 0; file < files.length; file++) {
zippedFileName = files[file].webkitRelativePath.split("/")[0] || files[file].name;
const fileDir = files[file].webkitRelativePath || files[file].name;
zip.file(fileDir, files[file]);
}
const zippedFile = await zip.generateAsync({ type: "blob" });
// sendToServer(zippedFile)
I shamelessly took the work done by @DesignByOnyx and added a bit of typescript, didn't need manual directory tree (maybe it was bug in jszip
or something from earlier version), and returned promise around final zip file write stream. Very very lightly tested on my ubuntu 22 machine with node v16 and jszip 3.10.1 but YMMV.
Still surprised jszip hasn't offered an easy api to do this. Great lib though. I wasn't comfortable using any of the other zip libs on npm.
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import JSZip from "jszip";
/**
* Compresses a folder to the specified zip file.
* @param {string} srcDir
* @param {string} destFile
*/
export const compressFolder = async (
srcDir: string,
destFile: string
): Promise<void> => {
//node write stream wants dest dir to already be created
await fsp.mkdir(path.dirname(destFile), { recursive: true });
const zip = await createZipFromFolder(srcDir);
return new Promise((resolve, reject) => {
zip
.generateNodeStream({ streamFiles: true, compression: "DEFLATE" })
.pipe(fs.createWriteStream(destFile))
.on("error", (err) => reject(err))
.on("finish", resolve);
});
};
/**
* Returns a flat list of all files and subfolders for a directory (recursively).
* @param {string} dir
* @returns {Promise<string[]>}
*/
const getFilePathsRecursively = async (dir: string): Promise<string[]> => {
// returns a flat array of absolute paths of all files recursively contained in the dir
const list = await fsp.readdir(dir);
const statPromises = list.map(async (file) => {
const fullPath = path.resolve(dir, file);
const stat = await fsp.stat(fullPath);
if (stat && stat.isDirectory()) {
return getFilePathsRecursively(fullPath);
}
return fullPath;
});
// cast to string[] is ts hack
// see: https://github.com/microsoft/TypeScript/issues/36554
return (await Promise.all(statPromises)).flat(
Number.POSITIVE_INFINITY
) as string[];
};
/**
* Creates an in-memory zip stream from a folder in the file system
* @param {string} dir
* @returns {Promise<JSZip>}
*/
const createZipFromFolder = async (dir: string): Promise<JSZip> => {
const absRoot = path.resolve(dir);
const filePaths = await getFilePathsRecursively(dir);
return filePaths.reduce((z, filePath) => {
const relative = filePath.replace(absRoot, "");
return z.file(relative, fs.createReadStream(filePath), {
unixPermissions: "777", //you probably want less permissive permissions
});
}, new JSZip());
};
thanks, it helps
Hello, thanks a lot for the lib.
I am trying to zip a single folder and all its subfolders/files on the server - rather than creating new files on the fly in memory in the browser. I am trying the following:
But the results are not what is expected. The output file comes out tiny, and unzipping it reveals other compressed files that recursively unzip forever.
Any idea how to:
zip('existing-folder-contents').saveToDisk('zipped.zip')
?