Skip to content

Commit

Permalink
feat: v1 logic for downloading plugin repository
Browse files Browse the repository at this point in the history
  • Loading branch information
devcatalin committed Oct 7, 2021
1 parent 8fed344 commit cebedce
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 13 deletions.
21 changes: 18 additions & 3 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {APP_MIN_HEIGHT, APP_MIN_WIDTH} from '@constants/constants';
import {checkMissingDependencies} from '@utils/index';

import log from 'loglevel';
import {DOWNLOAD_PLUGIN, DOWNLOAD_PLUGIN_RESULT} from '@constants/ipcEvents';
import terminal from '../cli/terminal';
import {createMenu} from './menu';

// console.log(store);
import {downloadPlugin} from './pluginService';

Object.assign(console, ElectronLog.functions);

Expand All @@ -34,12 +34,27 @@ const ElectronStore = require('electron-store');
const {MONOKLE_RUN_AS_NODE} = process.env;

const userHomeDir = app.getPath('home');
const userDataDir = app.getPath('userData');
const pluginsDir = path.join(userDataDir, 'monoklePlugins');
const APP_DEPENDENCIES = ['kubectl', 'helm'];

ipcMain.on('get-user-home-dir', event => {
event.returnValue = userHomeDir;
});

ipcMain.on(DOWNLOAD_PLUGIN, async (event, pluginUrl: string) => {
try {
await downloadPlugin(pluginUrl, pluginsDir);
event.sender.send(DOWNLOAD_PLUGIN_RESULT);
} catch (err) {
if (err instanceof Error) {
event.sender.send(DOWNLOAD_PLUGIN_RESULT, err);
} else {
log.warn(err);
}
}
});

/**
* called by thunk to preview a kustomization
*/
Expand Down
119 changes: 119 additions & 0 deletions electron/pluginService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import path from 'path';
import fetch from 'node-fetch';
import fs from 'fs';
import util from 'util';
import tar from 'tar';
import {PackageJsonMonoklePlugin} from '@models/plugin';
import {downloadFile} from '@utils/http';

const fsExistsPromise = util.promisify(fs.exists);
const fsMkdirPromise = util.promisify(fs.mkdir);

const GITHUB_URL = 'https://github.com';
const GITHUB_REPOSITORY_REGEX = /^https:\/\/github.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/i;

function isValidRepositoryUrl(repositoryUrl: string) {
return GITHUB_REPOSITORY_REGEX.test(repositoryUrl);
}

function extractRepositoryOwnerAndName(pluginUrl: string) {
if (!isValidRepositoryUrl(pluginUrl)) {
throw new Error('Invalig repository URL');
}
const repositoryPath = pluginUrl.split(`${GITHUB_URL}/`)[1];
const [repositoryOwner, repositoryName] = repositoryPath.split('/');

return {
repositoryOwner,
repositoryName,
};
}

function extractPluginInfo(packageJson: PackageJsonMonoklePlugin) {
const {name, version, author, description, monoklePlugin} = packageJson;
if (name === undefined || version === undefined || author === undefined || monoklePlugin === undefined) {
throw new Error('Invalid plugin package.json');
}
return {name, version, author, description, monoklePlugin};
}

async function fetchLatestRelease(repositoryOwner: string, repositoryName: string) {
const latestReleaseUrl = `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/releases/latest`;
const latestReleaseResponse = await fetch(latestReleaseUrl);
const latestReleaseJson: any = await latestReleaseResponse.json();
const targetCommitish = latestReleaseJson?.target_commitish;
const tarballUrl = latestReleaseJson?.tarball_url;
const tagName = latestReleaseJson?.tag_name;
if (typeof targetCommitish !== 'string' || typeof tarballUrl !== 'string' || typeof tagName !== 'string') {
throw new Error("Couldn't fetch the latest release of the plugin.");
}
return {targetCommitish, tarballUrl, tagName};
}

async function fetchPackageJson(repositoryOwner: string, repositoryName: string, targetCommitish: string) {
const packageJsonUrl = `https://raw.githubusercontent.com/${repositoryOwner}/${repositoryName}/${targetCommitish}/package.json`;
const packageJsonResponse = await fetch(packageJsonUrl);
if (!packageJsonResponse.ok) {
throw new Error("Couldn't fetch package.json");
}
const packageJson = await packageJsonResponse.json();
return packageJson as PackageJsonMonoklePlugin;
}

async function fetchLatestReleaseCommitSha(repositoryOwner: string, repositoryName: string, tagName: string) {
const refTagUrl = `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/git/ref/tags/${tagName}`;
const refTagResponse = await fetch(refTagUrl);
if (!refTagResponse.ok) {
throw new Error("Couldn't fetch git ref tag.");
}
const refTagJson: any = await refTagResponse.json();

let commitSha: string | undefined;
if (typeof refTagJson?.object?.type !== 'string' || typeof refTagJson?.object?.sha !== 'string') {
throw new Error("Couldn't find the ref tag object.");
}

if (refTagJson.object.type === 'commit') {
commitSha = refTagJson.object.sha;
} else {
const tagUrl = `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/git/tags/${refTagJson.object.sha}`;
const tagResponse = await fetch(tagUrl);
const tagJson: any = tagResponse.json();
if (tagJson?.object && typeof tagJson?.object?.sha === 'string') {
commitSha = tagJson.object.sha;
}
}

if (commitSha === undefined) {
throw new Error("Couldn't find the commit sha of the latest release.");
}
return commitSha;
}

