mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
* 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>
206 lines
7.3 KiB
TypeScript
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;
|