Skip to content

Commit

Permalink
Merge pull request #785 from MatrixAI/feature-unix-ls
Browse files Browse the repository at this point in the history
Implementing general-purpose file list handler
  • Loading branch information
aryanjassal authored Aug 26, 2024
2 parents 4e23705 + 4b99748 commit 6749415
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 40 deletions.
70 changes: 42 additions & 28 deletions src/client/handlers/VaultsSecretsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,69 @@ import type { DB } from '@matrixai/db';
import type {
ClientRPCRequestParams,
ClientRPCResponseResult,
SecretNameMessage,
VaultIdentifierMessage,
SecretFilesMessage,
SecretIdentifierMessage,
} 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';

class VaultsSecretsList extends ServerHandler<
{
vaultManager: VaultManager;
db: DB;
},
ClientRPCRequestParams<VaultIdentifierMessage>,
ClientRPCResponseResult<SecretNameMessage>
ClientRPCRequestParams<SecretIdentifierMessage>,
ClientRPCResponseResult<SecretFilesMessage>
> {
public async *handle(
input: ClientRPCRequestParams<VaultIdentifierMessage>,
_cancel,
_meta,
ctx,
): AsyncGenerator<ClientRPCResponseResult<SecretNameMessage>> {
if (ctx.signal.aborted) throw ctx.signal.reason;
input: ClientRPCRequestParams<SecretIdentifierMessage>,
_cancel: any,
): AsyncGenerator<ClientRPCResponseResult<SecretFilesMessage>, 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<
SecretFilesMessage,
void,
void
> {
let files: Array<string | Buffer>;
try {
files = await fs.promises.readdir(input.secretName);
} catch (e) {
if (e.code === 'ENOENT') {
throw new vaultsErrors.ErrorSecretsDirectoryUndefined(e.message, {
cause: e,
});
}
if (e.code === 'ENOTDIR') {
throw new vaultsErrors.ErrorSecretsIsSecret(e.message, {
cause: e,
});
}
throw e;
}
for await (const file of files) {
const filePath = path.join(input.secretName, file.toString());
const stat = await fs.promises.stat(filePath);
const type = stat.isFile() ? 'FILE' : 'DIRECTORY';
yield { path: filePath, type: type };
}
});
});
for (const secret of secrets) {
if (ctx.signal.aborted) throw ctx.signal.reason;
yield {
secretName: secret,
};
}
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,11 @@ type VaultsLatestVersionMessage = {
};

// Secrets

type SecretNameMessage = {
type SecretPathMessage = {
secretName: string;
};

type SecretIdentifierMessage = VaultIdentifierMessage & SecretNameMessage;
type SecretIdentifierMessage = VaultIdentifierMessage & SecretPathMessage;

// Contains binary content as a binary string 'toString('binary')'
type ContentMessage = {
Expand All @@ -327,6 +326,11 @@ type SecretRenameMessage = SecretIdentifierMessage & {
newSecretName: string;
};

type SecretFilesMessage = {
path: string;
type: 'FILE' | 'DIRECTORY';
};

// Stat is the 'JSON.stringify version of the file stat
type SecretStatMessage = {
stat: {
Expand Down Expand Up @@ -410,13 +414,14 @@ export type {
VaultsScanMessage,
VaultsVersionMessage,
VaultsLatestVersionMessage,
SecretNameMessage,
SecretPathMessage,
SecretIdentifierMessage,
ContentMessage,
SecretContentMessage,
SecretMkdirMessage,
SecretDirMessage,
SecretRenameMessage,
SecretFilesMessage,
SecretStatMessage,
SignatureMessage,
OverrideRPClientType,
Expand Down
3 changes: 2 additions & 1 deletion src/vaults/VaultManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,13 +1058,14 @@ class VaultManager {
},
);
// Running the function with locking
const vaultThis = this;
return yield* this.vaultLocks.withG(
...vaultLocks,
async function* (): AsyncGenerator<T, Treturn, Tnext> {
// 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);
Expand Down
12 changes: 12 additions & 0 deletions src/vaults/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ class ErrorSecretsIsDirectory<T> extends ErrorSecrets<T> {
exitCode = sysexits.USAGE;
}

class ErrorSecretsIsSecret<T> extends ErrorSecrets<T> {
static description = 'Is a secret and not a directory';
exitCode = sysexits.USAGE;
}

class ErrorSecretsDirectoryUndefined<T> extends ErrorSecrets<T> {
static description = 'Directory does not exist';
exitCode = sysexits.USAGE;
}

export {
ErrorVaults,
ErrorVaultManagerRunning,
Expand All @@ -144,4 +154,6 @@ export {
ErrorSecretsSecretUndefined,
ErrorSecretsSecretDefined,
ErrorSecretsIsDirectory,
ErrorSecretsIsSecret,
ErrorSecretsDirectoryUndefined,
};
12 changes: 12 additions & 0 deletions src/vaults/fileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,20 @@ function parserTransformStreamFactory(): TransformStream<
};
jsonParser.write(initialChunk);
};
/* Check if any chunks have been processed. If the stream is being flushed
* without processing any chunks, then something went wrong with the stream.
*/
let processedChunks: boolean = false;
return new TransformStream<Uint8Array, TreeNode | ContentNode | Uint8Array>({
flush: (controller) => {
if (!processedChunks) {
controller.error(
new validationErrors.ErrorParse('Stream ended prematurely'),
);
}
},
transform: (chunk, controller) => {
if (chunk.byteLength > 0) processedChunks = true;
switch (phase) {
case 'START': {
workingBuffer = vaultsUtils.uint8ArrayConcat([workingBuffer, chunk]);
Expand Down
30 changes: 23 additions & 7 deletions tests/client/handlers/vaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1587,16 +1587,32 @@ describe('vaultsSecretsNewDir and vaultsSecretsList', () => {
dirName: secretDir,
});
expect(addResponse.success).toBeTruthy();
// List secrets
const listResponse = await rpcClient.methods.vaultsSecretsList({

const noFiles = await rpcClient.methods.vaultsSecretsList({
nameOrId: vaultsIdEncoded,
secretName: 'doesntExist',
});
const secrets: Array<string> = [];
for await (const secret of listResponse) {
secrets.push(secret.secretName);

await expect(async () => {
try {
for await (const _ of noFiles);
} catch (e) {
throw e.cause;
}
}).rejects.toThrow(vaultsErrors.ErrorSecretsDirectoryUndefined);

const secrets = await rpcClient.methods.vaultsSecretsList({
nameOrId: vaultsIdEncoded,
secretName: 'secretDir',
});

// Extract secret file paths
const parsedFiles: Array<string> = [];
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)),
);
});
});
Expand Down

0 comments on commit 6749415

Please sign in to comment.