3

So far I have tried ADM-ZIP and easy-zip. Both of them create a zip that is mostly successful but has some malformed files:

enter image description here

This happens with a variety of file types (including just regular pictures and html pages). I suspect that the file type isn't even an issue. Either way, I can't use it if it doesn't work perfectly.

Some people have suggested node-archive but there are no instructions for how to zip a file, let alone recursively zip a directory while keeping its file structure.

update

as requested here is the code i am using (adm-zip)

var Zip = require("adm-zip");
var zip = new Zip();
zip.addLocalFolder("C:\\test");
zip.writeZip("C:\\test\\color.zip");
user1873073
  • 3,580
  • 5
  • 46
  • 81
  • How are you zipping them? Where is your code? It's hard to say what's wrong and asking for other libraries isn't a good fit generally on StackOverflow. – WiredPrairie Oct 07 '13 at 15:11
  • Can you post the code you used to zip with adm-zip or easy-zip. It may be a bug in your code. – user568109 Oct 07 '13 at 15:12
  • The error looks like copy error, not extraction error. Are you sure it is related to adm-zip. I tried the adm-zip and found that it fails/gives error to zip links to another file/folder, in particular the node_modules sub-directories linked. So is this the case with you? – user568109 Oct 07 '13 at 17:02
  • For me it even happens on .htm and .jpeg files that are not zipped. – user1873073 Oct 07 '13 at 17:28
  • Yeah, thought so. It is not related to adm-zip. Google the error code 80004005 and the text, you will find plenty of links to remedy that. – user568109 Oct 07 '13 at 17:42
  • @user1873073 have you tried implementing my solution yet? – Vinay Oct 16 '13 at 07:31
  • ... I'll take the silence as a "no". – Vinay Oct 17 '13 at 20:43

4 Answers4

4

As you had mentioned, I would use node-archiver. You can get nested folders from node-archiver by:

var archiver = require('archiver');

var archive = archiver('zip')

// create your file writeStream - let's call it writeStream

archive.pipe(writeStream);

// get all the subfiles/folders for the given folder - this can be done with readdir
// take a look at http://nodejs.org/api/fs.html#fs_fs_readdir_path_callback
// and https://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search
// appending to the archiver with a name that has slashes in it will treat the payload as a subfolder/file

// iterate through the files from the previous walk
ITERATE_THROUGH_RESULTS_FROM_WALK
    // look into http://nodejs.org/api/fs.html#fs_fs_readfile_filename_options_callback
    GET_READ_STREAM_ON_FILE_FROM_FS
        archive.append((new Buffer(data), 'utf-8'), {name: NAME_WITH_SLASHES_FOR_PATH});

archive.finalize(function (err) {
    // handle the error if there is one
});

Hopefully this is a pretty good nudge in the right direction (basically all of the steps are there for you). If it isn't clear yet:

  1. Create your archive with the 'zip' option.
  2. Pipe to a writestream that you created for where you want to save your zip file.
  3. Walk through the folder recursively, saving paths in a collection (array) as you go along.
  4. Iterate through this collection of paths, opening each file.
  5. When you have the data of each file, create a Buffer from it and pass that as well as the path into archive.append.
  6. Call archive.finalize.

For sake of readability, and because I believe I have given you pretty much all the steps you would need, I did not include all of the code (most notably the walk - which is laid out in the link anyhow).

Community
  • 1
  • 1
Vinay
  • 6,204
  • 6
  • 38
  • 55
3

You've asked for a zip but if you just need to archive files for transportation then I would suggest using tar.gz. Used it in production for transporting directories - works like a charm.

Here are usage examples: https://github.com/cranic/node-tar.gz#usage

var targz = require('tar.gz');
var compress = new targz().compress('/path/to/compress', '/path/to/store.tar.gz', function(err){
  if(err)
    console.log(err);
  console.log('The compression has ended!');
});

And uncompress:

