Consistent interface and response

- Creating a better, more long-term API here
- compress and extract are opposite, neat terms
- Use new Promise to get rid of callback argument when using archiver to compress a folder
- Add options argument, and make a couple of key details configurable
- Make the response intelligable
- Ensure both functions are consistent
- Updated tests to match
This commit is contained in:
Hannah Wolfe 2020-04-07 09:53:28 +01:00
parent 62c2fce6cc
commit adfb3fe499
6 changed files with 102 additions and 42 deletions

View File

@ -11,6 +11,17 @@ or
## Usage ## Usage
```
const zip = require('@tryghost/zip');
// Create a zip from a folder
let res = await zip.compress('path/to/a/folder', 'path/to/archive.zip', [options])
// Extract a zip to a folder
let res = await zip.extract('path/to/archive.zip', 'path/to/files', [options])
```
## Develop ## Develop
@ -34,6 +45,6 @@ Follow the instructions for the top-level repo.
# Copyright & License # Copyright & License
Copyright (c) 2020 Ghost Foundation - Released under the [MIT license](LICENSE). Copyright (c) 2020 Ghost Foundation - Released under the [MIT license](LICENSE).

View File

@ -1,7 +1,4 @@
const extract = require('extract-zip');
const zipFolder = require('./lib/zip-folder');
module.exports = { module.exports = {
extract, extract: require('./lib/extract'),
zipFolder compress: require('./lib/compress')
}; };

50
ghost/zip/lib/compress.js Normal file
View File

@ -0,0 +1,50 @@
const fs = require('fs-extra');
const Promise = require('bluebird');
const defaultOptions = {
type: 'zip',
glob: '**/*',
ignore: ['node_modules/**']
};
/**
* Compress
*
* - Create a zip file from a folder
*
* @param {String} folderToZip - full path to the folder to be zipped
* @param {String} destination - full path to the resulting zip file
* @param {Object} [options]
* @param {String} options.type - zip by default see archiver for other options
* @param {String} options.glob - the files to include, defaults to all files and folders
* @param {Array} options.ignore - any paths that should be ignored, sets node_modules by default
*/
module.exports = (folderToZip, destination, options = {}) => {
const opts = Object.assign({}, defaultOptions, options);
const archiver = require('archiver');
const output = fs.createWriteStream(destination);
const archive = archiver.create(opts.type, {});
return new Promise((resolve, reject) => {
// If folder to zip is a symlink, we want to get the target
// of the link and zip that instead of zipping the symlink
if (fs.lstatSync(folderToZip).isSymbolicLink()) {
folderToZip = fs.realpathSync(folderToZip);
}
output.on('close', function () {
resolve({path: destination, size: archive.pointer()});
});
archive.on('error', function (err) {
reject(err);
});
archive.glob(opts.glob, {
cwd: folderToZip,
ignore: opts.ignore
});
archive.pipe(output);
archive.finalize();
});
};

25
ghost/zip/lib/extract.js Normal file
View File

@ -0,0 +1,25 @@
const defaultOptions = {};
/**
* Extract
*
* - Unzip an archive to a folder
*
* @param {String} zipToExtract - full path to zip file that should be extracted
* @param {String} destination - full path of the extraction target
* @param {Object} [options]
* @param {Integer} options.defaultDirMode - Directory Mode (permissions), defaults to 0o755
* @param {Integer} options.defaultFileMode - File Mode (permissions), defaults to 0o644
* @param {Function} options.onEntry - if present, will be called with (entry, zipfile) for every entry in the zip
*/
module.exports = (zipToExtract, destination, options) => {
const opts = Object.assign({}, defaultOptions, options);
const extract = require('extract-zip');
opts.dir = destination;
return extract(zipToExtract, opts).then(() => {
return {path: destination};
});
};

View File

@ -1,30 +0,0 @@
const fs = require('fs-extra');
const Promise = require('bluebird');
const zipFolder = (folderToZip, destination, callback) => {
var archiver = require('archiver'),
output = fs.createWriteStream(destination),
archive = archiver.create('zip', {});
// If folder to zip is a symlink, we want to get the target
// of the link and zip that instead of zipping the symlink
if (fs.lstatSync(folderToZip).isSymbolicLink()) {
folderToZip = fs.realpathSync(folderToZip);
}
output.on('close', function () {
callback(null, archive.pointer());
});
archive.on('error', function (err) {
callback(err, null);
});
archive.glob(`**/*`, {
cwd: folderToZip,
ignore: ['node_modules/**']
});
archive.pipe(output);
archive.finalize();
};
module.exports = Promise.promisify(zipFolder);

View File

@ -6,9 +6,9 @@ const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
// Mimic how we expect this to be required // Mimic how we expect this to be required
const {zipFolder, extract} = require('../'); const {compress, extract} = require('../');
describe('lib/fs: read csv', function () { describe('Compress and Extract should be opposite functions', function () {
let symlinkPath, folderToSymlink, zipDestination, unzipDestination; let symlinkPath, folderToSymlink, zipDestination, unzipDestination;
const cleanUp = () => { const cleanUp = () => {
@ -33,10 +33,17 @@ describe('lib/fs: read csv', function () {
it('ensure symlinks work', function (done) { it('ensure symlinks work', function (done) {
fs.symlink(folderToSymlink, symlinkPath); fs.symlink(folderToSymlink, symlinkPath);
zipFolder(symlinkPath, zipDestination) compress(symlinkPath, zipDestination)
.then(() => { .then((res) => {
extract(zipDestination, {dir: unzipDestination}) res.should.be.an.Object().with.properties('path', 'size');
.then(() => { res.path.should.eql(zipDestination);
res.size.should.eql(321775);
extract(zipDestination, unzipDestination)
.then((res) => {
res.should.be.an.Object().with.properties('path');
res.path.should.eql(unzipDestination);
fs.readdir(unzipDestination, function (err, files) { fs.readdir(unzipDestination, function (err, files) {
if (err) { if (err) {
return done(err); return done(err);