diff --git a/src/client/errors.ts b/src/client/errors.ts index 804a351d9..5db96fc8c 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -18,6 +18,11 @@ class ErrorClientAuthDenied extends ErrorClient { exitCode = sysexits.NOPERM; } +class ErrorClientInvalidHeader extends ErrorClient { + static description = 'The header message does not match the expected type' + exitCode = sysexits.USAGE; +} + class ErrorClientService extends ErrorClient {} class ErrorClientServiceRunning extends ErrorClientService { @@ -45,6 +50,7 @@ export { ErrorClientAuthMissing, ErrorClientAuthFormat, ErrorClientAuthDenied, + ErrorClientInvalidHeader, ErrorClientService, ErrorClientServiceRunning, ErrorClientServiceNotRunning, diff --git a/src/client/handlers/VaultsSecretsRemove.ts b/src/client/handlers/VaultsSecretsRemove.ts index 2f8a5cbba..1115f62e2 100644 --- a/src/client/handlers/VaultsSecretsRemove.ts +++ b/src/client/handlers/VaultsSecretsRemove.ts @@ -1,103 +1,204 @@ import type { DB } from '@matrixai/db'; +import { ResourceAcquire, withG } from '@matrixai/resources'; import type { ClientRPCRequestParams, ClientRPCResponseResult, - SecretIdentifierMessage, + SecretsRemoveHeaderMessage, + SecretIdentifierMessageTagged, SuccessOrErrorMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; import { DuplexHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; +import * as clientErrors from '../errors'; +import { FileSystemWritable } from '../../vaults/types'; +import * as utils from '../../utils'; class VaultsSecretsRemove extends DuplexHandler< { db: DB; vaultManager: VaultManager; }, - ClientRPCRequestParams, + ClientRPCRequestParams< + SecretsRemoveHeaderMessage | SecretIdentifierMessageTagged + >, ClientRPCResponseResult > { public handle = async function* ( - input: AsyncIterable>, + input: AsyncIterable< + ClientRPCRequestParams< + SecretsRemoveHeaderMessage | SecretIdentifierMessageTagged + > + >, ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; - // Create a record of secrets to be removed, grouped by vault names - const vaultGroups: Record> = {}; - const secretNames: Array<[string, string]> = []; - let metadata: any = undefined; - for await (const secretRemoveMessage of input) { - if (metadata == null) metadata = secretRemoveMessage.metadata ?? {}; - secretNames.push([ - secretRemoveMessage.nameOrId, - secretRemoveMessage.secretName, - ]); - } - secretNames.forEach(([vaultName, secretName]) => { - if (vaultGroups[vaultName] == null) { - vaultGroups[vaultName] = []; + const vaultAcquires: Array> = []; + // Extracts the header message from the iterator + const headerMessage = await (async () => { + let header: SecretsRemoveHeaderMessage | undefined; + for await (const value of input) { + // TS cannot properly narrow down this type as it is too deeply wrapped. + // The as keyword is used to help it with type narrowing. + const message = value as + | SecretIdentifierMessageTagged + | SecretsRemoveHeaderMessage; + if (message.type === 'VaultNamesHeaderMesage') header = message; + break; } - vaultGroups[vaultName].push(secretName); - }); - // Now, all the paths will be removed for a vault within a single commit - yield* db.withTransactionG( - async function* (tran): AsyncGenerator { - for (const [vaultName, secretNames] of Object.entries(vaultGroups)) { - const vaultIdFromName = await vaultManager.getVaultId( - vaultName, - tran, + // The header message is mandatory + if (header == null) throw new clientErrors.ErrorClientInvalidHeader(); + return header; + })(); + // Create an array of write acquires + await db.withTransactionF(async (tran) => { + for (const vaultName of headerMessage!.vaultNames) { + 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`, ); - const vaultId = - vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName); - if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); - } - yield* vaultManager.withVaultsG( - [vaultId], - async function* (vault): AsyncGenerator { - yield* vault.writeG( - async function* (efs): AsyncGenerator { - for (const secretName of secretNames) { - try { - const stat = await efs.stat(secretName); - if (stat.isDirectory()) { - await efs.rmdir(secretName, { - recursive: metadata?.options?.recursive, - }); - } else { - await efs.unlink(secretName); - } - yield { - type: 'success', - success: true, - }; - } catch (e) { - if ( - e.code === 'ENOENT' || - e.code === 'ENOTEMPTY' || - e.code === 'EINVAL' - ) { - // INVAL can be triggered if removing the root of the - // vault is attempted. - yield { - type: 'error', - code: e.code, - reason: secretName, - }; - } else { - throw e; - } - } - } - }, + } + await vaultManager.withVaults([vaultId], async (vault) => { + vaultAcquires.push(vault.acquireWrite()); + }); + } + }); + // Acquire all locks in parallel and perform all operations at once + yield* withG( + vaultAcquires, + async function* (efses): AsyncGenerator { + // Creating the vault name to efs map for easy access + const vaultMap = new Map(); + for (let i = 0; i < efses.length; i++) { + vaultMap.set(headerMessage!.vaultNames[i], efses[i]); + } + for await (const value of input) { + // TS cannot properly narrow down this type as it is too deeply wrapped. + // The as keyword is used to help it with type narrowing. + const message = value as + | SecretIdentifierMessageTagged + | SecretsRemoveHeaderMessage; + // Ignoring any header messages + if (message.type === 'SecretIdentifierMessage') { + const efs = vaultMap.get(message.nameOrId); + if (efs == null) { + throw new vaultsErrors.ErrorVaultsVaultUndefined( + `Vault ${message.nameOrId} was not present in the header message`, ); - }, - tran, - ); + } + try { + const stat = await efs.stat(message.secretName); + if (stat.isDirectory()) { + await efs.rmdir(message.secretName, { + recursive: headerMessage.recursive, + }); + } else { + await efs.unlink(message.secretName); + } + yield { + type: 'success', + success: true, + }; + } catch (e) { + if ( + e.code === 'ENOENT' || + e.code === 'ENOTEMPTY' || + e.code === 'EINVAL' + ) { + // INVAL can be triggered if removing the root of the + // vault is attempted. + yield { + type: 'error', + code: e.code, + reason: message.secretName, + }; + } else { + throw e; + } + } + } } }, ); + + // Create a record of secrets to be removed, grouped by vault names + // const vaultGroups: Record> = {}; + // const secretNames: Array<[string, string]> = []; + // let metadata: any = undefined; + // for await (const secretRemoveMessage of input) { + // if (metadata == null) metadata = secretRemoveMessage.metadata ?? {}; + // secretNames.push([ + // secretRemoveMessage.nameOrId, + // secretRemoveMessage.secretName, + // ]); + // } + // secretNames.forEach(([vaultName, secretName]) => { + // if (vaultGroups[vaultName] == null) { + // vaultGroups[vaultName] = []; + // } + // vaultGroups[vaultName].push(secretName); + // }); + // Now, all the paths will be removed for a vault within a single commit + // yield* db.withTransactionG( + // async function* (tran): AsyncGenerator { + // for (const [vaultName, secretNames] of Object.entries(vaultGroups)) { + // const vaultIdFromName = await vaultManager.getVaultId( + // vaultName, + // tran, + // ); + // const vaultId = + // vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName); + // if (vaultId == null) { + // throw new vaultsErrors.ErrorVaultsVaultUndefined(); + // } + // yield* vaultManager.withVaultsG( + // [vaultId], + // async function* (vault): AsyncGenerator { + // yield* vault.writeG( + // async function* (efs): AsyncGenerator { + // for (const secretName of secretNames) { + // try { + // const stat = await efs.stat(secretName); + // if (stat.isDirectory()) { + // await efs.rmdir(secretName, { + // recursive: metadata?.options?.recursive, + // }); + // } else { + // await efs.unlink(secretName); + // } + // yield { + // type: 'success', + // success: true, + // }; + // } catch (e) { + // if ( + // e.code === 'ENOENT' || + // e.code === 'ENOTEMPTY' || + // e.code === 'EINVAL' + // ) { + // // INVAL can be triggered if removing the root of the + // // vault is attempted. + // yield { + // type: 'error', + // code: e.code, + // reason: secretName, + // }; + // } else { + // throw e; + // } + // } + // } + // }, + // ); + // }, + // tran, + // ); + // } + // }, + // ); }; } diff --git a/src/client/types.ts b/src/client/types.ts index ae415bb1e..de48d9c83 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -360,6 +360,19 @@ type SecretStatMessage = { }; }; +type SecretIdentifierMessageTagged = SecretIdentifierMessage & { + type: 'SecretIdentifierMessage'; +} + +type VaultNamesHeaderMesage = { + type: 'VaultNamesHeaderMesage'; + vaultNames: Array; +}; + +type SecretsRemoveHeaderMessage = VaultNamesHeaderMesage & { + recursive?: boolean; +}; + // Type casting for tricky handlers type OverrideRPClientType> = Omit< @@ -435,6 +448,9 @@ export type { SecretRenameMessage, SecretFilesMessage, SecretStatMessage, + SecretIdentifierMessageTagged, + VaultNamesHeaderMesage, + SecretsRemoveHeaderMessage, SignatureMessage, OverrideRPClientType, AuditMetricGetTypeOverride, diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index 1f1b85713..9889cfc2f 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -3,10 +3,13 @@ import type { FileSystem } from '@/types'; import type { VaultId } from '@/ids'; import type NodeManager from '@/nodes/NodeManager'; import type { + ClientRPCRequestParams, ContentSuccessMessage, ErrorMessage, LogEntryMessage, SecretContentMessage, + SecretIdentifierMessage, + SecretsRemoveHeaderMessage, VaultListMessage, VaultPermissionMessage, } from '@/client/types'; @@ -2371,7 +2374,19 @@ describe('vaultsSecretsRemove', () => { // Write paths const response = await rpcClient.methods.vaultsSecretsRemove(); const writer = response.writable.getWriter(); - await writer.write({ nameOrId: 'invalid', secretName: 'invalid' }); + let variable: ClientRPCRequestParams = {type: 'VaultNamesHeaderMesage', vaultNames: ['invalid']}; + console.log(variable); + // Header message + await writer.write({ + type: 'VaultNamesHeaderMesage', + vaultNames: ['invalid'], + }); + // Content messages + await writer.write({ + type: 'SecretIdentifierMessage', + nameOrId: 'invalid', + secretName: 'invalid', + }); await writer.close(); // Read response const consumeP = async () => {