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:
parent
15ba5da51e
commit
3f0de0550e
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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<string, string>; // extension names to paths
|
||||
}
|
||||
|
||||
interface Dependencies extends BaseStoreDependencies {
|
||||
readonly directoryForExtensionData: string;
|
||||
ensureDirectory: EnsureDirectory;
|
||||
joinPaths: JoinPaths;
|
||||
randomBytes: RandomBytes;
|
||||
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
async requestDirectory(extensionName: string): Promise<string> {
|
||||
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
|
||||
|
||||
@ -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;
|
||||
@ -92,6 +92,6 @@ export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user