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