diff --git a/.rescriptsrc.js b/.rescriptsrc.js
index 2514c4f..75244a2 100644
--- a/.rescriptsrc.js
+++ b/.rescriptsrc.js
@@ -8,6 +8,11 @@ module.exports = [
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')",
+ };
return config;
},
[
diff --git a/e2e/Import.e2e.ts b/e2e/Import.e2e.ts
index cb66d93..23d36c6 100644
--- a/e2e/Import.e2e.ts
+++ b/e2e/Import.e2e.ts
@@ -1,3 +1,4 @@
+import { setElectronDialogHandler } from 'testcafe-browser-provider-electron';
import { assertNoConsoleErrors, cleanup, getPageUrl, pageUrl } from './helpers';
import { Home } from './pages';
@@ -10,3 +11,29 @@ fixture`Import`
test('should be on the import network route', async t => {
await t.expect(getPageUrl()).match(/network_import/);
});
+
+test('when the user aborts the file dialog, nothing should happen', async t => {
+ let dialogOpened = false;
+ await setElectronDialogHandler(
+ type => {
+ if (type === 'save-dialog' || type === 'open-dialog') {
+ dialogOpened = true;
+ return undefined;
+ }
+ return;
+ },
+ { dialogOpened },
+ );
+
+ // to make our input clickable in Testcafe we have to make it visible
+ await t.eval(() => {
+ const input = window.document.querySelector('input');
+ input.style.display = '';
+ return;
+ });
+
+ return t
+ .click('input')
+ .expect(dialogOpened)
+ .ok();
+});
diff --git a/e2e/pages/Home.ts b/e2e/pages/Home.ts
index 559af64..b564264 100644
--- a/e2e/pages/Home.ts
+++ b/e2e/pages/Home.ts
@@ -3,12 +3,10 @@ import { Selector } from 'testcafe';
class Home {
getStarted = Selector('.ant-card-head-title');
createButton = Selector('button').withExactText('Create a Lightning Network');
- importButton = Selector('button').withExactText('Import a Lightning Network');
cardTitles = Selector('.ant-card-head-title');
getStartedText = () => this.getStarted.innerText;
clickCreateButton = async (t: TestController) => t.click(this.createButton);
- clickImportButton = async (t: TestController) => t.click(this.importButton);
getCardTitleWithText = (text: string) => this.cardTitles.withExactText(text);
}
diff --git a/src/__mocks__/fs-extra.js b/src/__mocks__/fs-extra.js
index 320bc9e..701df48 100644
--- a/src/__mocks__/fs-extra.js
+++ b/src/__mocks__/fs-extra.js
@@ -4,4 +4,5 @@ module.exports = {
readFile: jest.fn(),
remove: jest.fn(),
ensureDir: jest.fn(),
+ copyFile: jest.fn(),
};
diff --git a/src/components/network/ImportNetwork.spec.tsx b/src/components/network/ImportNetwork.spec.tsx
new file mode 100644
index 0000000..25a2879
--- /dev/null
+++ b/src/components/network/ImportNetwork.spec.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import { renderWithProviders } from 'utils/tests';
+import { NETWORK_IMPORT } from 'components/routing';
+import ImportNetwork from './ImportNetwork';
+
+describe('ImportNetwork component', () => {
+ beforeEach(jest.useFakeTimers);
+ afterEach(jest.useRealTimers);
+
+ const renderComponent = () => {
+ const history = createMemoryHistory({ initialEntries: [NETWORK_IMPORT] });
+ const location = { pathname: NETWORK_IMPORT, search: '', hash: '', state: undefined };
+ const match = { isExact: true, path: '', url: NETWORK_IMPORT, params: {} };
+ const cmp = ;
+ const result = renderWithProviders(cmp, { route: NETWORK_IMPORT });
+ return { ...result };
+ };
+
+ it('has a file uploader', async () => {
+ const { getByText } = renderComponent();
+ expect(
+ getByText('Click or drag ZIP file to this area to import'),
+ ).toBeInTheDocument();
+ });
+
+ it('should navigate home when back button clicked', () => {
+ const { getByLabelText, history } = renderComponent();
+ const backBtn = getByLabelText('Back');
+ expect(backBtn).toBeInTheDocument();
+ fireEvent.click(backBtn);
+ expect(history.location.pathname).toEqual('/');
+ });
+});
diff --git a/src/components/network/ImportNetwork.tsx b/src/components/network/ImportNetwork.tsx
index 33f65da..a484632 100644
--- a/src/components/network/ImportNetwork.tsx
+++ b/src/components/network/ImportNetwork.tsx
@@ -1,7 +1,8 @@
import React, { useState } from 'react';
+import { RouteComponentProps } from 'react-router';
import { UploadOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
-import { Button, Card, PageHeader, Upload } from 'antd';
+import { Card, PageHeader, Spin, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload';
import { usePrefixedTranslation } from 'hooks';
import { useTheme } from 'hooks/useTheme';
@@ -10,6 +11,11 @@ import { ThemeColors } from 'theme/colors';
import { HOME } from 'components/routing';
const Styled = {
+ Container: styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ `,
PageHeader: styled(PageHeader)<{ colors: ThemeColors['pageHeader'] }>`
border: 1px solid ${props => props.colors.border};
border-radius: 2px;
@@ -17,80 +23,72 @@ const Styled = {
margin-bottom: 10px;
flex: 0;
`,
- ButtonContainer: styled.div`
- margin-top: 20px;
- display: flex;
- justify-content: space-evenly;
-
- button {
- width: 200px;
- }
+ Card: styled(Card)`
+ flex: 1;
+ `,
+ Dragger: styled(Upload.Dragger)`
+ flex: 1;
`,
};
-const ImportNetwork: React.SFC = () => {
- const [file, setFile] = useState();
+const ImportNetwork: React.FC = () => {
const { navigateTo, notify } = useStoreActions(s => s.app);
const { importNetwork } = useStoreActions(s => s.network);
const { l } = usePrefixedTranslation('cmps.network.ImportNetwork');
+ const [importing, setImporting] = useState(false);
+
+ const doImportNetwork = (file: RcFile) => {
+ setImporting(true);
+
+ // we kick off the import promise, but don't wait for it
+ importNetwork(file.path)
+ .then(network => {
+ notify({ message: l('importSuccess', { name: network.name }) });
+ navigateTo(HOME);
+ })
+ .catch(error => {
+ notify({ message: l('importError', { file: file.name }), error });
+ })
+ .then(() => {
+ setImporting(false);
+ });
+
+ // return false to prevent the Upload.Dragger from sending the file somewhere
+ return false;
+ };
const theme = useTheme();
return (
- <>
+
navigateTo(HOME)}
/>
-
-
+ {
- setFile(undefined);
-
- // return false makes the operation stop. we don't want to actually
- // interact with a server, just operate in memory
- return false;
- }}
- beforeUpload={file => {
- setFile(file);
-
- // return false makes the upload stop. we don't want to actually
- // upload this file anywhere, just store it in memory
- return false;
- }}
+ disabled={importing}
+ beforeUpload={doImportNetwork}
>
-
-
-
- {l('fileDraggerArea')}
-
-
-
-
-
-
- >
+ {importing ? (
+ <>
+
+ {l('importText')}
+ >
+ ) : (
+ <>
+
+
+
+ {l('fileDraggerArea')}
+ >
+ )}
+
+
+
);
};
diff --git a/src/components/network/NetworkView.spec.tsx b/src/components/network/NetworkView.spec.tsx
index 1e47136..013dad4 100644
--- a/src/components/network/NetworkView.spec.tsx
+++ b/src/components/network/NetworkView.spec.tsx
@@ -1,6 +1,7 @@
import React from 'react';
+import electron from 'electron';
import fsExtra from 'fs-extra';
-import { fireEvent, wait } from '@testing-library/dom';
+import { fireEvent, wait, waitForElement } from '@testing-library/dom';
import { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Status } from 'shared/types';
@@ -11,6 +12,7 @@ import {
injections,
lightningServiceMock,
renderWithProviders,
+ suppressConsoleErrors,
testCustomImages,
} from 'utils/tests';
import NetworkView from './NetworkView';
@@ -23,6 +25,10 @@ const dockerServiceMock = injections.dockerService as jest.Mocked<
typeof injections.dockerService
>;
+jest.mock('utils/zip', () => ({
+ zip: jest.fn(),
+}));
+
describe('NetworkView Component', () => {
const renderComponent = (
id: string | undefined,
@@ -259,6 +265,52 @@ describe('NetworkView Component', () => {
});
});
+ describe('delete network', () => {
+ it('should show the confirm modal', async () => {
+ const { getByLabelText, getByText, findByText } = renderComponent('1');
+ fireEvent.mouseOver(getByLabelText('more'));
+ fireEvent.click(await findByText('Delete'));
+ expect(
+ getByText('Are you sure you want to delete this network?'),
+ ).toBeInTheDocument();
+ expect(getByText('Yes')).toBeInTheDocument();
+ expect(getByText('Cancel')).toBeInTheDocument();
+ });
+
+ it('should delete the network', async () => {
+ const { getByLabelText, getByText, findByText, network } = renderComponent(
+ '1',
+ Status.Started,
+ );
+ fireEvent.mouseOver(getByLabelText('more'));
+ fireEvent.click(await findByText('Delete'));
+ fireEvent.click(await findByText('Yes'));
+ // wait for the error notification to be displayed
+ await waitForElement(() => getByLabelText('check-circle'));
+ expect(
+ getByText("The network 'test network' and its data has been deleted!"),
+ ).toBeInTheDocument();
+ expect(fsMock.remove).toBeCalledWith(expect.stringContaining(network.path));
+ });
+
+ it('should display an error if the delete fails', async () => {
+ // antd Modal.confirm logs a console error when onOk fails
+ // this suppresses those errors from being displayed in test runs
+ await suppressConsoleErrors(async () => {
+ fsMock.remove = jest.fn().mockRejectedValue(new Error('cannot delete'));
+ const { getByLabelText, getByText, findByText, store } = renderComponent('1');
+ fireEvent.mouseOver(getByLabelText('more'));
+ fireEvent.click(await findByText('Delete'));
+ fireEvent.click(await findByText('Yes'));
+ // wait for the error notification to be displayed
+ await waitForElement(() => getByLabelText('close-circle'));
+ expect(getByText('cannot delete')).toBeInTheDocument();
+ expect(store.getState().network.networks).toHaveLength(1);
+ expect(store.getState().designer.allCharts[1]).toBeDefined();
+ });
+ });
+ });
+
describe('export network', () => {
beforeEach(jest.useFakeTimers);
afterEach(jest.useRealTimers);
@@ -278,5 +330,32 @@ describe('NetworkView Component', () => {
await wait(() => jest.runOnlyPendingTimers());
expect(getByText('Cannot export a running network')).toBeInTheDocument();
});
+
+ it('should export a stopped network', async () => {
+ const { primaryBtn, getByText, getByLabelText } = renderComponent('1');
+ expect(primaryBtn).toHaveTextContent('Start');
+
+ fireEvent.mouseOver(getByLabelText('more'));
+ await wait(() => jest.runOnlyPendingTimers());
+
+ fireEvent.click(getByText('Export'));
+ await wait(() => jest.runOnlyPendingTimers());
+ expect(getByText("Exported 'test network'", { exact: false })).toBeInTheDocument();
+ });
+
+ it('should not export the network if the user closes the file save dialogue', async () => {
+ // returns undefined if user closes the window
+ electron.remote.dialog.showSaveDialog = jest.fn(() => ({})) as any;
+
+ const { primaryBtn, queryByText, getByText, getByLabelText } = renderComponent('1');
+ expect(primaryBtn).toHaveTextContent('Start');
+
+ fireEvent.mouseOver(getByLabelText('more'));
+ await wait(() => jest.runOnlyPendingTimers());
+
+ fireEvent.click(getByText('Export'));
+ await wait(() => jest.runOnlyPendingTimers());
+ expect(queryByText("Exported 'test network'", { exact: false })).toBeNull();
+ });
});
});
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 43bea9f..100648b 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -262,9 +262,9 @@
"cmps.network.NetworkView.notReadyToExportDescription": "Make sure the network is completely stopped before exporting it.",
"cmps.network.ImportNetwork.title": "Import a pre-defined Lightning Network",
"cmps.network.ImportNetwork.fileDraggerArea": "Click or drag ZIP file to this area to import",
- "cmps.network.ImportNetwork.removeButton": "Remove",
- "cmps.network.ImportNetwork.importButton": "Import",
+ "cmps.network.ImportNetwork.importText": "Importing...",
"cmps.network.ImportNetwork.importSuccess": "Imported network '{{name}}' successfully",
+ "cmps.network.ImportNetwork.importError": "Could not import '{{file}}'",
"cmps.network.NewNetwork.title": "Create a new Lightning Network",
"cmps.network.NewNetwork.nameLabel": "Network Name",
"cmps.network.NewNetwork.namePhldr": "My Lightning Simnet",
diff --git a/src/store/models/network.spec.ts b/src/store/models/network.spec.ts
index 39b346f..d02ba18 100644
--- a/src/store/models/network.spec.ts
+++ b/src/store/models/network.spec.ts
@@ -1,6 +1,5 @@
+import electron from 'electron';
import * as log from 'electron-log';
-import { pathExists } from 'fs-extra';
-import { join } from 'path';
import { wait } from '@testing-library/react';
import detectPort from 'detect-port';
import { createStore } from 'easy-peasy';
@@ -27,11 +26,24 @@ jest.mock('utils/files', () => ({
}));
jest.mock('utils/network', () => ({
- importNetworkFromZip: jest.fn().mockImplementation(() => {
- const tests = jest.requireActual('utils/tests');
- const network = tests.getNetwork();
- return [network, tests.initChartFromNetwork(network)];
- }),
+ ...jest.requireActual('utils/network'),
+ importNetworkFromZip: () => {
+ return jest.fn().mockImplementation(() => {
+ const network = {
+ id: 1,
+ nodes: {
+ bitcoin: [{}],
+ lightning: [{}],
+ },
+ };
+ return [network, {}];
+ })();
+ },
+}));
+
+jest.mock('utils/zip', () => ({
+ zip: jest.fn(),
+ unzip: jest.fn(),
}));
const filesMock = files as jest.Mocked;
@@ -829,42 +841,49 @@ describe('Network model', () => {
});
describe('Export', () => {
- let exportedZip: string | undefined;
- afterEach(async () => {
- if (!exportedZip) {
- return;
- }
- await files.rm(exportedZip);
- });
-
- it('should export a network', async () => {
+ it('should export a network and show a save dialogue', async () => {
const { network: networkActions } = store.getActions();
- const network = getNetwork();
- await networkActions.addNetwork({
- name: 'test',
- lndNodes: 1,
- clightningNodes: 2,
- bitcoindNodes: 1,
- });
+ const spy = jest.spyOn(electron.remote.dialog, 'showSaveDialog');
+
+ const exported = await networkActions.exportNetwork(getNetwork());
+ expect(exported).toBeDefined();
- exportedZip = await networkActions.exportNetwork(network);
- expect(exportedZip).toBeDefined();
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const exists = await pathExists(exportedZip!);
- expect(exists).toBeTruthy();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should not export a network if the user closes the dialogue', async () => {
+ const mock = electron.remote.dialog.showSaveDialog as jest.MockedFunction<
+ typeof electron.remote.dialog.showSaveDialog
+ >;
+ // returns undefined if user closes the window
+ mock.mockImplementation(() => ({} as any));
+
+ const { network: networkActions } = store.getActions();
+ const exported = await networkActions.exportNetwork(getNetwork());
+ expect(exported).toBeUndefined();
});
});
- describe.only('Import', () => {
+ describe('Import', () => {
it('should import a network', async () => {
const { network: networkActions } = store.getActions();
+ const statePreImport = store.getState();
- const imported = await networkActions.importNetwork(
- join(__dirname, '..', '..', 'utils', 'tests', 'resources', 'zipped-network.zip'),
+ const imported = await networkActions.importNetwork('zip');
+ expect(imported.id).toBeDefined();
+ expect(imported.nodes.bitcoin.length).toBeGreaterThan(0);
+ expect(imported.nodes.lightning.length).toBeGreaterThan(0);
+
+ const statePostImport = store.getState();
+
+ expect(statePostImport.network.networks.length).toEqual(
+ statePreImport.network.networks.length + 1,
);
- console.log(imported);
- expect(true).toBe(false);
+
+ const numChartsPost = Object.keys(statePostImport.designer.allCharts).length;
+ const numChartsPre = Object.keys(statePreImport.designer.allCharts).length;
+ expect(numChartsPost).toEqual(numChartsPre + 1);
});
});
});
diff --git a/src/store/models/network.ts b/src/store/models/network.ts
index b9fda44..69c1b9c 100644
--- a/src/store/models/network.ts
+++ b/src/store/models/network.ts
@@ -1,7 +1,7 @@
import { remote, SaveDialogOptions } from 'electron';
import { info } from 'electron-log';
import { copyFile, ensureDir } from 'fs-extra';
-import { basename, join } from 'path';
+import { join } from 'path';
import { push } from 'connected-react-router';
import { Action, action, Computed, computed, Thunk, thunk } from 'easy-peasy';
import {
@@ -25,6 +25,7 @@ import {
getOpenPorts,
importNetworkFromZip,
OpenPorts,
+ zipNameForNetwork,
zipNetwork,
} from 'utils/network';
import { prefixTranslation } from 'utils/translate';
@@ -620,6 +621,19 @@ const networkModel: NetworkModel = {
}),
exportNetwork: thunk(async (_, network, { getStoreState }) => {
+ const options: SaveDialogOptions = {
+ title: 'title',
+ defaultPath: zipNameForNetwork(network),
+ properties: ['promptToCreate', 'createDirectory'],
+ } as any; // types are broken, but 'properties' allow us to customize how the dialog performs
+ const { filePath: zipDestination } = await remote.dialog.showSaveDialog(options);
+
+ // user aborted dialog
+ if (!zipDestination) {
+ info('User aborted network export');
+ return;
+ }
+
info('exporting network', network);
const {
@@ -632,22 +646,11 @@ const networkModel: NetworkModel = {
const zipped = await zipNetwork(network, allCharts[network.id]);
- const options: SaveDialogOptions = {
- title: 'title',
- defaultPath: basename(zipped),
- properties: ['promptToCreate', 'createDirectory'],
- } as any; // types are broken, but 'properties' allow us to customize how the dialog performs
- const { filePath: zipDestination } = await remote.dialog.showSaveDialog(options);
-
- // user aborted dialog
- if (!zipDestination) {
- return;
- }
-
await copyFile(zipped, zipDestination);
info('exported network to', zipDestination);
return zipDestination;
}),
+
importNetwork: thunk(async (_, path, { getStoreState, getStoreActions }) => {
const {
network: { networks },
@@ -656,7 +659,6 @@ const networkModel: NetworkModel = {
const { network: networkActions } = getStoreActions();
const { designer: designerActions } = getStoreActions();
- console.log('func', importNetworkFromZip);
const [newNetwork, chart] = await importNetworkFromZip(path, networks);
networkActions.add(newNetwork);
diff --git a/src/utils/network.spec.ts b/src/utils/network.spec.ts
index a6def93..501ec00 100644
--- a/src/utils/network.spec.ts
+++ b/src/utils/network.spec.ts
@@ -1,21 +1,12 @@
-import { join } from 'path';
import detectPort from 'detect-port';
import { LndNode, NodeImplementation, Status } from 'shared/types';
import { Network } from 'types';
import { defaultRepoState } from './constants';
-import {
- getImageCommand,
- getNetworkFromZip,
- getOpenPortRange,
- getOpenPorts,
- OpenPorts,
-} from './network';
+import { getImageCommand, getOpenPortRange, getOpenPorts, OpenPorts } from './network';
import { getNetwork, testManagedImages } from './tests';
const mockDetectPort = detectPort as jest.Mock;
-jest.mock('fs-extra', () => jest.requireActual('fs-extra'));
-
describe('Network Utils', () => {
describe('getImageCommand', () => {
it('should return the commands for managed images', () => {
@@ -168,22 +159,4 @@ describe('Network Utils', () => {
expect(ports[lnd2.name].rest).toBe(lnd2.ports.rest + 1);
});
});
-
- describe('getNetworkFromZip', () => {
- it('reads zipped-network.zip', async () => {
- const newId = 90;
- const [network] = await getNetworkFromZip(
- join(__dirname, 'tests', 'resources', 'zipped-network.zip'),
- newId,
- );
-
- expect(network.id).toBe(newId);
- expect(network.nodes.bitcoin).toHaveLength(1);
- expect(network.nodes.lightning).toHaveLength(3);
- });
-
- it('throws on non-existent zip', async () => {
- await expect(getNetworkFromZip('nonexistent', 9)).rejects.toThrow();
- });
- });
});
diff --git a/src/utils/network.ts b/src/utils/network.ts
index 3a2793d..b8a3e03 100644
--- a/src/utils/network.ts
+++ b/src/utils/network.ts
@@ -349,6 +349,10 @@ const sanitizeFileName = (name: string): string => {
return withoutSpaces.replace(/[^0-9a-zA-Z-._]/g, '');
};
+/** Creates a suitable file name for a Zip archive of the given network */
+export const zipNameForNetwork = (network: Network): string =>
+ `polar-${sanitizeFileName(network.name)}.zip`;
+
/**
* Archive the given network into a folder with the following content:
*
@@ -362,7 +366,7 @@ const sanitizeFileName = (name: string): string => {
* @return Path of created `.zip` file
*/
export const zipNetwork = async (network: Network, chart: IChart): Promise => {
- const destination = join(tmpdir(), `polar-${sanitizeFileName(network.name)}.zip`);
+ const destination = join(tmpdir(), zipNameForNetwork(network));
await zip({
destination,
diff --git a/src/utils/zip.spec.ts b/src/utils/zip.spec.ts
index 503a69d..5ab87fd 100644
--- a/src/utils/zip.spec.ts
+++ b/src/utils/zip.spec.ts
@@ -1,11 +1,18 @@
import { promises as fs } from 'fs';
import { join } from 'path';
+import archiver from 'archiver';
import { tmpdir } from 'os';
import { unzip, zip } from './zip';
jest.mock('fs-extra', () => jest.requireActual('fs-extra'));
describe('unzip', () => {
+ it("fail to unzip something that isn't a zip", async () => {
+ return expect(
+ unzip(join(__dirname, 'tests', 'resources', 'bar.txt'), 'foobar'),
+ ).rejects.toThrow();
+ });
+
it('unzips test.zip', async () => {
const destination = join(tmpdir(), 'zip-test-' + Date.now());
await unzip(join(__dirname, 'tests', 'resources', 'test.zip'), destination);
@@ -36,7 +43,7 @@ describe('unzip', () => {
expect(bazEntries.map(e => e.name)).toContain('qux.ts');
const qux = await fs.readFile(join(destination, 'baz', 'qux.ts'));
- expect(qux.toString('utf-8')).toBe("console.log('qux');\n");
+ expect(qux.toString('utf-8')).toBe('console.log("qux");\n');
const bar = await fs.readFile(join(destination, 'bar.txt'));
expect(bar.toString('utf-8')).toBe('bar\n');
@@ -44,9 +51,13 @@ describe('unzip', () => {
const foo = await fs.readFile(join(destination, 'foo.json'));
expect(foo.toString('utf-8')).toBe(JSON.stringify({ foo: 2 }, null, 4) + '\n');
});
+
+ it("fails to unzip something that doesn't exist", async () => {
+ return expect(unzip('foobar', 'bazfoo')).rejects.toThrow();
+ });
});
-describe.only('zip', () => {
+describe('zip', () => {
it('zips objects', async () => {
const objects: Array<{ name: string; object: any }> = [
{
diff --git a/src/utils/zip.ts b/src/utils/zip.ts
index e46ef2b..2dfe5e7 100644
--- a/src/utils/zip.ts
+++ b/src/utils/zip.ts
@@ -1,7 +1,7 @@
-import { error, info, warn } from 'electron-log';
+import { error, warn } from 'electron-log';
import fs from 'fs';
import { pathExists } from 'fs-extra';
-import { basename, join, resolve } from 'path';
+import { basename } from 'path';
import archiver from 'archiver';
import unzipper from 'unzipper';
@@ -55,50 +55,10 @@ const addStringToZip = (
content: string,
nameInArchive: string,
): void => {
- try {
- archive.append(content, { name: nameInArchive });
- } catch (err) {
- error(`Could not add ${nameInArchive} to zip: ${err}`);
- throw err;
- }
+ archive.append(content, { name: nameInArchive });
+ return;
};
-/**
- * Adds a file to the given ZIP archive. We read the file into
- * memory and then append it to the ZIP archive. There appears
- * to be issues with using Archiver.js with Electron/Webpack,
- * so that's why we have to do it in a somewhat inefficient way.
- *
- * Related issues:
- * * https://github.com/archiverjs/node-archiver/issues/349
- * * https://github.com/archiverjs/node-archiver/issues/403
- * * https://github.com/archiverjs/node-archiver/issues/174
- *
- * @param archive ZIP archive to add the file to
- * @param filePath file to add, absolute path
- * @param nameInArchive name of file in archive
- */
-const addFileToZip = async (
- archive: archiver.Archiver,
- filePath: string,
- nameInArchive: string,
-) => {
- return archive.append(await fs.promises.readFile(filePath), { name: nameInArchive });
-};
-
-// Generate a sequence of all regular files inside the given directory
-async function* getFiles(dir: string): AsyncGenerator {
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
- for (const entry of entries) {
- const res = resolve(dir, entry.name);
- if (entry.isDirectory()) {
- yield* getFiles(res);
- } else if (entry.isFile()) {
- yield res;
- }
- }
-}
-
/**
* Add the given path to the archive. If it's a file we add it directly, it it is a directory
* we recurse over all the files within that directory
@@ -109,17 +69,9 @@ async function* getFiles(dir: string): AsyncGenerator {
const addFileOrDirectoryToZip = async (archive: archiver.Archiver, filePath: string) => {
const isDir = await fs.promises.lstat(filePath).then(res => res.isDirectory());
if (isDir) {
- info('Adding directory to zip file:', filePath);
- for await (const file of getFiles(filePath)) {
- // a typical file might look like this:
- // /home/user/.polar/networks/1/volumes/bitcoind/backend1/regtest/mempool.dat
- // after applying this transformation, we end up with:
- // volumes/bitcoind/backend1/regtest/mempool.dat
- const nameInArchive = join(basename(filePath), file.slice(filePath.length));
- await addFileToZip(archive, file, nameInArchive);
- }
+ archive.directory(filePath, basename(filePath));
} else {
- return addFileToZip(archive, filePath, basename(filePath));
+ archive.file(filePath, { name: basename(filePath) });
}
};