Skip to content

Commit

Permalink
feat: (strf-8684) update 'tmp' package
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxGenash committed Sep 22, 2020
1 parent e0b6387 commit b1e932c
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 337 deletions.
95 changes: 95 additions & 0 deletions lib/archiveManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @module Contains functions for working with archives
*/

const yauzl = require('yauzl');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

const { readFromStream } = require('./utils/asyncUtils');

/**
* @param {object} options
* @param {string} options.zipPath
* @param {string} [options.fileToExtract] - filename to download only
* @param {string[]} [options.exclude] - paths of files and directories to exclude
* @param {object} [options.outputNames] - new names for some files. Format: { 'oldName1': 'newName1', ...}
* @returns {Promise<void>}
*/
async function extractZipFiles({ zipPath, fileToExtract, exclude, outputNames = {}}) {
let foundMatch = false;

const zipFile = await promisify(yauzl.open)(zipPath, { lazyEntries: true });

await new Promise((resolve, reject) => {
zipFile.on('entry', async entry => {
try {
const readableStream = await promisify(zipFile.openReadStream.bind(zipFile))(entry);

if (fileToExtract) {
if (fileToExtract !== entry.fileName) {
return zipFile.readEntry();
}
foundMatch = true;
} else if (exclude && exclude.length) {
// Do not process any file or directory within the exclude option
for (const excludeItem of exclude) {
if (entry.fileName.startsWith(excludeItem)) {
return zipFile.readEntry();
}
}
}

// If file is a directory, then move to next
if (/\/$/.test(entry.fileName)) {
return zipFile.readEntry();
}

// Create a directory if the parent directory does not exists
const parsedPath = path.parse(entry.fileName);

if (parsedPath.dir && !fs.existsSync(parsedPath.dir)) {
fs.mkdirSync(parsedPath.dir, { recursive: true });
}

let fileData = await readFromStream(readableStream);
if (entry.fileName.endsWith('.json')) {
// Make sure that the JSON file if valid
fileData = JSON.stringify(JSON.parse(fileData), null, 2);
}

const outputFileName = outputNames[entry.fileName] || entry.fileName;
await promisify(fs.writeFile)(outputFileName, fileData, { flag: 'w+' });

// Close read if the requested file is found
if (fileToExtract) {
zipFile.close();
return resolve();
}

zipFile.readEntry();
} catch (err) {
return reject(err);
}
});

zipFile.readEntry();

zipFile.once('end', function () {
zipFile.close();

if (!foundMatch && fileToExtract) {
return reject(new Error(`${fileToExtract} not found`));
}

resolve();
});
});
}


module.exports = {
extractZipFiles,
};

83 changes: 83 additions & 0 deletions lib/archiveManager.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const fs = require('fs');
const Path = require('path');
const yauzl = require('yauzl');

const { extractZipFiles } = require('./archiveManager');

describe('archiveManager', () => {
describe('extractZipFiles', () => {
// We run tests for a real archive containing 2 files:
// - config.json
// - schema.json
let zipPath = Path.join(process.cwd(), 'test', '_mocks', 'themes', 'valid', 'mock-theme.zip');
let fsWriteSub;
let yauzlOpenSpy;

beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(jest.fn());

fsWriteSub = jest.spyOn(fs, 'writeFile').mockImplementation((name, config, options, callback) => {
callback(false);
});
});

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});

beforeEach(() => {
yauzlOpenSpy = jest.spyOn(yauzl, 'open');
});

it('should call yauzl.open with the passed zipPath', async () => {
await extractZipFiles({ zipPath });

expect(yauzlOpenSpy).toHaveBeenCalledTimes(1);
});

it('should save all the files from the zip archive taking into account options.outputNames', async () => {
const newConfigName = 'config2.json';
const outputNames = { 'config.json': newConfigName };
await extractZipFiles({ zipPath, outputNames });

expect(fsWriteSub).toHaveBeenCalledTimes(2);
expect(fsWriteSub).toHaveBeenCalledWith(
'schema.json', expect.anything(), expect.objectContaining({ flag: 'w+' }), expect.any(Function),
);
expect(fsWriteSub).toHaveBeenCalledWith(
newConfigName, expect.anything(), expect.objectContaining({ flag: 'w+' }), expect.any(Function),
);
});

it('should not save files specified in options.exclude', async () => {
const exclude = ['config.json'];
await extractZipFiles({ zipPath, exclude });

expect(fsWriteSub).toHaveBeenCalledTimes(1);
expect(fsWriteSub).toHaveBeenCalledWith(
'schema.json', expect.anything(), expect.objectContaining({ flag: 'w+' }), expect.any(Function),
);
});

