Storing the serialized workspace into google cloud bucket

Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
bnechyporenko
2024-06-09 23:03:25 +02:00
committed by blam
parent 4df96964b2
commit 38875ed1c1
24 changed files with 482 additions and 56 deletions
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,3 @@
# @backstage/plugin-scaffolder-backend-module-gcp-bucket
## 0.0.1
@@ -0,0 +1,5 @@
# @backstage/plugin-scaffolder-backend-module-gcp-bucket
The GCP bucket module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-scaffolder-backend-module-gcp-bucket
title: '@backstage/plugin-scaffolder-backend-module-gcp-bucket'
description: The GCP bucket module for @backstage/plugin-scaffolder-backend
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,2 @@
# Knip report
@@ -0,0 +1,55 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-gcp-bucket",
"version": "0.0.1",
"description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend",
"backstage": {
"role": "backend-plugin-module"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/scaffolder-backend-module-gcp-bucket"
},
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"package.json": [
"package.json"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"@google-cloud/storage": "^7.11.2",
"raw-body": "^2.4.1"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2023 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.
*/
/**
* A module for the scaffolder backend that lets you interact with azure
*
* @packageDocumentation
*/
export { gcpBucketModule as default } from './module';
@@ -0,0 +1,44 @@
/*
* 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 {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { scaffolderWorkspaceProviderExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { GcpBucketWorkspaceProvider } from './providers/GcpBucketWorkspaceProvider';
/**
* @public
* The Azure Module for the Scaffolder Backend
*/
export const gcpBucketModule = createBackendModule({
moduleId: 'gcp-bucket',
pluginId: 'scaffolder',
register({ registerInit }) {
registerInit({
deps: {
scaffolderWorkspaceProviders: scaffolderWorkspaceProviderExtensionPoint,
config: coreServices.rootConfig,
logger: coreServices.logger,
},
async init({ config, logger, scaffolderWorkspaceProviders }) {
scaffolderWorkspaceProviders.addProviders({
gcpBucket: GcpBucketWorkspaceProvider.create(logger, config),
});
},
});
},
});
@@ -0,0 +1,93 @@
/*
* Copyright 2021 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 { Config } from '@backstage/config';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
import getRawBody from 'raw-body';
import { Storage } from '@google-cloud/storage';
import { LoggerService } from '@backstage/backend-plugin-api';
import {
serializeWorkspace,
restoreWorkspace,
} from '@backstage/plugin-scaffolder-node';
export class GcpBucketWorkspaceProvider implements WorkspaceProvider {
static create(logger: LoggerService, config?: Config) {
return new GcpBucketWorkspaceProvider(new Storage(), logger, config);
}
private constructor(
private readonly storage: Storage,
private readonly logger: LoggerService,
private readonly config?: Config,
) {}
public async cleanWorkspace(options: { taskId: string }): Promise<void> {
await this.storage
.bucket(this.getGcpBucketName())
.file(options.taskId)
.delete();
}
public async serializeWorkspace(options: {
path: string;
taskId: string;
}): Promise<void> {
const fileCloud = this.storage
.bucket(this.getGcpBucketName())
.file(options.taskId);
const workspace = await serializeWorkspace(options.path);
fileCloud.save(
workspace,
{
contentType: 'application/x-tar',
},
err => {
if (err) {
this.logger.error(
`An error occurred during uploading the workspace of task ${
options.taskId
} into GCP bucket ${this.getGcpBucketName()}`,
);
}
},
);
}
public async rehydrateWorkspace(options: {
taskId: string;
targetPath: string;
}): Promise<void> {
const bucket = this.storage.bucket(this.getGcpBucketName());
const file = bucket.file(options.taskId);
const workspace = getRawBody(file.createReadStream());
await restoreWorkspace(options.targetPath, await workspace);
}
private getGcpBucketName(): string {
const bucketName = this.config?.getOptionalString(
'scaffolder.EXPERIMENTAL_gcpBucketName',
);
if (!bucketName) {
throw new Error(
`You've missed to configure scaffolder.EXPERIMENTAL_gcpBucketName in app-config.yaml file`,
);
}
return bucketName;
}
}
+9
View File
@@ -52,6 +52,15 @@ export interface Config {
*/
EXPERIMENTAL_workspaceSerialization?: boolean;
/**
* Sets the provider for workspace serialization.
*
* By default, it is your database.
*/
EXPERIMENTAL_workspaceSerializationProvider?:
| 'database'
| 'googleCloudBucket';
/**
* Every task which is in progress state and having a last heartbeat longer than a specified timeout is going to
* be attempted to recover.
@@ -33,6 +33,8 @@ import {
scaffolderAutocompleteExtensionPoint,
scaffolderTaskBrokerExtensionPoint,
scaffolderTemplatingExtensionPoint,
scaffolderWorkspaceProviderExtensionPoint,
WorkspaceProvider,
} from '@backstage/plugin-scaffolder-node/alpha';
import {
createCatalogRegisterAction,
@@ -84,10 +86,17 @@ export const scaffolderPlugin = createBackendPlugin({
},
});
<<<<<<< HEAD
const autocompleteHandlers: Record<string, AutocompleteHandler> = {};
env.registerExtensionPoint(scaffolderAutocompleteExtensionPoint, {
addAutocompleteProvider(provider) {
autocompleteHandlers[provider.id] = provider.handler;
=======
const additionalWorkspaceProviders: Record<string, WorkspaceProvider> = {};
env.registerExtensionPoint(scaffolderWorkspaceProviderExtensionPoint, {
addProviders(provider) {
Object.assign(additionalWorkspaceProviders, provider);
>>>>>>> 801e5ff2007a (Storing the serialized workspace into google cloud bucket)
},
});
@@ -172,6 +181,7 @@ export const scaffolderPlugin = createBackendPlugin({
discovery,
permissions,
autocompleteHandlers,
additionalWorkspaceProviders,
});
httpRouter.use(router);
},
@@ -40,7 +40,10 @@ import { DateTime, Duration } from 'luxon';
import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common';
import { trimEventsTillLastRecovery } from './taskRecoveryHelper';
import { intervalFromNowTill } from './dbUtil';
import { restoreWorkspace, serializeWorkspace } from './serializer';
import {
restoreWorkspace,
serializeWorkspace,
} from '@backstage/plugin-scaffolder-node';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-scaffolder-backend',
@@ -0,0 +1,45 @@
/*
* Copyright 2021 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 { TaskStore } from './types';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
export class DatabaseWorkspaceProvider implements WorkspaceProvider {
static create(storage: TaskStore) {
return new DatabaseWorkspaceProvider(storage);
}
private constructor(private readonly storage: TaskStore) {}
public async serializeWorkspace(options: {
path: string;
taskId: string;
}): Promise<void> {
this.storage.serializeWorkspace?.(options);
}
public async rehydrateWorkspace(options: {
taskId: string;
targetPath: string;
}): Promise<void> {
return this.storage.rehydrateWorkspace?.(options);
}
public async cleanWorkspace(options: { taskId: string }): Promise<void> {
return this.storage.cleanWorkspace?.(options);
}
}
@@ -35,6 +35,7 @@ import {
BackstageCredentials,
} from '@backstage/backend-plugin-api';
import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
type TaskState = {
checkpoints: {
@@ -66,13 +67,21 @@ export class TaskManager implements TaskContext {
logger: Logger,
auth?: AuthService,
config?: Config,
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
) {
const workspaceService = DefaultWorkspaceService.create(
task,
storage,
additionalWorkspaceProviders,
config,
);
const agent = new TaskManager(
task,
storage,
abortSignal,
logger,
DefaultWorkspaceService.create(task, storage, config),
workspaceService,
auth,
);
agent.startTimeout();
@@ -262,6 +271,10 @@ export class StorageTaskBroker implements TaskBroker {
private readonly logger: Logger,
private readonly config?: Config,
private readonly auth?: AuthService,
private readonly additionalWorkspaceProviders?: Record<
string,
WorkspaceProvider
>,
) {}
async list(options?: {
@@ -350,6 +363,7 @@ export class StorageTaskBroker implements TaskBroker {
this.logger,
this.auth,
this.config,
this.additionalWorkspaceProviders,
);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 The Backstage Authors
* 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.
@@ -14,12 +14,11 @@
* limitations under the License.
*/
import { TaskStore } from './types';
import { CurrentClaimedTask } from './StorageTaskBroker';
import { Config } from '@backstage/config';
type WorkspaceSerializationProvider = 'database' | 'googleCloudBucket';
import { CurrentClaimedTask } from './StorageTaskBroker';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
import { DatabaseWorkspaceProvider } from './DatabaseWorkspaceProvider';
import { TaskStore } from './types';
export interface WorkspaceService {
serializeWorkspace(options: { path: string }): Promise<void>;
@@ -33,33 +32,40 @@ export interface WorkspaceService {
}
export class DefaultWorkspaceService implements WorkspaceService {
static create(task: CurrentClaimedTask, storage: TaskStore, config?: Config) {
return new DefaultWorkspaceService(task, storage, config);
static create(
task: CurrentClaimedTask,
storage: TaskStore,
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
config?: Config,
) {
const workspaceProviderName =
config?.getOptionalString(
'scaffolder.EXPERIMENTAL_workspaceSerializationProvider',
) ?? 'database';
const workspaceProvider =
additionalWorkspaceProviders?.[workspaceProviderName] ??
DatabaseWorkspaceProvider.create(storage);
return new DefaultWorkspaceService(task, workspaceProvider);
}
private constructor(
private readonly task: CurrentClaimedTask,
private readonly storage: TaskStore,
private readonly workspaceProvider: WorkspaceProvider,
private readonly config?: Config,
) {}
public async serializeWorkspace(options: { path: string }): Promise<void> {
if (this.isWorkspaceSerializationEnabled()) {
const provider = this.getWorkspaceSerializationProvider();
switch (provider) {
case 'database':
this.storage.serializeWorkspace?.({
path: options.path,
taskId: this.task.taskId,
});
return;
case 'googleCloudBucket':
return;
default:
throw new Error(
`Workspace serialization provider ${provider} is not supported`,
);
}
await this.workspaceProvider.serializeWorkspace({
path: options.path,
taskId: this.task.taskId,
});
}
}
public async cleanWorkspace(): Promise<void> {
if (this.isWorkspaceSerializationEnabled()) {
await this.workspaceProvider.cleanWorkspace({ taskId: this.task.taskId });
}
}
@@ -68,12 +74,10 @@ export class DefaultWorkspaceService implements WorkspaceService {
targetPath: string;
}): Promise<void> {
if (this.isWorkspaceSerializationEnabled()) {
this.storage.rehydrateWorkspace?.(options);
await this.workspaceProvider.rehydrateWorkspace(options);
}
}
public async cleanWorkspace(): Promise<void> {}
private isWorkspaceSerializationEnabled(): boolean {
return (
this.config?.getOptionalBoolean(
@@ -81,10 +85,4 @@ export class DefaultWorkspaceService implements WorkspaceService {
) ?? false
);
}
private getWorkspaceSerializationProvider(): WorkspaceSerializationProvider {
return (this.config?.getOptionalString(
'scaffolder.EXPERIMENTAL_workspaceSerializationProvider',
) ?? 'database') as WorkspaceSerializationProvider;
}
}
@@ -94,7 +94,10 @@ import {
} from '@backstage/plugin-auth-node';
import { InternalTaskSecrets } from '../scaffolder/tasks/types';
import { checkPermission } from '../util/checkPermissions';
import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha';
import {
AutocompleteHandler,
WorkspaceProvider,
} from '@backstage/plugin-scaffolder-node/alpha';
/**
*
@@ -159,6 +162,7 @@ export interface RouterOptions {
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>;
permissions?: PermissionsService;
permissionRules?: Array<
TemplatePermissionRuleInput | ActionPermissionRuleInput
@@ -272,6 +276,7 @@ export async function createRouter(
scheduler,
additionalTemplateFilters,
additionalTemplateGlobals,
additionalWorkspaceProviders,
permissions,
permissionRules,
discovery = HostDiscovery.fromConfig(config),
@@ -297,7 +302,13 @@ export async function createRouter(
let taskBroker: TaskBroker;
if (!options.taskBroker) {
const databaseTaskStore = await DatabaseTaskStore.create({ database });
taskBroker = new StorageTaskBroker(databaseTaskStore, logger, config, auth);
taskBroker = new StorageTaskBroker(
databaseTaskStore,
logger,
config,
auth,
additionalWorkspaceProviders,
);
if (scheduler && databaseTaskStore.listStaleTasks) {
await scheduler.scheduleTask({
@@ -68,5 +68,33 @@ export interface ScaffolderTemplatingExtensionPoint {
// @alpha
export const scaffolderTemplatingExtensionPoint: ExtensionPoint<ScaffolderTemplatingExtensionPoint>;
// @alpha
export interface ScaffolderWorkspaceProviderExtensionPoint {
// (undocumented)
addProviders(providers: Record<string, WorkspaceProvider>): void;
}
// @alpha
export const scaffolderWorkspaceProviderExtensionPoint: ExtensionPoint<ScaffolderWorkspaceProviderExtensionPoint>;
// @alpha
export interface WorkspaceProvider {
// (undocumented)
cleanWorkspace(options: { taskId: string }): Promise<void>;
// (undocumented)
rehydrateWorkspace(options: {
taskId: string;
targetPath: string;
}): Promise<void>;
// (undocumented)
serializeWorkspace({
path,
taskId,
}: {
path: string;
taskId: string;
}): Promise<void>;
}
// (No @packageDocumentation comment for this package)
```
+6
View File
@@ -256,6 +256,9 @@ export const parseRepoUrl: (
project?: string | undefined;
};
// @public
export const restoreWorkspace: (path: string, buffer?: Buffer) => Promise<void>;
// @public (undocumented)
export interface SerializedFile {
// (undocumented)
@@ -298,6 +301,9 @@ export type SerializedTaskEvent = {
createdAt: string;
};
// @public
export const serializeWorkspace: (path: string) => Promise<Buffer>;
// @public
export interface TaskBroker {
// (undocumented)
+2
View File
@@ -61,11 +61,13 @@
"@backstage/integration": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/types": "workspace:^",
"concat-stream": "^2.0.0",
"fs-extra": "^11.2.0",
"globby": "^11.0.0",
"isomorphic-git": "^1.23.0",
"jsonschema": "^1.2.6",
"p-limit": "^3.1.0",
"tar": "^6.1.12",
"winston": "^3.2.1",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.20.4"
+43 -1
View File
@@ -67,6 +67,7 @@ export const scaffolderTaskBrokerExtensionPoint =
*/
export interface ScaffolderTemplatingExtensionPoint {
addTemplateFilters(filters: Record<string, TemplateFilter>): void;
addTemplateGlobals(filters: Record<string, TemplateGlobal>): void;
}
@@ -109,7 +110,7 @@ export interface ScaffolderAutocompleteExtensionPoint {
}
/**
* Extension point for adding template filters and globals.
* Extension point for adding autocomplete handlers.
*
* @alpha
*/
@@ -117,3 +118,44 @@ export const scaffolderAutocompleteExtensionPoint =
createExtensionPoint<ScaffolderAutocompleteExtensionPoint>({
id: 'scaffolder.autocomplete',
});
/**
* This provider has to be implemented to make it possible to serialize/deserialize scaffolder workspace.
*
* @alpha
*/
export interface WorkspaceProvider {
serializeWorkspace({
path,
taskId,
}: {
path: string;
taskId: string;
}): Promise<void>;
cleanWorkspace(options: { taskId: string }): Promise<void>;
rehydrateWorkspace(options: {
taskId: string;
targetPath: string;
}): Promise<void>;
}
/**
* Extension point for adding workspace providers.
*
* @alpha
*/
export interface ScaffolderWorkspaceProviderExtensionPoint {
addProviders(providers: Record<string, WorkspaceProvider>): void;
}
/**
* Extension point for adding workspace providers.
*
* @alpha
*/
export const scaffolderWorkspaceProviderExtensionPoint =
createExtensionPoint<ScaffolderWorkspaceProviderExtensionPoint>({
id: 'scaffolder.workspace.provider',
});
@@ -26,3 +26,5 @@ export type {
TaskEventType,
TaskStatus,
} from './types';
export * from './serializer';
@@ -20,13 +20,22 @@ import { promisify } from 'util';
import { pipeline as pipelineCb, Readable } from 'stream';
const pipeline = promisify(pipelineCb);
/**
* Serializes provided path into tar archive
*
* @public
*/
export const serializeWorkspace = async (path: string): Promise<Buffer> => {
return await new Promise<Buffer>(async resolve => {
return new Promise<Buffer>(async resolve => {
await pipeline(tar.create({ cwd: path }, ['']), concatStream(resolve));
});
};
/**
* Rehydrates the provided buffer of tar archive into the provide destination path
*
* @public
*/
export const restoreWorkspace = async (path: string, buffer?: Buffer) => {
if (buffer) {
await pipeline(
+27 -16
View File
@@ -6779,6 +6779,22 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-gcp-bucket@workspace:plugins/scaffolder-backend-module-gcp-bucket":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-gcp-bucket@workspace:plugins/scaffolder-backend-module-gcp-bucket"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-scaffolder-node": "workspace:^"
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
"@google-cloud/storage": ^7.11.2
raw-body: ^2.4.1
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-gerrit@workspace:^, @backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit"
@@ -7042,11 +7058,13 @@ __metadata:
"@backstage/integration": "workspace:^"
"@backstage/plugin-scaffolder-common": "workspace:^"
"@backstage/types": "workspace:^"
concat-stream: ^2.0.0
fs-extra: ^11.2.0
globby: ^11.0.0
isomorphic-git: ^1.23.0
jsonschema: ^1.2.6
p-limit: ^3.1.0
tar: ^6.1.12
winston: ^3.2.1
zod: ^3.22.4
zod-to-json-schema: ^3.20.4
@@ -9354,9 +9372,9 @@ __metadata:
languageName: node
linkType: hard
"@google-cloud/storage@npm:^7.0.0":
version: 7.10.1
resolution: "@google-cloud/storage@npm:7.10.1"
"@google-cloud/storage@npm:^7.0.0, @google-cloud/storage@npm:^7.11.2":
version: 7.11.2
resolution: "@google-cloud/storage@npm:7.11.2"
dependencies:
"@google-cloud/paginator": ^5.0.0
"@google-cloud/projectify": ^4.0.0
@@ -9364,16 +9382,16 @@ __metadata:
abort-controller: ^3.0.0
async-retry: ^1.3.3
duplexify: ^4.1.3
ent: ^2.2.0
fast-xml-parser: ^4.3.0
gaxios: ^6.0.2
google-auth-library: ^9.6.3
html-entities: ^2.5.2
mime: ^3.0.0
p-limit: ^3.0.1
retry-request: ^7.0.0
teeny-request: ^9.0.0
uuid: ^8.0.0
checksum: 0605f8abf67cde98bc48b05c749b569416af821fcc3eb8ed3fac6dc5ade9c1f2f3210404c85e30525260ea4f34785d7436f7684e6eacd083613237669badff25
checksum: 763cd560245006e11a060fdbd5edff5d6bfe22b0c0b06dfe88af43b7b14458598c00a71c4e1b079e9ab09096d7d262bdc27fa77d76a8969af0765f23c93f8405
languageName: node
linkType: hard
@@ -25281,13 +25299,6 @@ __metadata:
languageName: node
linkType: hard
"ent@npm:^2.2.0":
version: 2.2.0
resolution: "ent@npm:2.2.0"
checksum: f588b5707d6fef36011ea10d530645912a69530a1eb0831f8708c498ac028363a7009f45cfadd28ceb4dafd9ac17ec15213f88d09ce239cd033cfe1328dd7d7d
languageName: node
linkType: hard
"entities@npm:^2.0.0":
version: 2.2.0
resolution: "entities@npm:2.2.0"
@@ -28821,10 +28832,10 @@ __metadata:
languageName: node
linkType: hard
"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0":
version: 2.4.0
resolution: "html-entities@npm:2.4.0"
checksum: 25bea32642ce9ebd0eedc4d24381883ecb0335ccb8ac26379a0958b9b16652fdbaa725d70207ce54a51db24103436a698a8e454397d3ba8ad81460224751f1dc
"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0, html-entities@npm:^2.5.2":
version: 2.5.2
resolution: "html-entities@npm:2.5.2"
checksum: b23f4a07d33d49ade1994069af4e13d31650e3fb62621e92ae10ecdf01d1a98065c78fd20fdc92b4c7881612210b37c275f2c9fba9777650ab0d6f2ceb3b99b6
languageName: node
linkType: hard