Merge pull request #27283 from secustor/feat/add-new-readdir-action
feat(plugins/scaffolder-backend): add new fs:readdir action
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add new `fs:readdir` action to list current content of the workspace
|
||||
@@ -231,6 +231,26 @@ export const createFilesystemDeleteAction: () => TemplateAction_2<
|
||||
JsonObject
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createFilesystemReadDirAction: () => TemplateAction_2<
|
||||
{
|
||||
paths: string[];
|
||||
recursive?: boolean | undefined;
|
||||
},
|
||||
{
|
||||
files: {
|
||||
name: string;
|
||||
path: string;
|
||||
fullPath: string;
|
||||
}[];
|
||||
folders: {
|
||||
name: string;
|
||||
path: string;
|
||||
fullPath: string;
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createFilesystemRenameAction: () => TemplateAction_2<
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from './fetch';
|
||||
import {
|
||||
createFilesystemDeleteAction,
|
||||
createFilesystemReadDirAction,
|
||||
createFilesystemRenameAction,
|
||||
} from './filesystem';
|
||||
import {
|
||||
@@ -224,6 +225,7 @@ export const createBuiltinActions = (
|
||||
createFetchCatalogEntityAction({ catalogClient, auth }),
|
||||
createCatalogWriteAction(),
|
||||
createFilesystemDeleteAction(),
|
||||
createFilesystemReadDirAction(),
|
||||
createFilesystemRenameAction(),
|
||||
createGithubActionsDispatchAction({
|
||||
integrations,
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { createFilesystemDeleteAction } from './delete';
|
||||
export { createFilesystemRenameAction } from './rename';
|
||||
export { createFilesystemReadDirAction } from './read';
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createFilesystemReadDirAction } from './read';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { examples } from './read.examples';
|
||||
import yaml from 'yaml';
|
||||
|
||||
describe('fs:readdir examples', () => {
|
||||
const action = createFilesystemReadDirAction();
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
mockDir.setContent({
|
||||
workspace: {
|
||||
'file1.txt': 'hello',
|
||||
'file2.txt': 'world',
|
||||
docs: {
|
||||
'doc1.md': 'hello',
|
||||
'doc2.md': 'world',
|
||||
assets: {
|
||||
'asset1.jpg': 'sddfsdfsdfsdf',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const workspacePath = resolvePath(mockDir.path, 'workspace');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be able to read workspace root', async () => {
|
||||
const example = examples[0].example;
|
||||
const mockContext = createMockActionContext<any, any>({
|
||||
workspacePath,
|
||||
input: yaml.parse(example).steps[0].input,
|
||||
});
|
||||
await action.handler(mockContext);
|
||||
expect(mockContext.output).toHaveBeenNthCalledWith(1, 'files', [
|
||||
{
|
||||
name: 'file1.txt',
|
||||
path: 'file1.txt',
|
||||
fullPath: resolvePath(workspacePath, 'file1.txt'),
|
||||
},
|
||||
{
|
||||
name: 'file2.txt',
|
||||
path: 'file2.txt',
|
||||
fullPath: resolvePath(workspacePath, 'file2.txt'),
|
||||
},
|
||||
]);
|
||||
expect(mockContext.output).toHaveBeenNthCalledWith(2, 'folders', [
|
||||
{
|
||||
name: 'docs',
|
||||
path: 'docs',
|
||||
fullPath: resolvePath(workspacePath, 'docs'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to read directory recursively', async () => {
|
||||
const example = examples[1].example;
|
||||
const mockContext = createMockActionContext<any, any>({
|
||||
workspacePath,
|
||||
input: yaml.parse(example).steps[0].input,
|
||||
});
|
||||
await action.handler(mockContext);
|
||||
expect(mockContext.output).toHaveBeenNthCalledWith(1, 'files', [
|
||||
{
|
||||
name: 'doc1.md',
|
||||
path: 'docs/doc1.md',
|
||||
fullPath: resolvePath(workspacePath, 'docs/doc1.md'),
|
||||
},
|
||||
{
|
||||
name: 'doc2.md',
|
||||
path: 'docs/doc2.md',
|
||||
fullPath: resolvePath(workspacePath, 'docs/doc2.md'),
|
||||
},
|
||||
{
|
||||
name: 'asset1.jpg',
|
||||
path: 'docs/assets/asset1.jpg',
|
||||
fullPath: resolvePath(workspacePath, 'docs/assets/asset1.jpg'),
|
||||
},
|
||||
]);
|
||||
expect(mockContext.output).toHaveBeenNthCalledWith(2, 'folders', [
|
||||
{
|
||||
name: 'assets',
|
||||
path: 'docs/assets',
|
||||
fullPath: resolvePath(workspacePath, 'docs/assets'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
|
||||
import * as yaml from 'yaml';
|
||||
|
||||
export const examples: TemplateExample[] = [
|
||||
{
|
||||
description: 'Read complete content of workspace',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'fs:readdir',
|
||||
id: 'read-workdir',
|
||||
name: 'Read workspace directory',
|
||||
input: {
|
||||
paths: ['.'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Get content of the docs folder',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'fs:readdir',
|
||||
id: 'read-workdir',
|
||||
name: 'Read workspace directory',
|
||||
input: {
|
||||
paths: ['docs'],
|
||||
recursive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: 'Get content of multiple folders',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'fs:readdir',
|
||||
id: 'read-workdir',
|
||||
name: 'Read workspace directory',
|
||||
input: {
|
||||
paths: ['foo', 'bar'],
|
||||
recursive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import z from 'zod';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const contentSchema = z.object({
|
||||
name: z.string().describe('Name of the file or directory'),
|
||||
path: z
|
||||
.string()
|
||||
.describe('path to the file or directory relative to the workspace'),
|
||||
fullPath: z.string().describe('full path to the file or directory'),
|
||||
});
|
||||
type Content = z.infer<typeof contentSchema>;
|
||||
|
||||
/**
|
||||
* Creates new action that enables reading directories in the workspace.
|
||||
* @public
|
||||
*/
|
||||
export const createFilesystemReadDirAction = () => {
|
||||
return createTemplateAction({
|
||||
id: 'fs:readdir',
|
||||
description: 'Reads files and directories from the workspace',
|
||||
supportsDryRun: true,
|
||||
schema: {
|
||||
input: z.object({
|
||||
paths: z.array(z.string().min(1)),
|
||||
recursive: z.boolean().default(false),
|
||||
}),
|
||||
output: z.object({
|
||||
files: z.array(contentSchema),
|
||||
folders: z.array(contentSchema),
|
||||
}),
|
||||
},
|
||||
async handler(ctx) {
|
||||
const files: Content[] = [];
|
||||
const folders: Content[] = [];
|
||||
|
||||
for (const localPath of ctx.input.paths) {
|
||||
const fullWorkspacePath = resolveSafeChildPath(
|
||||
ctx.workspacePath,
|
||||
localPath,
|
||||
);
|
||||
const content = await fs.readdir(fullWorkspacePath, {
|
||||
recursive: ctx.input.recursive,
|
||||
withFileTypes: true,
|
||||
});
|
||||
for (const dirent of content) {
|
||||
const fullPath = path.join(dirent.parentPath, dirent.name);
|
||||
const element = {
|
||||
name: dirent.name,
|
||||
path: path.relative(ctx.workspacePath, fullPath),
|
||||
fullPath,
|
||||
};
|
||||
if (dirent.isDirectory()) {
|
||||
folders.push(element);
|
||||
} else {
|
||||
files.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.output('files', files);
|
||||
ctx.output('folders', folders);
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user