export async function downloadPlugin(pluginUrl: string, pluginsDir: string) {
const {repositoryOwner, repositoryName} = extractRepositoryOwnerAndName(pluginUrl);
const {targetCommitish, tarballUrl, tagName} = await fetchLatestRelease(repositoryOwner, repositoryName);
const packageJson = await fetchPackageJson(repositoryOwner, repositoryName, targetCommitish);
const commitSha = await fetchLatestReleaseCommitSha(repositoryOwner, repositoryName, tagName);
const pluginInfo = extractPluginInfo(packageJson);
const doesPluginsDirExist = await fsExistsPromise(pluginsDir);
if (!doesPluginsDirExist) {
await fsMkdirPromise(pluginsDir);
}
const pluginFolderPath: string = path.join(pluginsDir, `${repositoryOwner}-${repositoryName}`);
if (!fs.existsSync(pluginFolderPath)) {
await fsMkdirPromise(pluginFolderPath);
}
const pluginTarballFilePath = path.join(pluginFolderPath, `${commitSha}.tgz`);
const pluginCommitShaFolder: string = path.join(pluginFolderPath, commitSha);
if (fs.existsSync(pluginTarballFilePath) || fs.existsSync(pluginCommitShaFolder)) {
throw new Error('Plugin already exists.');
}
await downloadFile(tarballUrl, pluginTarballFilePath);
await fsMkdirPromise(pluginCommitShaFolder);
await tar.extract({
file: pluginTarballFilePath,
cwd: pluginCommitShaFolder,
strip: 1,
});
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@types/micromatch": "4.0.2",
"@types/module-alias": "^2.0.1",
"@types/node": "16.6.1",
"@types/node-fetch": "2.5.12",
"@types/react": "17.0.18",
"@types/react-dom": "17.0.9",
"@types/redux-logger": "3.0.9",
Expand Down Expand Up @@ -92,7 +93,7 @@
"micromatch": "4.0.4",
"module-alias": "^2.2.2",
"monaco-yaml": "3.1.0",
"node-fetch": "3.0.0",
"node-fetch": "2.6.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-flow-renderer": "9.6.7",
Expand Down
36 changes: 34 additions & 2 deletions src/components/organisms/PluginManagerPane/PluginInstallModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import React, {useState} from 'react';
import {Input, Modal} from 'antd';
import {ipcRenderer} from 'electron';
import {DOWNLOAD_PLUGIN, DOWNLOAD_PLUGIN_RESULT} from '@constants/ipcEvents';

const downloadPlugin = (pluginUrl: string) => {
return new Promise<void>((resolve, reject) => {
const downloadPluginResult = (event: Electron.IpcRendererEvent, result: any) => {
if (result instanceof Error) {
reject(result);
}
resolve();
};
ipcRenderer.once(DOWNLOAD_PLUGIN_RESULT, downloadPluginResult);
ipcRenderer.send(DOWNLOAD_PLUGIN, pluginUrl);
});
};

function PluginInstallModal(props: {isVisible: boolean; onClose: () => void}) {
const {isVisible, onClose} = props;
const [pluginUrl, setPluginUrl] = useState<string>();
const [pluginUrl, setPluginUrl] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>();

const handleOk = async () => {
try {
await downloadPlugin(pluginUrl);
onClose();
} catch (err) {
if (err instanceof Error) {
setErrorMessage(err.message);
}
}
};

return (
<Modal visible={isVisible} onCancel={onClose}>
<Modal visible={isVisible} onCancel={onClose} onOk={handleOk}>
<p>Plugin URL:</p>
<Input value={pluginUrl} onChange={e => setPluginUrl(e.target.value)} />
{errorMessage && (
<div>
<p>{errorMessage}</p>
</div>
)}
</Modal>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants/ipcEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DOWNLOAD_PLUGIN = 'download-plugin';
export const DOWNLOAD_PLUGIN_RESULT = 'download-plugin-result';
51 changes: 44 additions & 7 deletions src/models/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
import {Merge, PackageJson} from 'type-fest';

type PackageJsonMonoklePlugin = Merge<
PackageJson,
{
monoklePlugin?: {
modules: MonoklePluginModule[];
};
}
>;

interface MonoklePluginRepositoryLatestRelease {
commitHash: string;
tagName: string;
tarballUrl: string;
}

interface MonoklePluginRepository {
owner: string;
name: string;
url: string;
latestRelease: MonoklePluginRepositoryLatestRelease;
}

interface MonoklePluginModule {
id: string;
type: string;
source: string;
description?: string;
}

interface MonoklePlugin {
name: string;
description: string;
version: string;
owner: string;
repository: {
name: string;
url: string;
};
author: string;
description?: string;
location: string;
repository: MonoklePluginRepository;
isActive: boolean;
isInstalled: boolean;
modules: MonoklePluginModule[];
}

export type {MonoklePlugin};
export type {
MonoklePlugin,
MonoklePluginModule,
MonoklePluginRepository,
MonoklePluginRepositoryLatestRelease,
PackageJsonMonoklePlugin,
};
33 changes: 33 additions & 0 deletions src/utils/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fetch from 'node-fetch';
import fs from 'fs';

export async function downloadFile(sourceUrl: string, destinationFilePath: string): Promise<void> {
const response = await fetch(sourceUrl);

if (!response.ok) {
throw new Error(`Downdload error: ${response.status} ${response.statusText}`);
}

return new Promise<void>((resolve, reject) => {
const fileStream = fs.createWriteStream(destinationFilePath);

if (!response.body) {
throw new Error(`Download error: missing response body.`);
}

response.body.pipe(fileStream);

response.body.on('error', err => {
fileStream.close();
if (fs.existsSync(destinationFilePath) && fs.statSync(destinationFilePath).isFile()) {
fs.unlinkSync(destinationFilePath);
}
reject(err);
});

fileStream.on('finish', () => {
fileStream.close();
resolve();
});
});
}

0 comments on commit cebedce

Please sign in to comment.