feat: make zip support larger files

chore: improve code readability
This commit is contained in:
Leangseu Kim
2022-03-15 09:49:58 -04:00
committed by leangseu-edx
parent 876f5d9413
commit d033fa310d
5 changed files with 83 additions and 79 deletions

View File

@@ -1,4 +1,4 @@
import JSZip from 'jszip';
import * as zip from '@zip.js/zip.js';
import FileSaver from 'file-saver';
import { StrictDict } from 'utils';
@@ -36,13 +36,21 @@ export const zipFileName = () => {
* @param {blob[]} blobs - file content blobs
* @return {Promise} - zip async process promise.
*/
export const zipFiles = (files, blobs) => {
const zip = new JSZip();
zip.file('manifest.txt', module.genManifest(files));
blobs.forEach((blob, i) => zip.file(files[i].name, blob));
return zip.generateAsync({ type: 'blob' }).then(
zipFile => FileSaver.saveAs(zipFile, module.zipFileName()),
);
export const zipFiles = async (files, blobs) => {
const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));
await zipWriter.add('manifest.txt', new zip.TextReader(module.genManifest(files)));
// forEach or map will create additional thread. It is less readable if we create more
// promise or async function just to circumvent that.
for (let i = 0; i < blobs.length; i++) {
// eslint-disable-next-line no-await-in-loop
await zipWriter.add(files[i].name, new zip.BlobReader(blobs[i]), {
bufferedWrite: true,
});
}
const zipFile = await zipWriter.close();
FileSaver.saveAs(zipFile, module.zipFileName());
};
/**
@@ -73,7 +81,7 @@ export const downloadFiles = () => (dispatch, getState) => {
if (blobs.some(blob => blob === null)) {
throw Error(ERRORS.fetchFailed);
}
module.zipFiles(files, blobs);
return module.zipFiles(files, blobs);
}),
}));
};

View File

@@ -1,21 +1,34 @@
import JSZip from 'jszip';
import * as zip from '@zip.js/zip.js';
import FileSaver from 'file-saver';
import { selectors } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import * as download from './download';
const mockBlobWriter = jest.fn().mockName('BlobWriter');
const mockTextReader = jest.fn().mockName('TextReader');
const mockBlobReader = jest.fn().mockName('BlobReader');
const mockZipAdd = jest.fn();
const mockZipClose = jest.fn();
jest.mock('@zip.js/zip.js', () => {
const files = [];
return {
ZipWriter: jest.fn().mockImplementation(() => ({
add: mockZipAdd.mockImplementation((file, content) => files.push([file, content])),
close: mockZipClose.mockImplementation(() => Promise.resolve(files)),
files,
})),
BlobWriter: () => mockBlobWriter,
TextReader: () => mockTextReader,
BlobReader: () => mockBlobReader,
};
});
jest.mock('file-saver', () => ({
saveAs: jest.fn(),
}));
jest.mock('jszip', () => {
const file = jest.fn();
const zipFile = 'test zip output';
const generateAsync = jest.fn(() => new Promise((resolve) => resolve(zipFile)));
return function zip() {
return { file, zipFile, generateAsync };
};
});
jest.mock('./requests', () => ({
networkRequest: (args) => ({ networkRequest: args }),
@@ -44,10 +57,12 @@ describe('download thunkActions', () => {
const getState = () => testState;
describe('genManifest', () => {
test('returns a list of strings with filename and description for each file', () => {
expect(download.genManifest(response.files)).toEqual([
`Filename: ${files[0].name}\nDescription: ${files[0].description}\nSize: ${files[0].size}`,
`Filename: ${files[1].name}\nDescription: ${files[1].description}\nSize: ${files[0].size}`,
].join('\n\n'));
expect(download.genManifest(response.files)).toEqual(
[
`Filename: ${files[0].name}\nDescription: ${files[0].description}\nSize: ${files[0].size}`,
`Filename: ${files[1].name}\nDescription: ${files[1].description}\nSize: ${files[0].size}`,
].join('\n\n'),
);
});
});
describe('zipFileName', () => {
@@ -55,18 +70,16 @@ describe('download thunkActions', () => {
});
describe('zipFiles', () => {
test('zips files and manifest', () => {
const mockZip = new JSZip();
const mockFilename = 'mock-filename';
download.genManifest = (testFiles) => ({ genManifest: testFiles });
download.zipFileName = () => mockFilename;
const mockZipWriter = new zip.ZipWriter();
return download.zipFiles(files, blobs).then(() => {
expect(mockZip.file.mock.calls).toEqual([
['manifest.txt', download.genManifest(files)],
[files[0].name, blobs[0]],
[files[1].name, blobs[1]],
expect(mockZipWriter.files).toEqual([
['manifest.txt', mockTextReader],
[files[0].name, mockBlobReader],
[files[1].name, mockBlobReader],
]);
expect(mockZip.generateAsync).toHaveBeenCalledWith({ type: 'blob' });
expect(FileSaver.saveAs).toHaveBeenCalledWith(mockZip.zipFile, mockFilename);
expect(mockZipAdd).toHaveBeenCalledTimes(mockZipWriter.files.length);
expect(mockZipClose).toHaveBeenCalledTimes(1);
expect(FileSaver.saveAs).toHaveBeenCalledTimes(1);
});
});
});
@@ -82,20 +95,28 @@ describe('download thunkActions', () => {
window.fetch = fetch;
});
it('returns blob output if successful', () => {
window.fetch.mockReturnValue(new Promise(resolve => resolve({ ok: true, blob: () => blob })));
return download.downloadFile(files[0]).then(val => expect(val).toEqual(blob));
window.fetch.mockReturnValue(
new Promise((resolve) => resolve({ ok: true, blob: () => blob })),
);
return download
.downloadFile(files[0])
.then((val) => expect(val).toEqual(blob));
});
it('returns null if not successful', () => {
window.fetch.mockReturnValue(new Promise(resolve => resolve({ ok: false })));
return download.downloadFile(files[0]).then(val => expect(val).toEqual(null));
window.fetch.mockReturnValue(
new Promise((resolve) => resolve({ ok: false })),
);
return download
.downloadFile(files[0])
.then((val) => expect(val).toEqual(null));
});
});
describe('downloadBlobs', () => {
it('returns a joing promise mapping all files to download action', async () => {
download.downloadFile = (file) => new Promise(resolve => resolve(file.name));
download.downloadFile = (file) => new Promise((resolve) => resolve(file.name));
const responses = await download.downloadBlobs(files);
expect(responses).toEqual(files.map(file => file.name));
expect(responses).toEqual(files.map((file) => file.name));
});
});
@@ -103,28 +124,27 @@ describe('download thunkActions', () => {
beforeEach(() => {
dispatch = jest.fn();
selectors.grading.selected.response = () => ({ files });
module.zipFiles = jest.fn();
});
it('dispatches network request with downloadFiles key', () => {
download.downloadBlobs = () => new Promise(resolve => resolve(blobs));
module.downloadBlobs = () => new Promise((resolve) => resolve(blobs));
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
expect(networkRequest.requestKey).toEqual(RequestKeys.downloadFiles);
});
it('dispatches network request for downloadFiles, zipping output of downloadBlobs', () => {
download.downloadBlobs = () => new Promise(resolve => resolve(blobs));
const zipSpy = jest.spyOn(download, 'zipFiles').mockImplementation(() => ({}));
module.downloadBlobs = () => new Promise((resolve) => resolve(blobs));
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
return networkRequest.promise.then(() => {
expect(zipSpy).toHaveBeenCalledWith(files, blobs);
networkRequest.promise.then(() => {
expect(module.zipFile).toHaveBeenCalledWith(files, blobs);
});
});
it('throws an error if any of the blobs fail to download', () => {
jest.spyOn(download, 'zipFiles').mockImplementation(() => ({}));
download.downloadBlobs = () => new Promise((resolve) => resolve([1, null, 2]));
it('throws an error on failure', () => {
module.downloadBlobs = () => new Promise((resolve, reject) => reject());
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
return expect(networkRequest.promise).rejects.toThrow(download.ERRORS.fetchFailed);
expect(networkRequest.promise).rejects.toThrow('Fetch failed');
});
});
});