1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix ensure hashed directory for extension (#7188)

* Fix ensure hashed directory for extension

Earlier open lens versions stored extension_data directory based on
path of extension's package.json. This causes problems
because extensions have moved to new location. This ensures
backward compatibility that extension will get the same
directory than before the change.

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Reconstruct the key for old hashed directories

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Remove unnecessary return

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Use sync version of random bytes

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Add migration to new type of key

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Fix global override for random bytes to not return a promise

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Make registeredExtensions a dependency

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

---------

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>
This commit is contained in:
Juho Heikka 2023-02-21 16:09:03 +02:00 committed by GitHub
parent b3ae5a0446
commit d8acff67d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 18 deletions

View File

@ -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) {

View File

@ -4,13 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { randomBytes } from "crypto";
import { promisify } from "util";
export type RandomBytes = (size: number) => Promise<Buffer>;
export type RandomBytes = (size: number) => Buffer;
const randomBytesInjectable = getInjectable({
id: "random-bytes",
instantiate: (): RandomBytes => promisify(randomBytes),
instantiate: (): RandomBytes => randomBytes,
causesSideEffects: true,
});

View File

@ -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<string, string>) => Promise<string>;
export type EnsureHashedDirectoryForExtension = (extensionName: string) => Promise<string>;
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);

View File

@ -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<string, string>;
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);
});
});
});

View File

@ -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<MigrationDeclaration>({
id: "file-system-provisioner-store-injection-token",
});

View File

@ -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),
}),
});

View File

@ -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<LensExtensionId, string>;
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly registeredExtensions = observable.map<LensExtensionId, string>();
constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-filesystem-provisioner-store",
@ -37,17 +37,17 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
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),
});
}
}

View File

@ -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;

View File

@ -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<LensExtensionId, string>(),
});