Skip to content

Commit 97ed38c

Browse files
committed
chore: updated behaviour of mkdir command
chore: updated tests for mkdir chore: updated mkdir and added tests chore: added warning for untracked directory fix: removed write.yml chore: working on adding non-zero exit codes chore: updated tests to be more accurate chore: addressed review fix: manually updated type of error chore: simplified streaming paths to handler chore: updated polykey version fix: lint fix: updated tests fix: lint
1 parent 71973d0 commit 97ed38c

File tree

7 files changed

+316
-98
lines changed

7 files changed

+316
-98
lines changed

npmDepsHash

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-k4xOpxG18ymouzdPMKiyOkaIg8hN29OJ8TFcRsCQc3g=
1+
sha256-Wp7VGWLaWozJWyUpJENIsD9Vq5RWgBguALIhZ8Hq+vc=

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
"nexpect": "^0.6.0",
154154
"node-gyp-build": "^4.4.0",
155155
"nodemon": "^3.0.1",
156-
"polykey": "^1.14.0",
156+
"polykey": "^1.15.0",
157157
"prettier": "^3.0.0",
158158
"shelljs": "^0.8.5",
159159
"shx": "^0.3.4",

src/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ class ErrorPolykeyCLIDuplicateEnvName<T> extends ErrorPolykeyCLI<T> {
152152
exitCode = sysexits.USAGE;
153153
}
154154

