diff --git a/src/client/errors.ts b/src/client/errors.ts index 804a351d9..edf76e6d0 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -18,6 +18,11 @@ class ErrorClientAuthDenied extends ErrorClient { exitCode = sysexits.NOPERM; } +class ErrorClientFSReadFailed extends ErrorClient { + static description = 'Failed to read from filesystem'; + exitCode = sysexits.IOERR; +} + class ErrorClientService extends ErrorClient {} class ErrorClientServiceRunning extends ErrorClientService { @@ -45,6 +50,7 @@ export { ErrorClientAuthMissing, ErrorClientAuthFormat, ErrorClientAuthDenied, + ErrorClientFSReadFailed, ErrorClientService, ErrorClientServiceRunning, ErrorClientServiceNotRunning, diff --git a/src/client/handlers/VaultsSecretsList.ts b/src/client/handlers/VaultsSecretsList.ts index c1e1dfbfa..2243d04ee 100644 --- a/src/client/handlers/VaultsSecretsList.ts +++ b/src/client/handlers/VaultsSecretsList.ts @@ -2,55 +2,65 @@ import type { DB } from '@matrixai/db'; import type { ClientRPCRequestParams, ClientRPCResponseResult, - SecretNameMessage, - VaultIdentifierMessage, + SecretFilesMessage, + SecretFiles, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; +import path from 'path'; import { ServerHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; -import * as vaultOps from '../../vaults/VaultOps'; +import * as clientErrors from '../errors'; class VaultsSecretsList extends ServerHandler< { vaultManager: VaultManager; db: DB; }, - ClientRPCRequestParams, - ClientRPCResponseResult + ClientRPCRequestParams, + ClientRPCResponseResult > { public async *handle( - input: ClientRPCRequestParams, - _cancel, - _meta, - ctx, - ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + input: ClientRPCRequestParams, + _cancel: any, + ): AsyncGenerator, void, void> { const { vaultManager, db } = this.container; - const secrets = await db.withTransactionF(async (tran) => { + const vaultId = await db.withTransactionF(async (tran) => { const vaultIdFromName = await vaultManager.getVaultId( input.nameOrId, tran, ); const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); - if (vaultId == null) { - throw new vaultsErrors.ErrorVaultsVaultUndefined(); - } - return await vaultManager.withVaults( - [vaultId], - async (vault) => { - return await vaultOps.listSecrets(vault); - }, - tran, - ); + if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + return vaultId; + }); + + yield* vaultManager.withVaultsG([vaultId], (vault) => { + return vault.readG(async function* (fs): AsyncGenerator< + SecretFiles, + void, + void + > { + let files: Array; + try { + // @ts-ignore: While the types don't fully match, it matches enough for our usage. + files = await fs.promises.readdir(input.path); + } catch (e) { + throw new clientErrors.ErrorClientFSReadFailed( + `No matches exist for path: ${input.path}`, + { cause: e }, + ); + } + files = files.map((file) => path.join(input.path, file)); + + for await (const file of files) { + const stat = await fs.promises.stat(file); + const type = stat.isFile() ? 'FILE' : 'DIRECTORY'; + yield { path: file, type: type }; + } + }); }); - for (const secret of secrets) { - if (ctx.signal.aborted) throw ctx.signal.reason; - yield { - secretName: secret, - }; - } } } diff --git a/src/client/types.ts b/src/client/types.ts index ee4055057..9a7faf3b4 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -300,6 +300,9 @@ type VaultsLatestVersionMessage = { }; // Secrets +type SecretFilesMessage = VaultIdentifierMessage & { + path: string; +}; type SecretNameMessage = { secretName: string; @@ -327,6 +330,11 @@ type SecretRenameMessage = SecretIdentifierMessage & { newSecretName: string; }; +type SecretFiles = { + path: string; + type: 'FILE' | 'DIRECTORY'; +}; + // Stat is the 'JSON.stringify version of the file stat type SecretStatMessage = { stat: { @@ -412,11 +420,13 @@ export type { VaultsLatestVersionMessage, SecretNameMessage, SecretIdentifierMessage, + SecretFilesMessage, ContentMessage, SecretContentMessage, SecretMkdirMessage, SecretDirMessage, SecretRenameMessage, + SecretFiles, SecretStatMessage, SignatureMessage, OverrideRPClientType, diff --git a/src/vaults/VaultManager.ts b/src/vaults/VaultManager.ts index 21c34c324..30a7183bd 100644 --- a/src/vaults/VaultManager.ts +++ b/src/vaults/VaultManager.ts @@ -1058,13 +1058,14 @@ class VaultManager { }, ); // Running the function with locking + const vaultThis = this; return yield* this.vaultLocks.withG( ...vaultLocks, async function* (): AsyncGenerator { // Getting the vaults while locked const vaults = await Promise.all( vaultIds.map(async (vaultId) => { - return await this.getVault(vaultId, tran); + return await vaultThis.getVault(vaultId, tran); }), ); return yield* g(...vaults); diff --git a/src/vaults/fileTree.ts b/src/vaults/fileTree.ts index 7180adaf6..b008aaec3 100644 --- a/src/vaults/fileTree.ts +++ b/src/vaults/fileTree.ts @@ -542,8 +542,17 @@ function parserTransformStreamFactory(): TransformStream< }; jsonParser.write(initialChunk); }; + let processed: boolean = false; return new TransformStream({ + flush: (controller) => { + if (!processed) { + controller.error( + new validationErrors.ErrorParse('Stream ended prematurely'), + ); + } + }, transform: (chunk, controller) => { + if (chunk.byteLength > 0) processed = true; switch (phase) { case 'START': { workingBuffer = vaultsUtils.uint8ArrayConcat([workingBuffer, chunk]); diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index af274acea..df452131c 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -69,6 +69,7 @@ import * as nodesUtils from '@/nodes/utils'; import * as vaultsUtils from '@/vaults/utils'; import * as vaultsErrors from '@/vaults/errors'; import * as networkUtils from '@/network/utils'; +import * as clientErrors from '@/client/errors'; import * as testsUtils from '../../utils'; describe('vaultsClone', () => { @@ -1587,16 +1588,31 @@ describe('vaultsSecretsNewDir and vaultsSecretsList', () => { dirName: secretDir, }); expect(addResponse.success).toBeTruthy(); - // List secrets - const listResponse = await rpcClient.methods.vaultsSecretsList({ + + await expect(async () => { + const files = await rpcClient.methods.vaultsSecretsList({ + nameOrId: vaultsIdEncoded, + path: 'doesntExist', + }); + try { + for await (const _ of files); // Consume values + } catch (e) { + throw e.cause; + } + }).rejects.toThrow(clientErrors.ErrorClientFSReadFailed); + + const secrets = await rpcClient.methods.vaultsSecretsList({ nameOrId: vaultsIdEncoded, + path: 'secretDir', }); - const secrets: Array = []; - for await (const secret of listResponse) { - secrets.push(secret.secretName); + + // Extract secret file paths + const parsedFiles: Array = []; + for await (const file of secrets) { + parsedFiles.push(file.path); } - expect(secrets.sort()).toStrictEqual( - secretList.map((secret) => path.join('secretDir', secret)).sort(), + expect(parsedFiles).toIncludeAllMembers( + secretList.map((secret) => path.join('secretDir', secret)), ); }); });