diff --git a/.rescriptsrc.js b/.rescriptsrc.js index 75244a2..bd08444 100644 --- a/.rescriptsrc.js +++ b/.rescriptsrc.js @@ -7,12 +7,9 @@ module.exports = [ // add support for hot reload of hooks config.resolve.alias['react-dom'] = '@hot-loader/react-dom'; // https://github.com/websockets/ws/issues/1538#issuecomment-577627369 - config.resolve.alias['ws'] = path.resolve(path.join(__dirname, 'node_modules/ws/index.js' )) - // fix for archiver module with webpack - // See https://github.com/archiverjs/node-archiver/issues/403 - config.externals = { - archiver: "require('archiver')", - }; + config.resolve.alias['ws'] = path.resolve( + path.join(__dirname, 'node_modules/ws/index.js'), + ); return config; }, [ diff --git a/electron/appIpcListener.ts b/electron/appIpcListener.ts index 3f9e619..74cdf8d 100644 --- a/electron/appIpcListener.ts +++ b/electron/appIpcListener.ts @@ -6,6 +6,7 @@ import { ipcChannels } from '../src/shared'; import { APP_ROOT, BASE_URL } from './constants'; import { httpProxy } from './httpProxy'; import { clearProxyCache } from './lnd/lndProxyServer'; +import { unzip, zip } from './utils/zip'; const openWindow = async (args: { url: string }): Promise => { debug('openWindow', args); @@ -60,6 +61,8 @@ const listeners: { [ipcChannels.openWindow]: openWindow, [ipcChannels.clearCache]: clearCache, [ipcChannels.http]: httpProxy, + [ipcChannels.zip]: zip, + [ipcChannels.unzip]: unzip, }; /** diff --git a/src/utils/zip.ts b/electron/utils/zip.ts similarity index 72% rename from src/utils/zip.ts rename to electron/utils/zip.ts index 31b501d..cc52df6 100644 --- a/src/utils/zip.ts +++ b/electron/utils/zip.ts @@ -9,35 +9,32 @@ import unzipper from 'unzipper'; * @param filePath the path to the zip file * @param destination the folder to extract to */ -export const unzip = (filePath: string, destination: string): Promise => { - return new Promise(async (resolve, reject) => { - try { - const exists = await pathExists(filePath); - if (!exists) { - throw Error(`${filePath} does not exist!`); - } - const stream = createReadStream(filePath).pipe( - unzipper.Extract({ path: destination }), - ); +export const unzip = (args: { filePath: string; destination: string }): Promise => + new Promise(async (resolve, reject) => { + const { filePath, destination } = args; + const exists = await pathExists(filePath); + if (!exists) { + throw Error(`${filePath} does not exist!`); + } + const stream = createReadStream(filePath).pipe( + unzipper.Extract({ path: destination }), + ); - stream.on('close', resolve); - stream.on('error', err => { - error(`Could not unzip ${filePath} into ${destination}:`, err); - reject(err); - }); - } catch (err) { + stream.on('close', resolve); + stream.on('error', err => { + error(`Could not unzip ${filePath} into ${destination}:`, err); reject(err); - } + }); }); -}; /** * Zips the contents of a folder * @param source the folder path containing the files to zip * @param destination the file path of where to store the zip */ -export const zip = (source: string, destination: string): Promise => +export const zip = (args: { source: string; destination: string }): Promise => new Promise(async (resolve, reject) => { + const { source, destination } = args; info(`zipping ${source} to ${destination}`); const archive = archiver('zip'); archive.on('finish', () => resolve()); diff --git a/package.json b/package.json index e34deb5..1042a99 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "@radar/lnrpc": "0.9.1-beta", "@types/detect-port": "1.1.0", + "archiver": "3.1.1", "detect-port": "1.3.0", "docker-compose": "0.23.3", "dockerode": "3.1.0", @@ -48,7 +49,9 @@ "electron-is-dev": "1.1.0", "electron-log": "4.1.0", "electron-window-state": "5.0.3", + "fs-extra": "9.0.0", "shell-env": "3.0.0", + "unzipper": "0.10.10", "xterm": "4.4.0", "xterm-addon-fit": "0.3.0" }, @@ -83,7 +86,6 @@ "@typescript-eslint/eslint-plugin": "2.24.0", "@typescript-eslint/parser": "2.24.0", "antd": "4.0.3", - "archiver": "3.1.1", "babel-plugin-emotion": "10.0.29", "babel-plugin-import": "1.13.0", "bitcoin-core": "3.0.0", @@ -104,7 +106,6 @@ "eslint-config-prettier": "6.10.0", "eslint-plugin-prettier": "3.1.2", "eslint-plugin-react": "7.19.0", - "fs-extra": "9.0.0", "history": "4.10.1", "husky": "4.2.3", "i18next": "19.3.3", @@ -141,7 +142,6 @@ "testcafe-react-selectors": "4.0.0", "ts-node": "8.7.0", "typescript": "3.8.3", - "unzipper": "0.10.10", "utf-8-validate": "5.0.2", "wait-on": "4.0.1", "webpack": "4.41.5", diff --git a/src/components/network/ImportNetwork.spec.tsx b/src/components/network/ImportNetwork.spec.tsx index 91f7314..e9a827a 100644 --- a/src/components/network/ImportNetwork.spec.tsx +++ b/src/components/network/ImportNetwork.spec.tsx @@ -4,21 +4,21 @@ import { join } from 'path'; import { IChart } from '@mrblenny/react-flow-chart'; import { fireEvent } from '@testing-library/react'; import * as os from 'os'; +import * as ipc from 'lib/ipc/ipcService'; import { Network } from 'types'; import { initChartFromNetwork } from 'utils/chart'; import * as files from 'utils/files'; import { getNetwork, renderWithProviders } from 'utils/tests'; -import * as zip from 'utils/zip'; import ImportNetwork from './ImportNetwork'; jest.mock('utils/files'); -jest.mock('utils/zip'); jest.mock('os'); +jest.mock('lib/ipc/ipcService'); const osMock = os as jest.Mocked; const fsMock = fs as jest.Mocked; const filesMock = files as jest.Mocked; -const zipMock = zip as jest.Mocked; +const ipcMock = ipc as jest.Mocked; describe('ImportNetwork component', () => { const exportFilePath = join('/', 'tmp', 'polar', 'file', 'export.json'); @@ -59,10 +59,11 @@ describe('ImportNetwork component', () => { chart = initChartFromNetwork(network); filesMock.read.mockResolvedValue(JSON.stringify({ network, chart })); filesMock.rm.mockResolvedValue(); - zipMock.unzip.mockResolvedValue(); fsMock.copy.mockResolvedValue(true as never); osMock.tmpdir.mockReturnValue('/tmp'); osMock.platform.mockReturnValue('darwin'); + const sender = jest.fn().mockResolvedValue(undefined); + ipcMock.createIpcSender.mockReturnValue(sender); }); it('should display the file upload label', async () => { diff --git a/src/components/network/NetworkView.spec.tsx b/src/components/network/NetworkView.spec.tsx index d0e911a..c36864e 100644 --- a/src/components/network/NetworkView.spec.tsx +++ b/src/components/network/NetworkView.spec.tsx @@ -6,6 +6,7 @@ import { fireEvent, wait, waitForElement } from '@testing-library/dom'; import { act } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Status } from 'shared/types'; +import * as ipc from 'lib/ipc/ipcService'; import { initChartFromNetwork } from 'utils/chart'; import { defaultRepoState } from 'utils/constants'; import { @@ -16,14 +17,13 @@ import { suppressConsoleErrors, testCustomImages, } from 'utils/tests'; -import * as zip from 'utils/zip'; import NetworkView from './NetworkView'; -jest.mock('utils/zip'); +jest.mock('lib/ipc/ipcService'); const fsMock = fsExtra as jest.Mocked; const logMock = log as jest.Mocked; -const zipMock = zip as jest.Mocked; +const ipcMock = ipc as jest.Mocked; const dialogMock = electron.remote.dialog as jest.Mocked; const bitcoindServiceMock = injections.bitcoindService as jest.Mocked< typeof injections.bitcoindService @@ -309,6 +309,11 @@ describe('NetworkView Component', () => { }); describe('export network', () => { + beforeEach(() => { + const sender = jest.fn().mockResolvedValue(undefined); + ipcMock.createIpcSender.mockReturnValue(sender); + }); + it('should fail to export a running network', async () => { const { getByLabelText, findByText } = renderComponent('1', Status.Started); fireEvent.mouseOver(getByLabelText('more')); @@ -339,7 +344,6 @@ describe('NetworkView Component', () => { await wait(() => jest.runOnlyPendingTimers()); expect(logMock.info).toBeCalledWith('User aborted network export'); expect(dialogMock.showSaveDialog).toBeCalled(); - expect(zipMock.zip).not.toBeCalled(); expect(queryByText("Exported 'test network'", { exact: false })).toBeNull(); jest.useRealTimers(); }); diff --git a/src/shared/ipcChannels.ts b/src/shared/ipcChannels.ts index 58d3aee..b6362d6 100644 --- a/src/shared/ipcChannels.ts +++ b/src/shared/ipcChannels.ts @@ -1,8 +1,10 @@ export default { // general app chnnels - openWindow: 'open-Window', + openWindow: 'open-window', clearCache: 'clear-cache', http: 'http', + zip: 'zip', + unzip: 'unzip', // LND proxy channels getInfo: 'get-info', walletBalance: 'wallet-balance', diff --git a/src/store/models/network.spec.ts b/src/store/models/network.spec.ts index c40c0f8..e9a808c 100644 --- a/src/store/models/network.spec.ts +++ b/src/store/models/network.spec.ts @@ -24,11 +24,6 @@ jest.mock('utils/files', () => ({ rm: jest.fn(), })); -jest.mock('utils/zip', () => ({ - zip: jest.fn(), - unzip: jest.fn(), -})); - const filesMock = files as jest.Mocked; const logMock = log as jest.Mocked; const detectPortMock = detectPort as jest.Mock; diff --git a/src/utils/network.ts b/src/utils/network.ts index e59c3ca..b3570ff 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -4,6 +4,7 @@ import { basename, join } from 'path'; import { IChart } from '@mrblenny/react-flow-chart'; import detectPort from 'detect-port'; import { tmpdir } from 'os'; +import { ipcChannels } from 'shared'; import { BitcoinNode, CLightningNode, @@ -14,6 +15,7 @@ import { NodeImplementation, Status, } from 'shared/types'; +import { createIpcSender } from 'lib/ipc/ipcService'; import { CustomImage, DockerRepoImage, @@ -29,7 +31,6 @@ import { range } from './numbers'; import { isVersionCompatible } from './strings'; import { getPolarPlatform } from './system'; import { prefixTranslation } from './translate'; -import { unzip, zip } from './zip'; const { l } = prefixTranslation('utils.network'); @@ -588,7 +589,8 @@ export const importNetworkFromZip = async ( ): Promise<[Network, IChart]> => { // extract zip to a temp folder first const tmpDir = join(tmpdir(), 'polar', basename(zipPath, '.zip')); - await unzip(zipPath, tmpDir); + const ipc = createIpcSender('NetworkUtil', 'app'); + await ipc(ipcChannels.unzip, { filePath: zipPath, destination: tmpDir }); debug(`Extracted '${zipPath}' to '${tmpDir}'`); // read and parse the export.json file @@ -666,5 +668,7 @@ export const zipNetwork = async ( const content = JSON.stringify({ network, chart }); await writeFile(join(network.path, 'export.json'), content); // zip the network dir into the zip path - await zip(network.path, zipPath); + const ipc = createIpcSender('NetworkUtil', 'app'); + await ipc(ipcChannels.zip, { source: network.path, destination: zipPath }); + // await zip(network.path, zipPath); }; diff --git a/src/utils/zip.spec.ts b/src/utils/zip.spec.ts deleted file mode 100644 index f9dd475..0000000 --- a/src/utils/zip.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import fsExtra from 'fs-extra'; -import { join } from 'path'; -import * as archiver from 'archiver'; -import { PassThrough } from 'stream'; -import unzipper from 'unzipper'; -import * as files from 'utils/files'; -import { delay } from './async'; -import { initChartFromNetwork } from './chart'; -import { getNetwork } from './tests'; -import { unzip, zip } from './zip'; - -jest.mock('utils/files'); - -const fsMock = fsExtra as jest.Mocked; -const filesMock = files as jest.Mocked; -const unzipperMock = unzipper as jest.Mocked; -const archiverMock = archiver as jest.Mocked; - -describe('Zip Util', () => { - let unzipStream: PassThrough; - let zipStream: any; - let zippedPaths: string[]; - const rpcPath = join('alice', 'lightningd', 'regtest', 'lightning-rpc'); - - beforeEach(() => { - const network = getNetwork(); - const chart = initChartFromNetwork(network); - filesMock.read.mockResolvedValue(JSON.stringify({ network, chart })); - fsMock.pathExists.mockResolvedValue(true as never); - fsMock.mkdirp.mockResolvedValue(true as never); - fsMock.copy.mockResolvedValue(true as never); - fsMock.createWriteStream.mockReturnValue(new PassThrough() as any); - fsMock.createReadStream.mockImplementation(() => { - return { - pipe: jest.fn(() => { - // return the mock stream when "pipe()" is called - unzipStream = new PassThrough(); - return unzipStream; - }), - } as any; - }); - unzipperMock.Extract.mockImplementation(jest.fn()); - archiverMock.mockImplementation(() => { - // return a fake stream when "archiver()" is called in the app - zipStream = new PassThrough(); - zipStream.file = jest.fn(); - zipStream.directory = jest.fn( - (src: string, dest: string, entryData: archiver.EntryDataFunction) => { - // call the entryData func on some test paths - const mockPaths = ['test.yml', 'other.json', rpcPath]; - zippedPaths = []; - mockPaths.forEach(name => { - const result = entryData({ name }); - if (result) zippedPaths.push(name); - }); - }, - ); - zipStream.append = jest.fn(); - zipStream.finalize = jest.fn(); - // attach a func to emit events on the stream from the tests - zipStream.mockEmit = (event: any, data: any) => zipStream.emit(event, data); - - return zipStream; - }); - }); - - afterEach(() => { - if (unzipStream) unzipStream.destroy(); - if (zipStream) zipStream.destroy(); - }); - - describe('unzip', () => { - it('should unzip a file successfully', async () => { - const promise = unzip('source-path', 'destination-path'); - // emit an error after a small delay - await delay(100); - unzipStream.emit('close'); - await expect(promise).resolves.toBeUndefined(); - }); - - it("fail to unzip something that isn't a zip", async () => { - const promise = unzip('source-path', 'destination-file'); - // fail the unzip stream after a small delay - await delay(100); - unzipStream.emit('error', new Error('test-error')); - await expect(promise).rejects.toThrow(); - }); - - it("fails to unzip something that doesn't exist", async () => { - fsMock.pathExists.mockResolvedValue(false as never); - return expect(unzip('source-path', 'destination-file')).rejects.toThrow(); - }); - }); - - describe('zip', () => { - it('should zip a folder successfully', async () => { - const promise = zip('source-path', 'destination-path'); - // emit an error after a small delay - await delay(100); - zipStream.emit('finish'); - await expect(promise).resolves.toBeUndefined(); - }); - - it('should not include the c-lightning RPC in the zip', async () => { - const promise = zip('source-path', 'destination-path'); - // emit an error after a small delay - await delay(100); - zipStream.emit('finish'); - await expect(promise).resolves.toBeUndefined(); - expect(zippedPaths).not.toContain(rpcPath); - expect(zippedPaths).toEqual(['test.yml', 'other.json']); - }); - - it('should fail if there is an archiver error', async () => { - const promise = zip('source-path', 'destination-path'); - // emit an error after a small delay - await delay(100); - const mockError = new Error('test-error'); - zipStream.emit('error', mockError); - await expect(promise).rejects.toEqual(mockError); - }); - - it('should fail if there is an archiver warning', async () => { - const promise = zip('source-path', 'destination-path'); - // emit a warning after a small delay - const mockError = new Error('test-warning'); - zipStream.emit('warning', mockError); - await expect(promise).rejects.toEqual(mockError); - }); - }); -});