diff --git a/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts b/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts index 6f83a264e4..9fce1baebd 100644 --- a/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts +++ b/packages/core/src/common/utils/random-bytes.global-override-for-injectable.ts @@ -6,7 +6,7 @@ import { getGlobalOverride } from "../test-utils/get-global-override"; import randomBytesInjectable from "./random-bytes.injectable"; -export default getGlobalOverride(randomBytesInjectable, () => async (size) => { +export default getGlobalOverride(randomBytesInjectable, () => (size) => { const res = Buffer.alloc(size); for (let i = 0; i < size; i += 1) { diff --git a/packages/core/src/common/utils/random-bytes.injectable.ts b/packages/core/src/common/utils/random-bytes.injectable.ts index 9f00961824..fce3a0d523 100644 --- a/packages/core/src/common/utils/random-bytes.injectable.ts +++ b/packages/core/src/common/utils/random-bytes.injectable.ts @@ -4,13 +4,12 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { randomBytes } from "crypto"; -import { promisify } from "util"; -export type RandomBytes = (size: number) => Promise; +export type RandomBytes = (size: number) => Buffer; const randomBytesInjectable = getInjectable({ id: "random-bytes", - instantiate: (): RandomBytes => promisify(randomBytes), + instantiate: (): RandomBytes => randomBytes, causesSideEffects: true, }); 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 index 6b696f663d..50051bf094 100644 --- 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 @@ -3,17 +3,18 @@ * 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 { getOrInsert } 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"; +import getPathToLegacyPackageJsonInjectable from "./get-path-to-legacy-package-json.injectable"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; -export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap) => Promise; +export type EnsureHashedDirectoryForExtension = (extensionName: string) => Promise; const ensureHashedDirectoryForExtensionInjectable = getInjectable({ id: "ensure-hashed-directory-for-extension", @@ -24,14 +25,27 @@ const ensureHashedDirectoryForExtensionInjectable = getInjectable({ const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable); const ensureDirectory = di.inject(ensureDirInjectable); const getHash = di.inject(getHashInjectable); + const getPathToLegacyPackageJson = di.inject(getPathToLegacyPackageJsonInjectable); + const registeredExtensions = di.inject(registeredExtensionsInjectable); - return async (extensionName, registeredExtensions) => { - const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => { - const salt = (await randomBytes(32)).toString("hex"); + return async (extensionName) => { + let dirPath: string; + + const legacyDirPath = getPathToLegacyPackageJson(extensionName); + const hashedDirectoryForLegacyDirPath = registeredExtensions.get(legacyDirPath); + + if (hashedDirectoryForLegacyDirPath) { + registeredExtensions.set(extensionName, hashedDirectoryForLegacyDirPath); + registeredExtensions.delete(legacyDirPath); + dirPath = hashedDirectoryForLegacyDirPath; + } else { + const salt = randomBytes(32).toString("hex"); const hashedName = getHash(`${extensionName}/${salt}`); - return joinPaths(directoryForExtensionData, hashedName); - }); + const hashedExtensionDirectory = joinPaths(directoryForExtensionData, hashedName); + + dirPath = getOrInsert(registeredExtensions, extensionName, hashedExtensionDirectory); + } await ensureDirectory(dirPath); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts new file mode 100644 index 0000000000..dab86f68b2 --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.test.ts @@ -0,0 +1,96 @@ +/** + * 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 { runInAction } from "mobx"; +import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable"; +import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; +import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable"; +import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; + +describe("ensure-hashed-directory-for-extension", () => { + let ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension; + let ensureDirMock: jest.Mock; + let registeredExtensions: ObservableMap; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + ensureDirMock = jest.fn(); + + di.override(ensureDirInjectable, () => ensureDirMock); + di.override(directoryForExtensionDataInjectable, () => "some-directory-for-extension-data"); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + + ensureHashedDirectoryForExtension = di.inject( + ensureHashedDirectoryForExtensionInjectable, + ); + + registeredExtensions = di.inject(registeredExtensionsInjectable); + }); + + it("given registered extension exists, returns existing directory", async () => { + runInAction(() => { + registeredExtensions.set("some-extension-name", "some-directory"); + }); + + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory"); + }); + + it("given registered extension does not exist, returns random directory", async () => { + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory-for-extension-data/a37a1cfefc0391af3733f23cb6b29443f596a2b8ffe6d116c35df7bc3cd99ef6"); + }); + + describe("given extension directory was saved based on extension's package.json path", () => { + beforeEach(() => { + runInAction(() => { + registeredExtensions.set("/some-directory-for-user-data/node_modules/some-extension-name/package.json", "some-directory"); + }); + ensureDirMock.mockClear(); + }); + + it("returns existing directory", async () => { + const actual = await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(actual).toBe("some-directory"); + }); + + it("ensure dir is called with some directory", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(ensureDirMock).toHaveBeenCalledWith("some-directory"); + }); + + it("is migrated to use the extension name as key", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(registeredExtensions.get("some-extension-name")).toEqual("some-directory"); + }); + + it("old key is removed", async () => { + await ensureHashedDirectoryForExtension( + "some-extension-name", + ); + + expect(registeredExtensions.has("/some-directory-for-user-data/node_modules/some-extension-name/package.json")).toEqual(false); + }); + }); +}); diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts new file mode 100644 index 0000000000..d7635a71b2 --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { MigrationDeclaration } from "../../../common/base-store/migrations.injectable"; + +export const fileSystemProvisionerStoreInjectionToken = getInjectionToken({ + id: "file-system-provisioner-store-injection-token", +}); 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 7a9d3c4aa1..847ae37f03 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 @@ -14,6 +14,7 @@ import { persistStateToConfigInjectionToken } from "../../../common/base-store/s 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"; +import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; const fileSystemProvisionerStoreInjectable = getInjectable({ id: "file-system-provisioner-store", @@ -30,6 +31,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({ enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable), + registeredExtensions: di.inject(registeredExtensionsInjectable), }), }); 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 7c4fb87361..37bff649ad 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,7 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, makeObservable, observable } from "mobx"; +import type { ObservableMap } from "mobx"; +import { action, makeObservable } 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"; @@ -16,11 +17,10 @@ interface FSProvisionModel { interface Dependencies extends BaseStoreDependencies { ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension; + registeredExtensions: ObservableMap; } export class FileSystemProvisionerStore extends BaseStore { - readonly registeredExtensions = observable.map(); - constructor(protected readonly dependencies: Dependencies) { super(dependencies, { configName: "lens-filesystem-provisioner-store", @@ -37,17 +37,17 @@ export class FileSystemProvisionerStore extends BaseStore { * @returns path to the folder that the extension can safely write files to. */ async requestDirectory(extensionName: string): Promise { - return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions); + return this.dependencies.ensureHashedDirectoryForExtension(extensionName); } @action protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void { - this.registeredExtensions.merge(extensions); + this.dependencies.registeredExtensions.merge(extensions); } toJSON(): FSProvisionModel { return toJS({ - extensions: Object.fromEntries(this.registeredExtensions), + extensions: Object.fromEntries(this.dependencies.registeredExtensions), }); } } diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts new file mode 100644 index 0000000000..668d010afa --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/get-path-to-legacy-package-json.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; + +export type GetPathToLegacyPackageJson = (extensionName: string) => string; + +const getPathToLegacyPackageJson = getInjectable({ + id: "get-path-to-legacy-package-json", + + instantiate: (di): GetPathToLegacyPackageJson => { + const directoryForUserData = di.inject(directoryForUserDataInjectable); + const joinPaths = di.inject(joinPathsInjectable); + + return (extensionName: string) => joinPaths(directoryForUserData, "node_modules", extensionName, "package.json"); + }, +}); + +export default getPathToLegacyPackageJson; diff --git a/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts new file mode 100644 index 0000000000..93fb7a342c --- /dev/null +++ b/packages/core/src/extensions/extension-loader/file-system-provisioner-store/registered-extensions.injectable.ts @@ -0,0 +1,12 @@ +/** + * 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 { observable } from "mobx"; +import type { LensExtensionId } from "../../lens-extension"; + +export const registeredExtensionsInjectable = getInjectable({ + id: "registered-extensions", + instantiate: () => observable.map(), +});