155+
class ErrorPolykeyCLIMakeDirectory<T> extends ErrorPolykeyCLI<T> {
156+
static description = 'Failed to create one or more directories';
157+
exitCode = 1;
158+
}
159+
155160
export {
156161
ErrorPolykeyCLI,
157162
ErrorPolykeyCLIUncaughtException,
@@ -172,4 +177,5 @@ export {
172177
ErrorPolykeyCLINodePingFailed,
173178
ErrorPolykeyCLIInvalidEnvName,
174179
ErrorPolykeyCLIDuplicateEnvName,
180+
ErrorPolykeyCLIMakeDirectory,
175181
};

src/secrets/CommandMkdir.ts

+64-15
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
import type PolykeyClient from 'polykey/dist/PolykeyClient';
2+
import type { ErrorMessage } from 'polykey/dist/client/types';
23
import CommandPolykey from '../CommandPolykey';
34
import * as binUtils from '../utils';
45
import * as binOptions from '../utils/options';
56
import * as binParsers from '../utils/parsers';
67
import * as binProcessors from '../utils/processors';
8+
import {
9+
ErrorPolykeyCLIMakeDirectory,
10+
ErrorPolykeyCLIUncaughtException,
11+
} from '../errors';
712

813
class CommandMkdir extends CommandPolykey {
914
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
1015
super(...args);
1116
this.name('mkdir');
12-
this.description('Create a Directory within a Vault');
17+
this.description(
18+
'Create a Directory within a Vault. Empty directories are not a part of the vault and will not be shared when cloning a Vault.',
19+
);
1320
this.argument(
14-
'<secretPath>',
21+
'<secretPath...>',
1522
'Path to where the directory to be created, specified as <vaultName>:<directoryPath>',
16-
binParsers.parseSecretPathValue,
1723
);
18-
this.option('-r, --recursive', 'Create the directory recursively');
1924
this.addOption(binOptions.nodeId);
2025
this.addOption(binOptions.clientHost);
2126
this.addOption(binOptions.clientPort);
22-
this.action(async (secretPath, options) => {
27+
this.addOption(binOptions.recursive);
28+
this.action(async (secretPaths, options) => {
29+
secretPaths = secretPaths.map((path: string) =>
30+
binParsers.parseSecretPath(path),
31+
);
2332
const { default: PolykeyClient } = await import(
2433
'polykey/dist/PolykeyClient'
2534
);
@@ -50,16 +59,56 @@ class CommandMkdir extends CommandPolykey {
5059
},
5160
logger: this.logger.getChild(PolykeyClient.name),
5261
});
53-
await binUtils.retryAuthentication(
54-
(auth) =>
55-
pkClient.rpcClient.methods.vaultsSecretsMkdir({
56-
metadata: auth,
57-
nameOrId: secretPath[0],
58-
dirName: secretPath[1],
59-
recursive: options.recursive,
60-
}),
61-
meta,
62-
);
62+
const response = await binUtils.retryAuthentication(async (auth) => {
63+
const response =
64+
await pkClient.rpcClient.methods.vaultsSecretsMkdir();
65+
const writer = response.writable.getWriter();
66+
let first = true;
67+
for (const [vault, path] of secretPaths) {
68+
await writer.write({
69+
nameOrId: vault,
70+
dirName: path,
71+
metadata: first
72+
? { ...auth, options: { recursive: options.recursive } }
73+
: undefined,
74+
});
75+
first = false;
76+
}
77+
await writer.close();
78+
return response;
79+
}, meta);
80+
81+
let hasErrored = false;
82+
for await (const result of response.readable) {
83+
if (result.type === 'error') {
84+
// TS cannot properly evaluate a type this deeply nested, so we use
85+
// the as keyword to help it. Inside this block, the type of data is
86+
// ensured to be 'error'.
87+
const error = result as ErrorMessage;
88+
hasErrored = true;
89+
let message: string = '';
90+
switch (error.code) {
91+
case 'ENOENT':
92+
message = 'No such secret or directory';
93+
break;
94+
case 'EEXIST':
95+
message = 'Secret or directory exists';
96+
break;
97+
default:
98+
throw new ErrorPolykeyCLIUncaughtException(
99+
`Unexpected error code: ${error.code}`,
100+
);
101+
}
102+
process.stderr.write(
103+
`${error.code}: cannot create directory ${error.reason}: ${message}\n`,
104+
);
105+
}
106+
}
107+
if (hasErrored) {
108+
throw new ErrorPolykeyCLIMakeDirectory(
109+
'Failed to create one or more directories',
110+
);
111+
}
63112
} finally {
64113
if (pkClient! != null) await pkClient.stop();
65114
}

tests/secrets/mkdir.test.ts

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import type { VaultName } from 'polykey/dist/vaults/types';
2+
import path from 'path';
3+
import fs from 'fs';
4+
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
5+
import PolykeyAgent from 'polykey/dist/PolykeyAgent';
6+
import { vaultOps } from 'polykey/dist/vaults';
7+
import * as keysUtils from 'polykey/dist/keys/utils';
8+
import * as testUtils from '../utils';
9+
10+
describe('commandMkdir', () => {
11+
const password = 'password';
12+
const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]);
13+
let dataDir: string;
14+
let polykeyAgent: PolykeyAgent;
15+
let command: Array<string>;
16+
17+
beforeEach(async () => {
18+
dataDir = await fs.promises.mkdtemp(
19+
path.join(globalThis.tmpDir, 'polykey-test-'),
20+
);
21+
polykeyAgent = await PolykeyAgent.createPolykeyAgent({
22+
password,
23+
options: {
24+
nodePath: dataDir,
25+
agentServiceHost: '127.0.0.1',
26+
clientServiceHost: '127.0.0.1',
27+
keys: {
28+
passwordOpsLimit: keysUtils.passwordOpsLimits.min,
29+
passwordMemLimit: keysUtils.passwordMemLimits.min,
30+
strictMemoryLock: false,
31+
},
32+
},
33+
logger: logger,
34+
});
35+
});
36+
afterEach(async () => {
37+
await polykeyAgent.stop();
38+
await fs.promises.rm(dataDir, {
39+
force: true,
40+
recursive: true,
41+
});
42+
});
43+
44+
test('should make a directory', async () => {
45+
const vaultName = 'vault' as VaultName;
46+
const dirName = 'dir';
47+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
48+
command = ['secrets', 'mkdir', '-np', dataDir, `${vaultName}:${dirName}`];
49+
const result = await testUtils.pkStdio([...command], {
50+
env: { PK_PASSWORD: password },
51+
cwd: dataDir,
52+
});
53+
expect(result.exitCode).toBe(0);
54+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
55+
const stat = await vaultOps.statSecret(vault, dirName);
56+
expect(stat.isDirectory()).toBeTruthy();
57+
});
58+
});
59+
test('should make directories recursively', async () => {
60+
const vaultName = 'vault' as VaultName;
61+
const dirName1 = 'dir1';
62+
const dirName2 = 'dir2';
63+
const dirNameNested = path.join(dirName1, dirName2);
64+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
65+
command = [
66+
'secrets',
67+
'mkdir',
68+
'-np',
69+
dataDir,
70+
`${vaultName}:${dirNameNested}`,
71+
'--recursive',
72+
];
73+
const result = await testUtils.pkStdio([...command], {
74+
env: { PK_PASSWORD: password },
75+
cwd: dataDir,
76+
});
77+
expect(result.exitCode).toBe(0);
78+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
79+
const stat1 = await vaultOps.statSecret(vault, dirName1);
80+
expect(stat1.isDirectory()).toBeTruthy();
81+
const stat2 = await vaultOps.statSecret(vault, dirNameNested);
82+
expect(stat2.isDirectory()).toBeTruthy();
83+
});
84+
});
85+
test('should fail without recursive set', async () => {
86+
const vaultName = 'vault' as VaultName;
87+
const dirName1 = 'dir1';
88+
const dirName2 = 'dir2';
89+
const dirNameNested = path.join(dirName1, dirName2);
90+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
91+
command = [
92+
'secrets',
93+
'mkdir',
94+
'-np',
95+
dataDir,
96+
`${vaultName}:${dirNameNested}`,
97+
];
98+
const result = await testUtils.pkStdio([...command], {
99+
env: { PK_PASSWORD: password },
100+
cwd: dataDir,
101+
});
102+
expect(result.exitCode).toBe(1);
103+
expect(result.stderr).toInclude('ENOENT');
104+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
105+
await vault.readF(async (efs) => {
106+
const dirName1P = efs.readdir(dirName1);
107+
await expect(dirName1P).rejects.toThrow('ENOENT');
108+
const dirNameNestedP = efs.readdir(dirNameNested);
109+
await expect(dirNameNestedP).rejects.toThrow('ENOENT');
110+
});
111+
});
112+
});
113+
test('should fail to make existing directory', async () => {
114+
const vaultName = 'vault' as VaultName;
115+
const dirName = 'dir-exists';
116+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
117+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
118+
await vault.writeF(async (efs) => {
119+
await efs.mkdir(dirName);
120+
});
121+
});
122+
command = ['secrets', 'mkdir', '-np', dataDir, `${vaultName}:${dirName}`];
123+
const result = await testUtils.pkStdio([...command], {
124+
env: { PK_PASSWORD: password },
125+
cwd: dataDir,
126+
});
127+
expect(result.exitCode).toBe(1);
128+
expect(result.stderr).toInclude('EEXIST');
129+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
130+
await vault.readF(async (efs) => {
131+
const dirP = efs.readdir(dirName);
132+
await expect(dirP).toResolve();
133+
});
134+
});
135+
});
136+
test('should fail to make existing secret', async () => {
137+
const vaultName = 'vault' as VaultName;
138+
const secretName = 'secret-exists';
139+
const secretContent = 'secret-content';
140+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
141+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
142+
await vault.writeF(async (efs) => {
143+
await efs.writeFile(secretName, secretContent);
144+
});
145+
});
146+
command = [
147+
'secrets',
148+
'mkdir',
149+
'-np',
150+
dataDir,
151+
`${vaultName}:${secretName}`,
152+
];
153+
const result = await testUtils.pkStdio([...command], {
154+
env: { PK_PASSWORD: password },
155+
cwd: dataDir,
156+
});
157+
expect(result.exitCode).toBe(1);
158+
expect(result.stderr).toInclude('EEXIST');
159+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
160+
await vault.readF(async (efs) => {
161+
const stat = await efs.stat(secretName);
162+
expect(stat.isFile()).toBeTruthy();
163+
const contents = await efs.readFile(secretName);
164+
expect(contents.toString()).toEqual(secretContent);
165+
});
166+
});
167+
});
168+
test('should make directories in multiple vaults', async () => {
169+
const vaultName1 = 'vault1' as VaultName;
170+
const vaultName2 = 'vault2' as VaultName;
171+
const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1);
172+
const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2);
173+
const dirName1 = 'dir1';
174+
const dirName2 = 'dir2';
175+
const dirName3 = 'dir3';
176+
command = [
177+
'secrets',
178+
'mkdir',
179+
'-np',
180+
dataDir,
181+
`${vaultName1}:${dirName1}`,
182+
`${vaultName2}:${dirName2}`,
183+
`${vaultName1}:${dirName3}`,
184+
];
185+
const result = await testUtils.pkStdio([...command], {
186+
env: { PK_PASSWORD: password },
187+
cwd: dataDir,
188+
});
189+
expect(result.exitCode).toBe(0);
190+
await polykeyAgent.vaultManager.withVaults(
191+
[vaultId1, vaultId2],
192+
async (vault1, vault2) => {
193+
const stat1 = await vaultOps.statSecret(vault1, dirName1);
194+
expect(stat1.isDirectory()).toBeTruthy();
195+
const stat2 = await vaultOps.statSecret(vault2, dirName2);
196+
expect(stat2.isDirectory()).toBeTruthy();
197+
const stat3 = await vaultOps.statSecret(vault1, dirName3);
198+
expect(stat3.isDirectory()).toBeTruthy();
199+
},
200+
);
201+
});
202+
test('should continue after error', async () => {
203+
const vaultName1 = 'vault1' as VaultName;
204+
const vaultName2 = 'vault2' as VaultName;
205+
const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1);
206+
const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2);
207+
const dirName1 = 'dir1';
208+
const dirName2 = 'nodir/dir2';
209+
const dirName3 = 'dir3';
210+
const dirName4 = 'dir4';
211+
command = [
212+
'secrets',
213+
'mkdir',
214+
'-np',
215+
dataDir,
216+
`${vaultName1}:${dirName1}`,
217+
`${vaultName2}:${dirName2}`,
218+
`${vaultName2}:${dirName3}`,
219+
`${vaultName1}:${dirName4}`,
220+
];
221+
const result = await testUtils.pkStdio([...command], {
222+
env: { PK_PASSWORD: password },
223+
cwd: dataDir,
224+
});
225+
expect(result.exitCode).not.toBe(0);
226+
expect(result.stderr).toInclude('ENOENT');
227+
await polykeyAgent.vaultManager.withVaults(
228+
[vaultId1, vaultId2],
229+
async (vault1, vault2) => {
230+
const stat1 = await vaultOps.statSecret(vault1, dirName1);
231+
expect(stat1.isDirectory()).toBeTruthy();
232+
await expect(vaultOps.statSecret(vault2, dirName2)).toReject();
233+
const stat3 = await vaultOps.statSecret(vault2, dirName3);
234+
expect(stat3.isDirectory()).toBeTruthy();
235+
const stat4 = await vaultOps.statSecret(vault1, dirName4);
236+
expect(stat4.isDirectory()).toBeTruthy();
237+
},
238+
);
239+
});
240+
});

0 commit comments

Comments
 (0)