Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding RPC handler to touch files #820

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/client/callers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import vaultsSecretsNewDir from './vaultsSecretsNewDir';
import vaultsSecretsRename from './vaultsSecretsRename';
import vaultsSecretsRemove from './vaultsSecretsRemove';
import vaultsSecretsStat from './vaultsSecretsStat';
import vaultsSecretsTouch from './vaultsSecretsTouch';
import vaultsSecretsWriteFile from './vaultsSecretsWriteFile';
import vaultsVersion from './vaultsVersion';

Expand Down Expand Up @@ -151,6 +152,7 @@ const clientManifest = {
vaultsSecretsRename,
vaultsSecretsRemove,
vaultsSecretsStat,
vaultsSecretsTouch,
vaultsSecretsWriteFile,
vaultsVersion,
};
Expand Down Expand Up @@ -230,6 +232,7 @@ export {
vaultsSecretsRename,
vaultsSecretsRemove,
vaultsSecretsStat,
vaultsSecretsTouch,
vaultsSecretsWriteFile,
vaultsVersion,
};
12 changes: 12 additions & 0 deletions src/client/callers/vaultsSecretsTouch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HandlerTypes } from '@matrixai/rpc';
import type VaultsSecretsTouch from '../handlers/VaultsSecretsTouch';
import { DuplexCaller } from '@matrixai/rpc';

type CallerTypes = HandlerTypes<VaultsSecretsTouch>;

const vaultsSecretsTouch = new DuplexCaller<
CallerTypes['input'],
CallerTypes['output']
>();

export default vaultsSecretsTouch;
139 changes: 139 additions & 0 deletions src/client/handlers/VaultsSecretsTouch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { ContextTimed } from '@matrixai/contexts';
import type { DB } from '@matrixai/db';
import type { ResourceAcquire } from '@matrixai/resources';
import type { JSONValue } from '@matrixai/rpc';
import type {
ClientRPCRequestParams,
ClientRPCResponseResult,
SecretIdentifierMessageTagged,
SuccessOrErrorMessageTagged,
VaultNamesHeaderMessageTagged,
} from '../types';
import type VaultManager from '../../vaults/VaultManager';
import type { FileSystemWritable } from '../../vaults/types';
import { withG } from '@matrixai/resources';
import { DuplexHandler } from '@matrixai/rpc';
import * as vaultsUtils from '../../vaults/utils';
import * as vaultsErrors from '../../vaults/errors';
import * as clientErrors from '../errors';

