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:
committed by
Facebook GitHub Bot
parent
ab441d8226
commit
97d37abbb2
129
desktop/app/src/dispatcher/pluginDownloads.tsx
Normal file
129
desktop/app/src/dispatcher/pluginDownloads.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user