it('should save the file specified in options.fileToExtract only', async () => {
const fileToExtract = 'config.json';
await extractZipFiles({ zipPath, fileToExtract });

expect(fsWriteSub).toHaveBeenCalledTimes(1);
expect(fsWriteSub).toHaveBeenCalledWith(
fileToExtract, expect.anything(), expect.objectContaining({ flag: 'w+' }), expect.any(Function),
);
});

it('should throw an error when the file with name options.fileToExtract was not found', async () => {
const fileToExtract = 'I dont exist.txt';

await expect(
extractZipFiles({ zipPath, fileToExtract }),
).rejects.toThrow(/not found/);

expect(fsWriteSub).toHaveBeenCalledTimes(0);
});
});
});
129 changes: 17 additions & 112 deletions lib/stencil-download.utils.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,28 @@
const fetch = require('node-fetch');
const yauzl = require('yauzl');
const fs = require('fs');
const tmp = require('tmp');
const path = require('path');
const tmp = require('tmp-promise');
const { extractZipFiles } = require('./archiveManager');
const { fetchFile } = require('./utils/networkUtils');

const utils = {};

module.exports = utils;

utils.downloadThemeFiles = (options, callback) => {
tmp.file(function _tempFileCreated(err, tempThemePath, fd, cleanupCallback) {
if (err) {
callback(err);
}

Promise.resolve()
.then(() => fetch(options.downloadUrl))
.then(response => new Promise((resolve, reject) => {
if (!response.ok) {
reject(`Unable to download theme files from ${options.downloadUrl}: ${response.statusText}`);
}

response.body.pipe(fs.createWriteStream(tempThemePath))
.on('finish', () => resolve(tempThemePath))
.on('error', reject);
}))
.then(tempThemePath => new Promise((resolve, reject) => {
let foundMatch = false;

console.log('ok'.green + ' -- Theme files downloaded');
console.log('ok'.green + ' -- Extracting theme files');

yauzl.open(tempThemePath, {lazyEntries: true}, (error, zipFile) => {
if (error) {
return reject(error);
}

zipFile.on('entry', entry => {
zipFile.openReadStream(entry, (readStreamError, readStream) => {
if (readStreamError) {
return reject(readStreamError);
}

let configFileData = '';

if (options.file && options.file.length) {
if (options.file !== entry.fileName) {
zipFile.readEntry();
return;
}
foundMatch = true;
} else if (options.exclude && options.exclude.length) {
// Do not process any file or directory within the exclude option
for (const excludeItem of options.exclude) {
if (entry.fileName.startsWith(excludeItem)) {
zipFile.readEntry();
return;
}
}
}

// Create a directory if the parent directory does not exists
const parsedPath = path.parse(entry.fileName);

if (parsedPath.dir && !fs.existsSync(parsedPath.dir)) {
fs.mkdirSync(parsedPath.dir, {recursive: true});
}

// If file is a directory, then move to next
if (/\/$/.test(entry.fileName)) {
zipFile.readEntry();
return;
}

readStream.on('end', () => {
if (entry.fileName.endsWith('.json')) {
configFileData = JSON.stringify(JSON.parse(configFileData), null, 2);
}
utils.downloadThemeFiles = async options => {
const { path: tempThemePath, cleanup } = await tmp.file();

fs.writeFile(entry.fileName, configFileData, {flag: 'w+'}, error => {
if (error) {
reject(error);
}
try {
await fetchFile(options.downloadUrl, tempThemePath);
} catch (err) {
throw new Error(`Unable to download theme files from ${options.downloadUrl}: ${err.message}`);
}

// Close read if file requested is found
if (options.file && options.file.length) {
console.log('ok'.green + ' -- Theme files extracted');
zipFile.close();
resolve(options);
} else {
zipFile.readEntry();
}
});
});
console.log('ok'.green + ' -- Theme files downloaded');
console.log('ok'.green + ' -- Extracting theme files');

readStream.on('data', chunk => {
configFileData += chunk;
});
});
});
await extractZipFiles({ zipPath: tempThemePath, fileToExtract: options.file, exclude: options.exclude });

zipFile.readEntry();
console.log('ok'.green + ' -- Theme files extracted');

zipFile.once('end', function () {
if (!foundMatch && (options.file && options.file.length)) {
console.log('Warning'.yellow + ` -- ${options.file} not found!`);
reject(`${options.file} not found`);
return;
}
await cleanup();

console.log('ok'.green + ' -- Theme files extracted');
zipFile.close();
resolve(options);
});
});
}))
.then(() => {
cleanupCallback();
callback(null, options);
})
.catch(callback);
});
return options;
};
Loading

0 comments on commit b1e932c

Please sign in to comment.