Install plugins from sidebar

Summary:
This diff adds "download" button to the plugins shown in "Detected in App" section, so they can be downloaded, installed and enabled just in one click.

For now UI is very simple - there is no progress indication and no error handling for failed downloads. I'll add them in next diffs.

Please note that we are explicitly "star" every installed plugin to enable it straight away without additional clicks in "disabled" section.

Reviewed By: mweststrate

Differential Revision: D25393472

fbshipit-source-id: 0a224ea6d03db0ee9a70b7fa35ede9616c03d824
This commit is contained in:
Anton Nikolaev
2020-12-15 09:28:58 -08:00
committed by Facebook GitHub Bot
parent ab441d8226
commit 97d37abbb2
7 changed files with 357 additions and 7 deletions

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
DownloadablePluginDetails,
installPluginFromFile,
} from 'flipper-plugin-lib';
import {Store} from '../reducers/index';
import {
PluginDownloadStatus,
pluginDownloadStarted,
pluginDownloadFailed,
pluginDownloadSucceeded,
} from '../reducers/pluginDownloads';
import {sideEffect} from '../utils/sideEffect';
import {default as axios} from 'axios';
import fs from 'fs-extra';
import path from 'path';
import tmp from 'tmp';
import {promisify} from 'util';
import {requirePlugin} from './plugins';
import {registerPluginUpdate} from '../reducers/connections';
// Adapter which forces node.js implementation for axios instead of browser implementation
// used by default in Electron. Node.js implementation is better, because it
// supports streams which can be used for direct downloading to disk.
const axiosHttpAdapter = require('axios/lib/adapters/http'); // eslint-disable-line import/no-commonjs
const getTempDirName = promisify(tmp.dir) as (
options?: tmp.DirOptions,
) => Promise<string>;
export default (store: Store) => {
sideEffect(
store,
{name: 'handlePluginDownloads', throttleMs: 1000, fireImmediately: true},
(state) => state.pluginDownloads,
(state, store) => {
for (const download of Object.values(state)) {
if (download.status === PluginDownloadStatus.QUEUED) {
handlePluginDownload(
download.plugin,
download.enableDownloadedPlugin,
store,
);
}
}
},
);
return async () => {};
};
async function handlePluginDownload(
plugin: DownloadablePluginDetails,
enableDownloadedPlugin: boolean,
store: Store,
) {
const dispatch = store.dispatch;
const {name, title, version, downloadUrl, dir} = plugin;
console.log(
`Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
);
const targetDir = await getTempDirName();
const targetFile = path.join(targetDir, `${name}-${version}.tgz`);
try {
const cancellationSource = axios.CancelToken.source();
dispatch(
pluginDownloadStarted({plugin, cancel: cancellationSource.cancel}),
);
await fs.ensureDir(targetDir);
let percentCompleted = 0;
const response = await axios.get(plugin.downloadUrl, {
adapter: axiosHttpAdapter,
cancelToken: cancellationSource.token,
responseType: 'stream',
onDownloadProgress: async (progressEvent) => {
const newPercentCompleted = !progressEvent.total
? 0
: Math.round((progressEvent.loaded * 100) / progressEvent.total);
if (newPercentCompleted - percentCompleted >= 20) {
percentCompleted = newPercentCompleted;
console.log(
`Downloading plugin "${title}" v${version} from "${downloadUrl}": ${percentCompleted}% completed (${progressEvent.loaded} from ${progressEvent.total})`,
);
}
},
});
if (response.headers['content-type'] !== 'application/octet-stream') {
throw new Error(
`Unexpected content type ${response.headers['content-type']} received from ${plugin.downloadUrl}`,
);
}
const responseStream = response.data as fs.ReadStream;
const writeStream = responseStream.pipe(
fs.createWriteStream(targetFile, {autoClose: true}),
);
await new Promise((resolve, reject) =>
writeStream.once('finish', resolve).once('error', reject),
);
await installPluginFromFile(targetFile);
if (!store.getState().plugins.clientPlugins.has(plugin.id)) {
const pluginDefinition = requirePlugin(plugin);
dispatch(
registerPluginUpdate({
plugin: pluginDefinition,
enablePlugin: enableDownloadedPlugin,
}),
);
}
console.log(
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
);
dispatch(pluginDownloadSucceeded({plugin}));
} catch (error) {
console.error(
`Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
error,
);
dispatch(pluginDownloadFailed({plugin, error}));
} finally {
await fs.remove(targetDir);
}
}