Skip to content

Commit 7d78a29

Browse files
authored
Merge pull request #820 from MatrixAI/feature-unix-touch
Adding RPC handler to `touch` files
2 parents 98d8231 + fce8dbe commit 7d78a29

File tree

9 files changed

+1012
-5
lines changed

9 files changed

+1012
-5
lines changed

src/client/callers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import vaultsSecretsNewDir from './vaultsSecretsNewDir';
7171
import vaultsSecretsRename from './vaultsSecretsRename';
7272
import vaultsSecretsRemove from './vaultsSecretsRemove';
7373
import vaultsSecretsStat from './vaultsSecretsStat';
74+
import vaultsSecretsTouch from './vaultsSecretsTouch';
7475
import vaultsSecretsWriteFile from './vaultsSecretsWriteFile';
7576
import vaultsVersion from './vaultsVersion';
7677

@@ -151,6 +152,7 @@ const clientManifest = {
151152
vaultsSecretsRename,
152153
vaultsSecretsRemove,
153154
vaultsSecretsStat,
155+
vaultsSecretsTouch,
154156
vaultsSecretsWriteFile,
155157
vaultsVersion,
156158
};
@@ -230,6 +232,7 @@ export {
230232
vaultsSecretsRename,
231233
vaultsSecretsRemove,
232234
vaultsSecretsStat,
235+
vaultsSecretsTouch,
233236
vaultsSecretsWriteFile,
234237
vaultsVersion,
235238
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { HandlerTypes } from '@matrixai/rpc';
2+
import type VaultsSecretsTouch from '../handlers/VaultsSecretsTouch';
3+
import { DuplexCaller } from '@matrixai/rpc';
4+
5+
type CallerTypes = HandlerTypes<VaultsSecretsTouch>;
6+
7+
const vaultsSecretsTouch = new DuplexCaller<
8+
CallerTypes['input'],
9+
CallerTypes['output']
10+
>();
11+
12+
export default vaultsSecretsTouch;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { ContextTimed } from '@matrixai/contexts';
2+
import type { DB } from '@matrixai/db';
3+
import type { ResourceAcquire } from '@matrixai/resources';
4+
import type { JSONValue } from '@matrixai/rpc';
5+
import type {
6+
ClientRPCRequestParams,
7+
ClientRPCResponseResult,
8+
SecretIdentifierMessageTagged,
9+
SuccessOrErrorMessageTagged,
10+
VaultNamesHeaderMessageTagged,
11+
} from '../types';
12+
import type VaultManager from '../../vaults/VaultManager';
13+
import type { FileSystemWritable } from '../../vaults/types';
14+
import { withG } from '@matrixai/resources';
15+
import { DuplexHandler } from '@matrixai/rpc';
16+
import * as vaultsUtils from '../../vaults/utils';
17+
import * as vaultsErrors from '../../vaults/errors';
18+
import * as clientErrors from '../errors';
19+
20+
class VaultsSecretsTouch extends DuplexHandler<
21+
{
22+
db: DB;
23+
vaultManager: VaultManager;
24+
},
25+
ClientRPCRequestParams<
26+
VaultNamesHeaderMessageTagged | SecretIdentifierMessageTagged
27+
>,
28+
ClientRPCResponseResult<SuccessOrErrorMessageTagged>
29+
> {
30+
public handle = async function* (
31+
input: AsyncIterableIterator<
32+
ClientRPCRequestParams<
33+
VaultNamesHeaderMessageTagged | SecretIdentifierMessageTagged
34+
>
35+
>,
36+
_cancel: (reason?: any) => void,
37+
_meta: Record<string, JSONValue>,
38+
ctx: ContextTimed,
39+
): AsyncGenerator<ClientRPCResponseResult<SuccessOrErrorMessageTagged>> {
40+
const { db, vaultManager }: { db: DB; vaultManager: VaultManager } =
41+
this.container;
42+
// Extract the header message from the iterator
43+
const headerMessagePair = await input.next();
44+
const headerMessage:
45+
| VaultNamesHeaderMessageTagged
46+
| SecretIdentifierMessageTagged = headerMessagePair.value;
47+
// Testing if the header is of the expected format
48+
if (
49+
headerMessagePair.done ||
50+
headerMessage.type !== 'VaultNamesHeaderMessage'
51+
) {
52+
throw new clientErrors.ErrorClientInvalidHeader();
53+
}
54+
// Create an array of write acquires
55+
const vaultAcquires = await db.withTransactionF(async (tran) => {
56+
const vaultAcquires: Array<ResourceAcquire<FileSystemWritable>> = [];
57+
for (const vaultName of headerMessage.vaultNames) {
58+
ctx.signal.throwIfAborted();
59+
const vaultIdFromName = await vaultManager.getVaultId(vaultName, tran);
60+
const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName);
61+
if (vaultId == null) {
62+
throw new vaultsErrors.ErrorVaultsVaultUndefined(
63+
`Vault "${vaultName}" does not exist`,
64+
);
65+
}
66+
// The resource acquisition will automatically create a transaction and
67+
// release it when cleaning up.
68+
const acquire = await vaultManager.withVaults(
69+
[vaultId],
70+
async (vault) => vault.acquireWrite(undefined, ctx),
71+
);
72+
vaultAcquires.push(acquire);
73+
}
74+
return vaultAcquires;
75+
});
76+
// Acquire all locks in parallel and perform all operations at once
77+
yield* withG(
78+
vaultAcquires,
79+
async function* (efses): AsyncGenerator<SuccessOrErrorMessageTagged> {
80+
// Creating the vault name to efs map for easy access
81+
const vaultMap = new Map<string, FileSystemWritable>();
82+
for (let i = 0; i < efses.length; i++) {
83+
vaultMap.set(headerMessage!.vaultNames[i], efses[i]);
84+
}
85+
let loopRan = false;
86+
for await (const message of input) {
87+
ctx.signal.throwIfAborted();
88+
loopRan = true;
89+
// Header messages should not be seen anymore
90+
if (message.type === 'VaultNamesHeaderMessage') {
91+
throw new clientErrors.ErrorClientProtocolError(
92+
'The header message cannot be sent multiple times',
93+
);
94+
}
95+
const efs = vaultMap.get(message.nameOrId);
96+
if (efs == null) {
97+
throw new vaultsErrors.ErrorVaultsVaultUndefined(
98+
`Vault ${message.nameOrId} was not present in the header message`,
99+
);
100+
}
101+
try {
102+
// If the file exists, update its timestamps. Otherwise, create the
103+
// file. Note that this can throw errors, which are handled later.
104+
if (await efs.exists(message.secretName)) {
105+
const now = new Date();
106+
await efs.utimes(message.secretName, now, now);
107+
} else {
108+
await efs.writeFile(message.secretName);
109+
}
110+
yield {
111+
type: 'SuccessMessage',
112+
success: true,
113+
};
114+
} catch (e) {
115+
switch (e.code) {
116+
case 'ENOENT':
117+
yield {
118+
type: 'ErrorMessage',
119+
code: e.code,
120+
reason: message.secretName,
121+
};
122+
break;
123+
default:
124+
throw e;
125+
}
126+
}
127+
}
128+
// Content messages must follow header messages
129+
if (!loopRan) {
130+
throw new clientErrors.ErrorClientProtocolError(
131+
'No content messages followed header message',
132+
);
133+
}
134+
},
135+
);
136+
};
137+
}
138+
139+
export default VaultsSecretsTouch;

