Files
flipper/desktop/app/src/dispatcher/pluginDownloads.tsx
Anton Nikolaev c3d61cc32d Persist uninstalled plugins list
Summary: This diff changes uninstallation procedure for plugins. Instead of deleting plugin files immediately we are keeping them, but mark them as "uninstalled". This makes it possible to re-install plugins quickly in case when user clicked "delete" by mistake.

Reviewed By: mweststrate

Differential Revision: D25493479

fbshipit-source-id: 9ff29d717cdd5401c55388f24d479599579c8dd3
2020-12-15 09:31:56 -08:00

136 lines
4.5 KiB
TypeScript

/**
* 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}),
);
if (await fs.pathExists(dir)) {
console.log(
`Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${dir}"`,
);
} else {
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);
}
}