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

Enable extension to specify storeName for persisting data (#7107)

* Enable extension to specify storeName for persisting data

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Add unit tests and update injectables

Signed-off-by: Antti Lustila <antti.lustila@luotocompany.fi>

* Remove unnecessary mock

Signed-off-by: Antti Lustila <antti.lustila@luotocompany.fi>

---------

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
Signed-off-by: Antti Lustila <antti.lustila@luotocompany.fi>
Co-authored-by: Antti Lustila <antti.lustila@luotocompany.fi>
This commit is contained in:
Panu Horsmalahti 2023-02-15 19:56:56 +07:00 committed by GitHub
parent 15ba5da51e
commit 3f0de0550e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 29 deletions

View File

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

View File

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

View File

@ -4,10 +4,6 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { FileSystemProvisionerStore } from "./file-system-provisioner-store"; 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 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 getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
@ -17,15 +13,12 @@ import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../
import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
const fileSystemProvisionerStoreInjectable = getInjectable({ const fileSystemProvisionerStoreInjectable = getInjectable({
id: "file-system-provisioner-store", id: "file-system-provisioner-store",
instantiate: (di) => new FileSystemProvisionerStore({ instantiate: (di) => new FileSystemProvisionerStore({
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
ensureDirectory: di.inject(ensureDirectoryInjectable),
joinPaths: di.inject(joinPathsInjectable),
randomBytes: di.inject(randomBytesInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable), directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
@ -36,6 +29,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({
persistStateToConfig: di.inject(persistStateToConfigInjectionToken), persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
}), }),
}); });

View File

@ -3,25 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { SHA256 } from "crypto-js";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import type { BaseStoreDependencies } from "../../../common/base-store/base-store"; import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store/base-store"; import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension"; import type { LensExtensionId } from "../../lens-extension";
import { getOrInsertWithAsync, toJS } from "../../../common/utils"; import { toJS } from "../../../common/utils";
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable"; import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable";
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
import type { RandomBytes } from "../../../common/utils/random-bytes.injectable";
interface FSProvisionModel { interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths extensions: Record<string, string>; // extension names to paths
} }
interface Dependencies extends BaseStoreDependencies { interface Dependencies extends BaseStoreDependencies {
readonly directoryForExtensionData: string; ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
ensureDirectory: EnsureDirectory;
joinPaths: JoinPaths;
randomBytes: RandomBytes;
} }
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> { export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
@ -43,16 +37,7 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to. * @returns path to the folder that the extension can safely write files to.
*/ */
async requestDirectory(extensionName: string): Promise<string> { async requestDirectory(extensionName: string): Promise<string> {
const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => { return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions);
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;
} }
@action @action

View File

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

View File

@ -92,6 +92,6 @@ export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
protected cwd() { protected cwd() {
assert(this.extension, "must call this.load() first"); 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);
} }
} }

View File

@ -27,6 +27,10 @@ export interface LensExtensionManifest extends PackageJson {
npm?: string; npm?: string;
node?: 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"); export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
@ -62,7 +66,10 @@ export class LensExtension<
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
makeObservable(this); makeObservable(this);
// id is the name of the manifest
this.id = id; this.id = id;
this.manifest = manifest; this.manifest = manifest;
this.manifestPath = manifestPath; this.manifestPath = manifestPath;
this.isBundled = !!isBundled; this.isBundled = !!isBundled;
@ -80,6 +87,11 @@ export class LensExtension<
return this.manifest.description; return this.manifest.description;
} }
// Name of extension for persisting data
get storeName() {
return this.manifest.storeName || this.name;
}
/** /**
* @ignore * @ignore
*/ */
@ -93,7 +105,8 @@ export class LensExtension<
* folder name. * folder name.
*/ */
async getExtensionFileFolder(): Promise<string> { async getExtensionFileFolder(): Promise<string> {
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 @action