src/client/handlers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import VaultsSecretsNewDir from './VaultsSecretsNewDir';
8888
import VaultsSecretsRename from './VaultsSecretsRename';
8989
import VaultsSecretsRemove from './VaultsSecretsRemove';
9090
import VaultsSecretsStat from './VaultsSecretsStat';
91+
import VaultsSecretsTouch from './VaultsSecretsTouch';
9192
import VaultsSecretsWriteFile from './VaultsSecretsWriteFile';
9293
import VaultsVersion from './VaultsVersion';
9394

@@ -191,6 +192,7 @@ const serverManifest = (container: {
191192
vaultsSecretsRename: new VaultsSecretsRename(container),
192193
vaultsSecretsRemove: new VaultsSecretsRemove(container),
193194
vaultsSecretsStat: new VaultsSecretsStat(container),
195+
vaultsSecretsTouch: new VaultsSecretsTouch(container),
194196
vaultsSecretsWriteFile: new VaultsSecretsWriteFile(container),
195197
vaultsVersion: new VaultsVersion(container),
196198
};
@@ -272,6 +274,7 @@ export {
272274
VaultsSecretsRename,
273275
VaultsSecretsRemove,
274276
VaultsSecretsStat,
277+
VaultsSecretsTouch,
275278
VaultsSecretsWriteFile,
276279
VaultsVersion,
277280
};

src/vaults/VaultOps.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,43 @@ async function writeSecret(
299299
}
300300
}
301301

302+
/**
303+
* Performs a touch operation on a secret by updating all it's timestamps to the
304+
* current time.
305+
*/
306+
async function touchSecret(
307+
vault: Vault,
308+
secretName: string,
309+
ctx?: ContextTimed,
310+
): Promise<void> {
311+
const now = new Date();
312+
try {
313+
await vault.writeF(
314+
async (efs) => {
315+
// If the file exists, update its timestamps. Otherwise, create the
316+
// file. Note that this can throw errors, which are handled later.
317+
if (await efs.exists(secretName)) {
318+
await efs.utimes(secretName, now, now);
319+
} else {
320+
await efs.writeFile(secretName);
321+
}
322+
},
323+
undefined,
324+
ctx,
325+
);
326+
} catch (e) {
327+
switch (e.code) {
328+
case 'ENOENT':
329+
throw new vaultsErrors.ErrorSecretsSecretUndefined(
330+
`One or more parent directories for '${secretName}' do not exist`,
331+
{ cause: e },
332+
);
333+
default:
334+
throw e;
335+
}
336+
}
337+
}
338+
302339
export {
303340
addSecret,
304341
renameSecret,
@@ -309,4 +346,5 @@ export {
309346
addSecretDirectory,
310347
listSecrets,
311348
writeSecret,
349+
touchSecret,
312350
};

0 commit comments

Comments
 (0)