Stuk / jszip

Create, read and edit .zip files with Javascript
https://stuk.github.io/jszip/
Other
9.8k stars 1.3k forks source link

Is it possible to create a zip from an existing file/folder structure on the file system? #386

Open thinkloop opened 7 years ago

thinkloop commented 7 years ago

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:

const JSZip = require('jszip');
const zip = new JSZip();
const fs = require("fs");

var data = zip.generateAsync({base64:false,compression:'DEFLATE'});

zip.folder('../lambdas/').generateAsync({type:'base64', compression:'DEFLATE'}).then(function (base64) {
    fs.writeFileSync('../../build/lambdas.zip', data, 'binary');
});

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')?

kwv commented 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.

markandcurry commented 6 years ago

I agree. If someone can solve this it would be most appreciated.

greg-benner-klick-sensei commented 6 years ago

zip-dir worked well for me too, would be nice to add this 'sugar' into the main library

thameerakn commented 5 years ago

This option would be great if can add to the JSZip. Will this option add to JSZip?

markandcurry commented 5 years ago

I built something like that here: https://github.com/markandcurry/AngJSPHPZipArchive

MattijsKneppers commented 5 years ago

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
}
r-hannuschka commented 5 years ago

@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);
            }
        }
    }
}
diegoperini commented 4 years ago

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!

DesignByOnyx commented 2 years ago

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:

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');
ktivadar commented 2 years ago

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.

diegoperini commented 2 years ago

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.

itka0526 commented 1 year ago

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)
rt-joe commented 1 year ago

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());
};
whoafridi commented 9 months ago

thanks, it helps