Skip to content

Commit

Permalink
Merge pull request #820 from MatrixAI/feature-unix-touch
Browse files Browse the repository at this point in the history
Adding RPC handler to `touch` files
  • Loading branch information
aryanjassal authored Feb 4, 2025
2 parents 98d8231 + fce8dbe commit 7d78a29
Show file tree
Hide file tree
Showing 9 changed files with 1,012 additions and 5 deletions.
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

0 comments on commit 7d78a29

Please sign in to comment.