From 3f0de0550e2348d4a333280e41607ae2f133a1ab Mon Sep 17 00:00:00 2001 From: Panu Horsmalahti Date: Wed, 15 Feb 2023 19:56:56 +0700 Subject: [PATCH] Enable extension to specify storeName for persisting data (#7107) * Enable extension to specify storeName for persisting data Signed-off-by: Panu Horsmalahti * Add unit tests and update injectables Signed-off-by: Antti Lustila * Remove unnecessary mock Signed-off-by: Antti Lustila --------- Signed-off-by: Panu Horsmalahti Signed-off-by: Antti Lustila Co-authored-by: Antti Lustila --- .../configurable-directories.test.ts | 103 ++++++++++++++++++ ...shed-directory-for-extension.injectable.ts | 43 ++++++++ ...ile-system-provisioner-store.injectable.ts | 10 +- .../file-system-provisioner-store.ts | 23 +--- .../get-hash.injectable.ts | 15 +++ .../core/src/extensions/extension-store.ts | 2 +- .../core/src/extensions/lens-extension.ts | 15 ++- 7 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/extensions/__tests__/configurable-directories.test.ts create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts create mode 100644 packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-hash.injectable.ts diff --git a/packages/core/src/extensions/__tests__/configurable-directories.test.ts b/packages/core/src/extensions/__tests__/configurable-directories.test.ts new file mode 100644 index 0000000000..f8d6f1d678 --- /dev/null +++ b/packages/core/src/extensions/__tests__/configurable-directories.test.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { runInAction } from "mobx"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake"; +import getHashInjectable from "../../extensions/extension-loader/file-system-provisioner-store/get-hash.injectable"; +import fsInjectable from "../../common/fs/fs.injectable"; + +describe("configurable directories for extension files", () => { + let builder: ApplicationBuilder; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + builder.beforeApplicationStart( + (mainDi) => { + runInAction(() => { + mainDi.override(getHashInjectable, () => x => x); + }); + }, + ); + + await builder.startHidden(); + + const window = builder.applicationWindow.create("some-window-id"); + + await window.start(); + + }); + + describe("when extension with a specific store name is enabled", () => { + let testExtensionOptions: FakeExtensionOptions; + + beforeEach(() => { + + testExtensionOptions = { + id: "some-extension", + name: "some-extension-name", + + mainOptions: { + manifest: { + name: "irrelevant", + storeName: "some-specific-store-name", + version: "0", + engines: { + lens: "0", + }, + }, + }, + }; + + builder.extensions.enable(testExtensionOptions); + }); + + it("creates extension directory for specific store name", async () => { + const fs = builder.mainDi.inject(fsInjectable); + + await builder.extensions.get("some-extension").main.getExtensionFileFolder(); + + const nonHashedExtensionDirectories = await fs.readdir("/some-directory-for-app-data/some-product-name/extension_data"); + + expect(nonHashedExtensionDirectories).toContain("some-specific-store-name"); + }); + }); + + describe("when extension with no specific store name is enabled", () => { + let testExtensionOptions: FakeExtensionOptions; + + beforeEach(() => { + + testExtensionOptions = { + id: "some-extension", + name: "some-extension-name", + + mainOptions: { + manifest: { + name: "some-package-name", + storeName: undefined, + version: "0", + engines: { + lens: "0", + }, + }, + }, + }; + + builder.extensions.enable(testExtensionOptions); + }); + + it("creates extension directory for package name", async () => { + const fs = builder.mainDi.inject(fsInjectable); + + await builder.extensions.get("some-extension").main.getExtensionFileFolder(); + + const nonHashedExtensionDirectories = await fs.readdir("/some-directory-for-app-data/some-product-name/extension_data"); + + expect(nonHashedExtensionDirectories).toContain("some-package-name"); + }); + }); +}); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts new file mode 100644 index 0000000000..6b696f663d --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ObservableMap } from "mobx"; +import { getInjectable } from "@ogre-tools/injectable"; + +import { getOrInsertWithAsync } from "../../../common/utils"; +import randomBytesInjectable from "../../../common/utils/random-bytes.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; +import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable"; +import getHashInjectable from "./get-hash.injectable"; + +export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap) => Promise; + +const ensureHashedDirectoryForExtensionInjectable = getInjectable({ + id: "ensure-hashed-directory-for-extension", + + instantiate: (di): EnsureHashedDirectoryForExtension => { + const randomBytes = di.inject(randomBytesInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable); + const ensureDirectory = di.inject(ensureDirInjectable); + const getHash = di.inject(getHashInjectable); + + return async (extensionName, registeredExtensions) => { + const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => { + const salt = (await randomBytes(32)).toString("hex"); + const hashedName = getHash(`${extensionName}/${salt}`); + + return joinPaths(directoryForExtensionData, hashedName); + }); + + await ensureDirectory(dirPath); + + return dirPath; + }; + }, +}); + +export default ensureHashedDirectoryForExtensionInjectable; diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts index b511437da9..7a9d3c4aa1 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -4,10 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { FileSystemProvisionerStore } from "./file-system-provisioner-store"; -import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; -import ensureDirectoryInjectable from "../../../common/fs/ensure-dir.injectable"; -import joinPathsInjectable from "../../../common/path/join-paths.injectable"; -import randomBytesInjectable from "../../../common/utils/random-bytes.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -17,15 +13,12 @@ import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../ import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token"; +import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; const fileSystemProvisionerStoreInjectable = getInjectable({ id: "file-system-provisioner-store", instantiate: (di) => new FileSystemProvisionerStore({ - directoryForExtensionData: di.inject(directoryForExtensionDataInjectable), - ensureDirectory: di.inject(ensureDirectoryInjectable), - joinPaths: di.inject(joinPathsInjectable), - randomBytes: di.inject(randomBytesInjectable), directoryForUserData: di.inject(directoryForUserDataInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), logger: di.inject(loggerInjectable), @@ -36,6 +29,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({ persistStateToConfig: di.inject(persistStateToConfigInjectionToken), enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable), }), }); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts index 5655a0cf06..7c4fb87361 100644 --- a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts @@ -3,25 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { SHA256 } from "crypto-js"; import { action, makeObservable, observable } from "mobx"; import type { BaseStoreDependencies } from "../../../common/base-store/base-store"; import { BaseStore } from "../../../common/base-store/base-store"; import type { LensExtensionId } from "../../lens-extension"; -import { getOrInsertWithAsync, toJS } from "../../../common/utils"; -import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable"; -import type { JoinPaths } from "../../../common/path/join-paths.injectable"; -import type { RandomBytes } from "../../../common/utils/random-bytes.injectable"; +import { toJS } from "../../../common/utils"; +import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable"; interface FSProvisionModel { extensions: Record; // extension names to paths } interface Dependencies extends BaseStoreDependencies { - readonly directoryForExtensionData: string; - ensureDirectory: EnsureDirectory; - joinPaths: JoinPaths; - randomBytes: RandomBytes; + ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension; } export class FileSystemProvisionerStore extends BaseStore { @@ -43,16 +37,7 @@ export class FileSystemProvisionerStore extends BaseStore { * @returns path to the folder that the extension can safely write files to. */ async requestDirectory(extensionName: string): Promise { - const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => { - const salt = (await this.dependencies.randomBytes(32)).toString("hex"); - const hashedName = SHA256(`${extensionName}/${salt}`).toString(); - - return this.dependencies.joinPaths(this.dependencies.directoryForExtensionData, hashedName); - }); - - await this.dependencies.ensureDirectory(dirPath); - - return dirPath; + return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions); } @action diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-hash.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-hash.injectable.ts new file mode 100644 index 0000000000..5d7faa5884 --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-hash.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable } from "@ogre-tools/injectable"; +import { SHA256 } from "crypto-js"; + +const getHashInjectable = getInjectable({ + id: "get-hash", + + instantiate: () => (text: string) => SHA256(text).toString(), +}); + +export default getHashInjectable; diff --git a/packages/core/src/extensions/extension-store.ts b/packages/core/src/extensions/extension-store.ts index 271175f0a4..e72f394005 100644 --- a/packages/core/src/extensions/extension-store.ts +++ b/packages/core/src/extensions/extension-store.ts @@ -92,6 +92,6 @@ export abstract class ExtensionStore extends BaseStore { protected cwd() { assert(this.extension, "must call this.load() first"); - return path.join(super.cwd(), "extension-store", this.extension.name); + return path.join(super.cwd(), "extension-store", this.extension.storeName); } } diff --git a/packages/core/src/extensions/lens-extension.ts b/packages/core/src/extensions/lens-extension.ts index 44848d5f84..53c9343607 100644 --- a/packages/core/src/extensions/lens-extension.ts +++ b/packages/core/src/extensions/lens-extension.ts @@ -27,6 +27,10 @@ export interface LensExtensionManifest extends PackageJson { npm?: string; node?: string; }; + + // Specify extension name used for persisting data. + // Useful if extension is renamed but the data should not be lost. + storeName?: string; } export const lensExtensionDependencies = Symbol("lens-extension-dependencies"); @@ -62,7 +66,10 @@ export class LensExtension< constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { makeObservable(this); + + // id is the name of the manifest this.id = id; + this.manifest = manifest; this.manifestPath = manifestPath; this.isBundled = !!isBundled; @@ -80,6 +87,11 @@ export class LensExtension< return this.manifest.description; } + // Name of extension for persisting data + get storeName() { + return this.manifest.storeName || this.name; + } + /** * @ignore */ @@ -93,7 +105,8 @@ export class LensExtension< * folder name. */ async getExtensionFileFolder(): Promise { - return this[lensExtensionDependencies].fileSystemProvisionerStore.requestDirectory(this.id); + // storeName is read from the manifest and has a fallback to the manifest name, which equals id + return this[lensExtensionDependencies].fileSystemProvisionerStore.requestDirectory(this.storeName); } @action