class VaultsSecretsTouch extends DuplexHandler<
{
db: DB;
vaultManager: VaultManager;
},
ClientRPCRequestParams<
VaultNamesHeaderMessageTagged | SecretIdentifierMessageTagged
>,
ClientRPCResponseResult<SuccessOrErrorMessageTagged>
> {
public handle = async function* (
input: AsyncIterableIterator<
ClientRPCRequestParams<
VaultNamesHeaderMessageTagged | SecretIdentifierMessageTagged
>
>,
_cancel: (reason?: any) => void,
_meta: Record<string, JSONValue>,
ctx: ContextTimed,
): AsyncGenerator<ClientRPCResponseResult<SuccessOrErrorMessageTagged>> {
const { db, vaultManager }: { db: DB; vaultManager: VaultManager } =
this.container;
// Extract the header message from the iterator
const headerMessagePair = await input.next();
const headerMessage:
| VaultNamesHeaderMessageTagged
| SecretIdentifierMessageTagged = headerMessagePair.value;
// Testing if the header is of the expected format
if (
headerMessagePair.done ||
headerMessage.type !== 'VaultNamesHeaderMessage'
) {
throw new clientErrors.ErrorClientInvalidHeader();
}
// Create an array of write acquires
const vaultAcquires = await db.withTransactionF(async (tran) => {
const vaultAcquires: Array<ResourceAcquire<FileSystemWritable>> = [];
for (const vaultName of headerMessage.vaultNames) {
ctx.signal.throwIfAborted();
const vaultIdFromName = await vaultManager.getVaultId(vaultName, tran);
const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName);
if (vaultId == null) {
throw new vaultsErrors.ErrorVaultsVaultUndefined(
`Vault "${vaultName}" does not exist`,
);
}
// The resource acquisition will automatically create a transaction and
// release it when cleaning up.
const acquire = await vaultManager.withVaults(
[vaultId],
async (vault) => vault.acquireWrite(undefined, ctx),
);
vaultAcquires.push(acquire);
}
return vaultAcquires;
});
// Acquire all locks in parallel and perform all operations at once
yield* withG(
vaultAcquires,
async function* (efses): AsyncGenerator<SuccessOrErrorMessageTagged> {
// Creating the vault name to efs map for easy access
const vaultMap = new Map<string, FileSystemWritable>();
for (let i = 0; i < efses.length; i++) {
vaultMap.set(headerMessage!.vaultNames[i], efses[i]);
}
let loopRan = false;
for await (const message of input) {
ctx.signal.throwIfAborted();
loopRan = true;
// Header messages should not be seen anymore
if (message.type === 'VaultNamesHeaderMessage') {
throw new clientErrors.ErrorClientProtocolError(
'The header message cannot be sent multiple times',
);
}
const efs = vaultMap.get(message.nameOrId);
if (efs == null) {
throw new vaultsErrors.ErrorVaultsVaultUndefined(
`Vault ${message.nameOrId} was not present in the header message`,
);
}
try {
// If the file exists, update its timestamps. Otherwise, create the
// file. Note that this can throw errors, which are handled later.
if (await efs.exists(message.secretName)) {
const now = new Date();
await efs.utimes(message.secretName, now, now);
} else {
await efs.writeFile(message.secretName);
}
yield {
type: 'SuccessMessage',
success: true,
};
} catch (e) {
switch (e.code) {
case 'ENOENT':
yield {
type: 'ErrorMessage',
code: e.code,
reason: message.secretName,
};
break;
default:
throw e;
}
}
}
// Content messages must follow header messages
if (!loopRan) {
throw new clientErrors.ErrorClientProtocolError(
'No content messages followed header message',
);
}
},
);
};
}

export default VaultsSecretsTouch;
3 changes: 3 additions & 0 deletions src/client/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import VaultsSecretsNewDir from './VaultsSecretsNewDir';
import VaultsSecretsRename from './VaultsSecretsRename';
import VaultsSecretsRemove from './VaultsSecretsRemove';
import VaultsSecretsStat from './VaultsSecretsStat';
import VaultsSecretsTouch from './VaultsSecretsTouch';
import VaultsSecretsWriteFile from './VaultsSecretsWriteFile';
import VaultsVersion from './VaultsVersion';

Expand Down Expand Up @@ -191,6 +192,7 @@ const serverManifest = (container: {
vaultsSecretsRename: new VaultsSecretsRename(container),
vaultsSecretsRemove: new VaultsSecretsRemove(container),
vaultsSecretsStat: new VaultsSecretsStat(container),
vaultsSecretsTouch: new VaultsSecretsTouch(container),
vaultsSecretsWriteFile: new VaultsSecretsWriteFile(container),
vaultsVersion: new VaultsVersion(container),
};
Expand Down Expand Up @@ -272,6 +274,7 @@ export {
VaultsSecretsRename,
VaultsSecretsRemove,
VaultsSecretsStat,
VaultsSecretsTouch,
VaultsSecretsWriteFile,
VaultsVersion,
};
38 changes: 38 additions & 0 deletions src/vaults/VaultOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,43 @@ async function writeSecret(
}
}

/**
* Performs a touch operation on a secret by updating all it's timestamps to the
* current time.
*/
async function touchSecret(
vault: Vault,
secretName: string,
ctx?: ContextTimed,
): Promise<void> {
const now = new Date();
try {
await vault.writeF(
async (efs) => {
// If the file exists, update its timestamps. Otherwise, create the
// file. Note that this can throw errors, which are handled later.
if (await efs.exists(secretName)) {
await efs.utimes(secretName, now, now);
} else {
await efs.writeFile(secretName);
}
},
undefined,
ctx,
);
} catch (e) {
switch (e.code) {
case 'ENOENT':
throw new vaultsErrors.ErrorSecretsSecretUndefined(
`One or more parent directories for '${secretName}' do not exist`,
{ cause: e },
);
default:
throw e;
}
}
}

export {
addSecret,
renameSecret,
Expand All @@ -309,4 +346,5 @@ export {
addSecretDirectory,
listSecrets,
writeSecret,
touchSecret,
};
Loading