Browse Source

Add more tests for import network functionality

feat: add import/export network functionality
master
Torkel Rogstad 5 years ago
committed by jamaljsr
parent
commit
57f7604795
  1. 5
      .rescriptsrc.js
  2. 27
      e2e/Import.e2e.ts
  3. 2
      e2e/pages/Home.ts
  4. 1
      src/__mocks__/fs-extra.js
  5. 35
      src/components/network/ImportNetwork.spec.tsx
  6. 116
      src/components/network/ImportNetwork.tsx
  7. 81
      src/components/network/NetworkView.spec.tsx
  8. 4
      src/i18n/locales/en-US.json
  9. 85
      src/store/models/network.spec.ts
  10. 30
      src/store/models/network.ts
  11. 29
      src/utils/network.spec.ts
  12. 6
      src/utils/network.ts
  13. 15
      src/utils/zip.spec.ts
  14. 60
      src/utils/zip.ts

5
.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;
},
[

27
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();
});

2
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);
}

1
src/__mocks__/fs-extra.js

@ -4,4 +4,5 @@ module.exports = {
readFile: jest.fn(),
remove: jest.fn(),
ensureDir: jest.fn(),
copyFile: jest.fn(),
};

35
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 = <ImportNetwork history={history} location={location} match={match} />;
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('/');
});
});

116
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<RcFile | undefined>();
const ImportNetwork: React.FC<RouteComponentProps> = () => {
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 (
<>
<Styled.Container>
<Styled.PageHeader
title={l('title')}
colors={theme.pageHeader}
onBack={() => navigateTo(HOME)}
/>
<Card>
<Upload.Dragger
fileList={file ? [file] : []}
<Styled.Card bodyStyle={{ height: '100%' }}>
<Styled.Dragger
// to not display a file in the upload dragger after the user has selected a zip
fileList={undefined}
accept=".zip"
onRemove={async () => {
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}
>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">{l('fileDraggerArea')}</p>
</Upload.Dragger>
<Styled.ButtonContainer>
<Button onClick={() => setFile(undefined)} disabled={!file}>
{l('removeButton')}
</Button>
<Button
onClick={async () => {
try {
// if file is undefined, export button is disabled
// so we can be sure that this assertions is OK
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await importNetwork(file!.path);
notify({ message: l('importSuccess') });
navigateTo(HOME);
} catch (error) {
notify({ message: '', error });
}
}}
type="primary"
disabled={!file}
>
{l('importButton')}
</Button>
</Styled.ButtonContainer>
</Card>
</>
{importing ? (
<>
<Spin size="large" />
<p>{l('importText')}</p>
</>
) : (
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">{l('fileDraggerArea')}</p>
</>
)}
</Styled.Dragger>
</Styled.Card>
</Styled.Container>
);
};

81
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();
});
});
});

4
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",

85
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<typeof files>;
@ -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);
});
});
});

30
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);

29
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();
});
});
});

6
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<string> => {
const destination = join(tmpdir(), `polar-${sanitizeFileName(network.name)}.zip`);
const destination = join(tmpdir(), zipNameForNetwork(network));
await zip({
destination,

15
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 }> = [
{

60
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<string> {
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<string> {
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) });
}
};

Loading…
Cancel
Save