From ff6f98fc0dd5be6a70191e2423f29c8fb4a6a05c Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Mon, 14 Aug 2023 11:33:06 -0700 Subject: [PATCH] Import File implementation Summary: Implementation was missing for the browser. This provides a default implementation. Reviewed By: aigoncharov Differential Revision: D48311198 fbshipit-source-id: fd067600f571234e0fbccfb90853b62f175ff8fb --- .../app/src/electron/initializeElectron.tsx | 1 - desktop/flipper-common/src/server-types.tsx | 4 +- .../src/plugin/FlipperLib.tsx | 6 +- .../flipper-plugin/src/ui/FileSelector.tsx | 21 +++--- .../src/FlipperServerImpl.tsx | 7 +- .../src/plugins/PluginManager.tsx | 6 +- desktop/flipper-ui-browser/package.json | 1 + .../src/initializeRenderHost.tsx | 72 ++++++++++++++++++- .../plugin-manager/PluginPackageInstaller.tsx | 19 ++--- desktop/plugin-lib/src/pluginInstaller.tsx | 4 +- .../request-mocking/NetworkRouteManager.tsx | 3 + .../public/shared_preferences/src/index.tsx | 40 ++++++----- 12 files changed, 130 insertions(+), 54 deletions(-) diff --git a/desktop/app/src/electron/initializeElectron.tsx b/desktop/app/src/electron/initializeElectron.tsx index 55974125b..c2e4311ca 100644 --- a/desktop/app/src/electron/initializeElectron.tsx +++ b/desktop/app/src/electron/initializeElectron.tsx @@ -134,7 +134,6 @@ export async function initializeElectron( return { data, name: fileName, - path: filePath, }; }), ); diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index a3b94e467..7596c0f6d 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -319,8 +319,8 @@ export type FlipperServerCommands = { name: string, ) => Promise; 'plugins-install-from-npm': (name: string) => Promise; - 'plugins-install-from-file': ( - path: string, + 'plugins-install-from-content': ( + contents: string, ) => Promise; 'plugins-remove-plugins': (names: string[]) => Promise; 'plugins-server-add-on-start': ( diff --git a/desktop/flipper-plugin-core/src/plugin/FlipperLib.tsx b/desktop/flipper-plugin-core/src/plugin/FlipperLib.tsx index 98dd29757..db28db2e8 100644 --- a/desktop/flipper-plugin-core/src/plugin/FlipperLib.tsx +++ b/desktop/flipper-plugin-core/src/plugin/FlipperLib.tsx @@ -32,12 +32,12 @@ import { import {CreatePasteArgs, CreatePasteResult} from './Paste'; import {Atom} from '../state/atom'; -export type FileEncoding = 'utf-8' | 'base64'; +export type FileEncoding = 'utf-8' | 'base64' | 'binary'; export interface FileDescriptor { - data: string; + data: string | Uint8Array | undefined; name: string; - path?: string; + encoding: FileEncoding; } export interface DownloadFileResponse extends DownloadFileStartResponse { diff --git a/desktop/flipper-plugin/src/ui/FileSelector.tsx b/desktop/flipper-plugin/src/ui/FileSelector.tsx index 3b70cf6ef..2842f864c 100644 --- a/desktop/flipper-plugin/src/ui/FileSelector.tsx +++ b/desktop/flipper-plugin/src/ui/FileSelector.tsx @@ -56,7 +56,7 @@ export type FileSelectorProps = { ); const formatFileDescriptor = (fileDescriptor?: FileDescriptor) => - fileDescriptor?.path || fileDescriptor?.name; + fileDescriptor?.name; export function FileSelector({ onChange, @@ -74,14 +74,8 @@ export function FileSelector({ const onSetFiles = async () => { setLoading(true); - let defaultPath: string | undefined = files[0]?.path ?? files[0]?.name; - if (multi) { - defaultPath = files[0]?.path; - } - try { const newFileSelection = await getFlipperLib().importFile?.({ - defaultPath, extensions, title: label, encoding, @@ -126,7 +120,7 @@ export function FileSelector({ droppedFiles.map(async (droppedFile) => { const raw = await droppedFile.arrayBuffer(); - let data: string; + let data: string | Uint8Array | undefined; switch (encoding) { case 'utf-8': { data = new TextDecoder().decode(raw); @@ -136,18 +130,19 @@ export function FileSelector({ data = fromUint8Array(new Uint8Array(raw)); break; } + case 'binary': + data = new Uint8Array(raw); + break; default: { assertNever(encoding); } } - const droppedFileDescriptor: FileDescriptor = { - data: data!, + return { + data, name: droppedFile.name, - // Electron "File" has "path" attribute - path: (droppedFile as any).path, + encoding, }; - return droppedFileDescriptor; }), ); diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index 511cd2003..23f41af14 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -521,8 +521,11 @@ export class FlipperServerImpl implements FlipperServer { this.pluginManager.downloadPlugin(details), 'plugins-get-updatable-plugins': (query) => this.pluginManager.getUpdatablePlugins(query), - 'plugins-install-from-file': (path) => - this.pluginManager.installPluginFromFile(path), + 'plugins-install-from-content': (contents) => { + const bytes = Base64.toUint8Array(contents); + const buffer = Buffer.from(bytes); + return this.pluginManager.installPluginFromFileOrBuffer(buffer); + }, 'plugins-install-from-marketplace': (name: string) => this.pluginManager.installPluginForMarketplace(name), 'plugins-install-from-npm': (name) => diff --git a/desktop/flipper-server-core/src/plugins/PluginManager.tsx b/desktop/flipper-server-core/src/plugins/PluginManager.tsx index 8ba356436..7baa08dee 100644 --- a/desktop/flipper-server-core/src/plugins/PluginManager.tsx +++ b/desktop/flipper-server-core/src/plugins/PluginManager.tsx @@ -28,7 +28,7 @@ import { getInstalledPlugins, getPluginVersionInstallationDir, getPluginDirNameFromPackageName, - installPluginFromFile, + installPluginFromFileOrBuffer, removePlugins, getUpdatablePlugins, getInstalledPlugin, @@ -71,7 +71,7 @@ export class PluginManager { removePlugins = removePlugins; getUpdatablePlugins = getUpdatablePlugins; getInstalledPlugin = getInstalledPlugin; - installPluginFromFile = installPluginFromFile; + installPluginFromFileOrBuffer = installPluginFromFileOrBuffer; installPluginFromNpm = installPluginFromNpm; async loadSource(path: string): Promise { @@ -186,7 +186,7 @@ export class PluginManager { await new Promise((resolve, reject) => writeStream.once('finish', resolve).once('error', reject), ); - return await installPluginFromFile(tmpFile); + return await installPluginFromFileOrBuffer(tmpFile); } } catch (error) { console.warn( diff --git a/desktop/flipper-ui-browser/package.json b/desktop/flipper-ui-browser/package.json index 70e146da5..6e25ac516 100644 --- a/desktop/flipper-ui-browser/package.json +++ b/desktop/flipper-ui-browser/package.json @@ -11,6 +11,7 @@ "bugs": "https://github.com/facebook/flipper/issues", "dependencies": { "file-saver": "^2.0.5", + "js-base64": "^3.7.5", "reconnecting-websocket": "^4.4.0" }, "devDependencies": { diff --git a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx index 1f5761442..e09d34468 100644 --- a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx +++ b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx @@ -16,6 +16,8 @@ import { import type {RenderHost} from 'flipper-ui-core'; import FileSaver from 'file-saver'; +import {Base64} from 'js-base64'; + declare module globalThis { let require: any; } @@ -31,6 +33,13 @@ globalThis.require = wrapRequire((module: string) => { ); }); +type FileEncoding = 'utf-8' | 'base64' | 'binary'; +interface FileDescriptor { + data: string | Uint8Array | undefined; + name: string; + encoding: FileEncoding; +} + export function initializeRenderHost( flipperServer: FlipperServer, flipperServerConfig: FlipperServerConfig, @@ -42,8 +51,67 @@ export function initializeRenderHost( writeTextToClipboard(text: string) { return navigator.clipboard.writeText(text); }, - async importFile() { - throw new Error('Not implemented'); + async importFile(options?: { + defaultPath?: string; + extensions?: string[]; + title?: string; + encoding?: FileEncoding; + multi?: false; + }) { + return new Promise( + (resolve, reject) => { + try { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + if (options?.extensions) { + fileInput.accept = options?.extensions.join(', '); + } + fileInput.multiple = options?.multi ?? false; + + fileInput.addEventListener('change', async (event) => { + const target = event.target as HTMLInputElement | undefined; + if (!target || !target.files) { + resolve(undefined); + return; + } + + const files: File[] = Array.from(target.files); + const descriptors: FileDescriptor[] = await Promise.all( + files.map(async (file) => { + switch (options?.encoding) { + case 'base64': { + const bytes = new Uint8Array(await file.arrayBuffer()); + const base64Content = Base64.fromUint8Array(bytes); + return { + data: base64Content, + name: file.name, + encoding: 'base64', + }; + } + case 'binary': + return { + data: new Uint8Array(await file.arrayBuffer()), + name: file.name, + encoding: 'binary', + }; + default: + return { + data: await file.text(), + name: file.name, + encoding: 'utf-8', + }; + } + }), + ); + resolve(options?.multi ? descriptors : descriptors[0]); + }); + + fileInput.click(); + } catch (error) { + reject(error); + } + }, + ); }, async exportFile(data: string, {defaultPath}: {defaultPath?: string}) { const file = new File([data], defaultPath ?? 'unknown', { diff --git a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx index 7295494e3..d76133d8e 100644 --- a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx +++ b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx @@ -43,17 +43,20 @@ export default function PluginPackageInstaller({ }: { onInstall: () => Promise; }) { - const [path, setPath] = useState(''); + const [content, setContent] = useState(); const [isPathValid, setIsPathValid] = useState(false); const [error, setError] = useState(); const [inProgress, setInProgress] = useState(false); const onClick = async () => { + if (!content) { + return; + } setError(undefined); setInProgress(true); try { - await getRenderHostInstance().flipperServer!.exec( - 'plugins-install-from-file', - path, + await getRenderHostInstance().flipperServer?.exec( + 'plugins-install-from-content', + content, ); await onInstall(); } catch (e) { @@ -83,13 +86,13 @@ export default function PluginPackageInstaller({ { + encoding="base64" + onChange={async (newFile) => { if (newFile) { - // TODO: Fix me before implementing Browser Flipper. "path" is only availbale in Electron! - setPath(newFile.path!); + setContent(newFile.data as string); setIsPathValid(true); } else { - setPath(''); + setContent(undefined); setIsPathValid(false); } setError(undefined); diff --git a/desktop/plugin-lib/src/pluginInstaller.tsx b/desktop/plugin-lib/src/pluginInstaller.tsx index eaa0bf16f..365a44d6d 100644 --- a/desktop/plugin-lib/src/pluginInstaller.tsx +++ b/desktop/plugin-lib/src/pluginInstaller.tsx @@ -108,8 +108,8 @@ export async function installPluginFromNpm(name: string) { } } -export async function installPluginFromFile( - packagePath: string, +export async function installPluginFromFileOrBuffer( + packagePath: string | Buffer, ): Promise { const tmpDir = await promisify(tmp.dir)(); try { diff --git a/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx b/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx index 24f0a2bd6..9322d0bc8 100644 --- a/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx +++ b/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx @@ -139,6 +139,9 @@ export function createNetworkManager( }) .then((res) => { if (res) { + if (res.encoding !== 'utf-8' || typeof res.data !== 'string') { + return; + } const importedRoutes = JSON.parse(res.data); importedRoutes?.forEach((importedRoute: Route) => { if (importedRoute != null) { diff --git a/desktop/plugins/public/shared_preferences/src/index.tsx b/desktop/plugins/public/shared_preferences/src/index.tsx index 4e898569d..a60dbd2b5 100644 --- a/desktop/plugins/public/shared_preferences/src/index.tsx +++ b/desktop/plugins/public/shared_preferences/src/index.tsx @@ -123,27 +123,31 @@ export function plugin(client: PluginClient) { } async function loadFromFile() { const file = await getFlipperLib().importFile(); - if (file?.path != undefined) { - const data = await getFlipperLib().remoteServerContext.fs.readFile( - file.path, - {encoding: 'utf-8'}, - ); - const preferences = JSON.parse(data) as SharedPreferencesEntry; - const name = selectedPreferences.get(); - if (name != null) { - updateSharedPreferences({ - name: name, - preferences: preferences.preferences, - }); - - for (const key in preferences.preferences) { - await client.send('setSharedPreference', { - sharedPreferencesName: name, - preferenceName: key, - preferenceValue: preferences.preferences[key], + if (file && file.encoding === 'utf-8' && typeof file.data === 'string') { + try { + const preferences = JSON.parse(file.data) as SharedPreferencesEntry; + const name = selectedPreferences.get(); + if (name != null) { + updateSharedPreferences({ + name: name, + preferences: preferences.preferences, }); + + for (const key in preferences.preferences) { + await client.send('setSharedPreference', { + sharedPreferencesName: name, + preferenceName: key, + preferenceValue: preferences.preferences[key], + }); + } } + } catch (e) { + console.warn('Unable to import shared preferences', e); } + } else { + console.warn( + 'The loaded file either has wrong encoding or is not a valid json file', + ); } }