1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx
Sebastian Malton f021d9d0db
Change notification when extension is not found (#5184)
* Change notification when extension is not found

- Removes legacy downloadFile and downloadJson in favour of using
  `node-fetch`

- A notification will be displayed if the URL provided results in a
  status of 404

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Resolve PR comments

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-10-11 10:40:02 -04:00

206 lines
7.3 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isObject } from "../../../common/utils";
import React from "react";
import { SemVer } from "semver";
import URLParse from "url-parse";
import { getInjectable } from "@ogre-tools/injectable";
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable";
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import confirmInjectable from "../confirm-dialog/confirm.injectable";
import { reduce } from "lodash";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { withTimeout } from "../../../common/fetch/timeout-controller";
import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable";
import downloadJsonInjectable from "../../../common/fetch/download-json.injectable";
import type { PackageJson } from "type-fest";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
import loggerInjectable from "../../../common/logger.injectable";
export interface ExtensionInfo {
name: string;
version?: string;
requireConfirmation?: boolean;
}
interface NpmPackageVersionDescriptor extends PackageJson {
dist: {
integrity: string;
shasum: string;
tarball: string;
};
}
interface NpmRegistryPackageDescriptor {
versions: Partial<Record<string, NpmPackageVersionDescriptor>>;
"dist-tags"?: Partial<Record<string, string>>;
}
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
const attemptInstallByInfoInjectable = getInjectable({
id: "attempt-install-by-info",
instantiate: (di): AttemptInstallByInfo => {
const attemptInstall = di.inject(attemptInstallInjectable);
const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable);
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
const confirm = di.inject(confirmInjectable);
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
const downloadJson = di.inject(downloadJsonInjectable);
const downloadBinary = di.inject(downloadBinaryInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const logger = di.inject(loggerInjectable);
return async (info) => {
const { name, version: versionOrTagName, requireConfirmation = false } = info;
const disposer = extensionInstallationStateStore.startPreInstall();
const baseUrl = await getBaseRegistryUrl();
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
let json: NpmRegistryPackageDescriptor;
try {
const result = await downloadJson(registryUrl);
if (!result.callWasSuccessful) {
showErrorNotification(`Failed to get registry information for extension: ${result.error}`);
return disposer();
}
if (!isObject(result.response) || Array.isArray(result.response)) {
showErrorNotification("Failed to get registry information for extension");
return disposer();
}
if (result.response.error || !isObject(result.response.versions)) {
const message = result.response.error ? `: ${result.response.error}` : "";
showErrorNotification(`Failed to get registry information for extension${message}`);
return disposer();
}
json = result.response as unknown as NpmRegistryPackageDescriptor;
} catch (error) {
if (error instanceof SyntaxError) {
// assume invalid JSON
logger.warn("Set registry has invalid json", { url: baseUrl }, error);
showErrorNotification("Failed to get valid registry information for extension. Registry did not return valid JSON");
} else {
logger.error("Failed to download registry information", error);
showErrorNotification(`Failed to get valid registry information for extension. ${error}`);
}
return disposer();
}
let version = versionOrTagName;
if (versionOrTagName) {
validDistTagName:
if (!json.versions[versionOrTagName]) {
if (json["dist-tags"]) {
const potentialVersion = json["dist-tags"][versionOrTagName];
if (potentialVersion) {
if (!json.versions[potentialVersion]) {
showErrorNotification((
<p>
Configured registry claims to have tag
{" "}
<code>{versionOrTagName}</code>
.
{" "}
But does not have version infomation for the reference.
</p>
));
return disposer();
}
version = potentialVersion;
break validDistTagName;
}
}
showErrorNotification((
<p>
{"The "}
<em>{name}</em>
{" extension does not have a version or tag "}
<code>{versionOrTagName}</code>
.
</p>
));
return disposer();
}
} else {
const versions = Object.keys(json.versions)
.map(version => new SemVer(version, { loose: true }))
// ignore pre-releases for auto picking the version
.filter(version => version.prerelease.length === 0);
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
version = latestVersion?.format();
}
if (!version) {
logger.error("No versions supplied for extension", { name });
showErrorNotification(`No versions found for ${name}`);
return disposer();
}
const versionInfo = json.versions[version];
const tarballUrl = versionInfo?.dist.tarball;
if (!tarballUrl) {
showErrorNotification("Configured registry has invalid data model. Please verify that it is like NPM's.");
logger.warn(`[ATTEMPT-INSTALL-BY-INFO]: registry returned unexpected data, final version is ${version} but the versions object is missing .dist.tarball as a string`, versionInfo);
return disposer();
}
if (requireConfirmation) {
const proceed = await confirm({
message: (
<p>
{"Are you sure you want to install "}
<b>
{`${name}@${version}`}
</b>
?
</p>
),
labelCancel: "Cancel",
labelOk: "Install",
});
if (!proceed) {
return disposer();
}
}
const fileName = getBasenameOfPath(tarballUrl);
const { signal } = withTimeout(10 * 60 * 1000);
const request = await downloadBinary(tarballUrl, { signal });
if (!request.callWasSuccessful) {
showErrorNotification(`Failed to download extension: ${request.error}`);
return disposer();
}
return attemptInstall({ fileName, data: request.response }, disposer);
};
},
});
export default attemptInstallByInfoInjectable;