var targz = require('tar.gz');
var compress = new targz().extract('/path/to/stored.tar.gz', '/path/to/extract', function(err){
  if(err)
    console.log(err);

  console.log('The extraction has ended!');
});
moka
  • 22,846
  • 4
  • 51
  • 67
  • 1
    On Windows, using tar.gz files is far less friendly than zip files. I'd go so far as not recommending them at all. – WiredPrairie Oct 07 '13 at 15:13
  • i will probably just do this if i can't find something for .zip – user1873073 Oct 07 '13 at 15:25
  • @WiredPrairie if we are talking about exchange of files, then there is nothing bad about them. As well .tar.gz is extremely popular on linux and mac systems, as well if we are talking about hosting (node.js) then it is mostly to be linux (again). I use all three systems, and do not find any problems with .tar.gz especially using node.js (in production) – moka Oct 07 '13 at 15:39
  • The question was about Windows, where tar.gz isn't common, and as to nothing bad about them, that's your opinion. It's certainly not a friendly format for consumers (vs. programmers, IT, etc.). – WiredPrairie Oct 07 '13 at 16:27
  • For Windows consumers, .tar.gz - yes, is not the friendliest format. But if you are developer, you definitely have software to work with them, as they are very common in development world. As well, I've assume that TS need solution for archive transportation, rather than exchange to end consumer/developer. – moka Oct 08 '13 at 08:51
3

archiver has a bulk method which supports Grunt's "Files Array" format. Here's an example of recursively zipping folder foo and everything in it recursively.

foo
├── bar
|   ├── a.js
|   └── b.js
└── index.html

JavaScript based on this example in archiver

var output = fs.createWriteStream('foo.zip');
var archive = archiver('zip');

output.on('close', function() { console.log('done') });
archive.on('error', function(err) { throw err });

archive.pipe(output);

archive.bulk([
  { expand: true, cwd: 'foo', src: ['**/*'] }
]).finalize();
Wei
  • 1,252
  • 13
  • 19
2

There is now an easier way to do it compared to the other answers.

archiver (page on npmjs) now has a method named 'directory' to help you do this.

Install archiver with:

npm install archiver --save

There are four things you need to do to recursively zip a directory and write it to a file.

A. Create an instance of archiver by using

    var ar = archive.create('zip',{});

B. Create a fs.writeStream and set up its event handlers...

var outputStream = fs.createWriteStream(odsFilepath, {flags: 'w'});
    outputStream.on('close', function () {
       console.log(ar.pointer() + ' total bytes written');
       console.log('ODS file written to:', odsFilepath);
    });
...

C. Wire up the output of our archiver to be piped to the writeStream we created:

ar.pipe(outputStream);

D. Ask our archiver to zip up the contents of the directory and place it in the 'root' or '/' of our zip file.

ar.directory(directoryPathToAddToZip, '/')
    .finalize();

Here is the code snippet of the function where I am using this. Note: put the following code snippet into a file, say index.js

var archiver = require('archiver');
var utility = require('utility');
var path = require('path');
var fs = require('fs');

//This is the directory where the zip file will be written into.
var outputDirname = ".";

//This directory should contain the stuff that we want to
//  put inside the zip file
var pathOfContentDirToInsertIntoArchive = "./tozip";

saveToZip(function () {
        console.log('We are now done');
});

function saveToZip(done) {
    //we use the utility package just to get a timestamp string that we put
    //into the output zip filename
    var ts = utility.logDate();
    var timestamp = ts.replace(/[ :]/g, '-');
    var zipFilepath = path.normalize(outputDirname + '/' + timestamp + '.zip')

    var ar = archiver.create('zip', {});

    var output = fs.createWriteStream(zipFilepath, {flags: 'w'});
    output.on('close', function () {
        //console.log(ar.pointer() + ' total bytes');
       console.log('ZIP file written to:', zipFilepath);
        return done(null, 'Finished writing')
    });

    ar.on('error', function (err) {
        console.error('error compressing: ', err);
        return done(err, 'Could not compress');
    });

    ar.pipe(output);
    ar.directory(path.normalize(pathOfContentDirToInsertIntoArchive + '/'), '/')
    .finalize();
}

Then do a npm install archiver --save and npm install utility --save

Then create a directory named "tozip" in the current directory and put in there some files that you want to compress into the output zip file.

Then, run node index.js and you will see output similar to:

$ node index.js
ZIP file written to: 2016-12-19-17-32-14.817.zip
We are now done

The zip file created will contain the contents of the tozip directory, compressed.

saraf
  • 530
  • 7
  • 22
  • This seems like the right answer, none of the others work anymore, but there are missing deps and vars in the final solution making it difficult to test, is it possible to fill it in so it is self-contained? – Baz Dec 17 '16 at 17:16
  • 1
    Ok, thanks, done ... edited answer to be self-contained. – saraf Dec 19 '16 at 12:05