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
This commit is contained in:
Lorenzo Blasa
2023-08-14 11:33:06 -07:00
committed by Facebook GitHub Bot
parent 2f5f4911e5
commit ff6f98fc0d
12 changed files with 130 additions and 54 deletions

View File

@@ -134,7 +134,6 @@ export async function initializeElectron(
return { return {
data, data,
name: fileName, name: fileName,
path: filePath,
}; };
}), }),
); );

View File

@@ -319,8 +319,8 @@ export type FlipperServerCommands = {
name: string, name: string,
) => Promise<InstalledPluginDetails>; ) => Promise<InstalledPluginDetails>;
'plugins-install-from-npm': (name: string) => Promise<InstalledPluginDetails>; 'plugins-install-from-npm': (name: string) => Promise<InstalledPluginDetails>;
'plugins-install-from-file': ( 'plugins-install-from-content': (
path: string, contents: string,
) => Promise<InstalledPluginDetails>; ) => Promise<InstalledPluginDetails>;
'plugins-remove-plugins': (names: string[]) => Promise<void>; 'plugins-remove-plugins': (names: string[]) => Promise<void>;
'plugins-server-add-on-start': ( 'plugins-server-add-on-start': (

View File

@@ -32,12 +32,12 @@ import {
import {CreatePasteArgs, CreatePasteResult} from './Paste'; import {CreatePasteArgs, CreatePasteResult} from './Paste';
import {Atom} from '../state/atom'; import {Atom} from '../state/atom';
export type FileEncoding = 'utf-8' | 'base64'; export type FileEncoding = 'utf-8' | 'base64' | 'binary';
export interface FileDescriptor { export interface FileDescriptor {
data: string; data: string | Uint8Array | undefined;
name: string; name: string;
path?: string; encoding: FileEncoding;
} }
export interface DownloadFileResponse extends DownloadFileStartResponse { export interface DownloadFileResponse extends DownloadFileStartResponse {

View File

@@ -56,7 +56,7 @@ export type FileSelectorProps = {
); );
const formatFileDescriptor = (fileDescriptor?: FileDescriptor) => const formatFileDescriptor = (fileDescriptor?: FileDescriptor) =>
fileDescriptor?.path || fileDescriptor?.name; fileDescriptor?.name;
export function FileSelector({ export function FileSelector({
onChange, onChange,
@@ -74,14 +74,8 @@ export function FileSelector({
const onSetFiles = async () => { const onSetFiles = async () => {
setLoading(true); setLoading(true);
let defaultPath: string | undefined = files[0]?.path ?? files[0]?.name;
if (multi) {
defaultPath = files[0]?.path;
}
try { try {
const newFileSelection = await getFlipperLib().importFile?.({ const newFileSelection = await getFlipperLib().importFile?.({
defaultPath,
extensions, extensions,
title: label, title: label,
encoding, encoding,
@@ -126,7 +120,7 @@ export function FileSelector({
droppedFiles.map(async (droppedFile) => { droppedFiles.map(async (droppedFile) => {
const raw = await droppedFile.arrayBuffer(); const raw = await droppedFile.arrayBuffer();
let data: string; let data: string | Uint8Array | undefined;
switch (encoding) { switch (encoding) {
case 'utf-8': { case 'utf-8': {
data = new TextDecoder().decode(raw); data = new TextDecoder().decode(raw);
@@ -136,18 +130,19 @@ export function FileSelector({
data = fromUint8Array(new Uint8Array(raw)); data = fromUint8Array(new Uint8Array(raw));
break; break;
} }
case 'binary':
data = new Uint8Array(raw);
break;
default: { default: {
assertNever(encoding); assertNever(encoding);
} }
} }
const droppedFileDescriptor: FileDescriptor = { return {
data: data!, data,
name: droppedFile.name, name: droppedFile.name,
// Electron "File" has "path" attribute encoding,
path: (droppedFile as any).path,
}; };
return droppedFileDescriptor;
}), }),
); );

View File

@@ -521,8 +521,11 @@ export class FlipperServerImpl implements FlipperServer {
this.pluginManager.downloadPlugin(details), this.pluginManager.downloadPlugin(details),
'plugins-get-updatable-plugins': (query) => 'plugins-get-updatable-plugins': (query) =>
this.pluginManager.getUpdatablePlugins(query), this.pluginManager.getUpdatablePlugins(query),
'plugins-install-from-file': (path) => 'plugins-install-from-content': (contents) => {
this.pluginManager.installPluginFromFile(path), const bytes = Base64.toUint8Array(contents);
const buffer = Buffer.from(bytes);
return this.pluginManager.installPluginFromFileOrBuffer(buffer);
},
'plugins-install-from-marketplace': (name: string) => 'plugins-install-from-marketplace': (name: string) =>
this.pluginManager.installPluginForMarketplace(name), this.pluginManager.installPluginForMarketplace(name),
'plugins-install-from-npm': (name) => 'plugins-install-from-npm': (name) =>

View File

@@ -28,7 +28,7 @@ import {
getInstalledPlugins, getInstalledPlugins,
getPluginVersionInstallationDir, getPluginVersionInstallationDir,
getPluginDirNameFromPackageName, getPluginDirNameFromPackageName,
installPluginFromFile, installPluginFromFileOrBuffer,
removePlugins, removePlugins,
getUpdatablePlugins, getUpdatablePlugins,
getInstalledPlugin, getInstalledPlugin,
@@ -71,7 +71,7 @@ export class PluginManager {
removePlugins = removePlugins; removePlugins = removePlugins;
getUpdatablePlugins = getUpdatablePlugins; getUpdatablePlugins = getUpdatablePlugins;
getInstalledPlugin = getInstalledPlugin; getInstalledPlugin = getInstalledPlugin;
installPluginFromFile = installPluginFromFile; installPluginFromFileOrBuffer = installPluginFromFileOrBuffer;
installPluginFromNpm = installPluginFromNpm; installPluginFromNpm = installPluginFromNpm;
async loadSource(path: string): Promise<PluginSource> { async loadSource(path: string): Promise<PluginSource> {
@@ -186,7 +186,7 @@ export class PluginManager {
await new Promise((resolve, reject) => await new Promise((resolve, reject) =>
writeStream.once('finish', resolve).once('error', reject), writeStream.once('finish', resolve).once('error', reject),
); );
return await installPluginFromFile(tmpFile); return await installPluginFromFileOrBuffer(tmpFile);
} }
} catch (error) { } catch (error) {
console.warn( console.warn(

View File

@@ -11,6 +11,7 @@
"bugs": "https://github.com/facebook/flipper/issues", "bugs": "https://github.com/facebook/flipper/issues",
"dependencies": { "dependencies": {
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"js-base64": "^3.7.5",
"reconnecting-websocket": "^4.4.0" "reconnecting-websocket": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -16,6 +16,8 @@ import {
import type {RenderHost} from 'flipper-ui-core'; import type {RenderHost} from 'flipper-ui-core';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import {Base64} from 'js-base64';
declare module globalThis { declare module globalThis {
let require: any; 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( export function initializeRenderHost(
flipperServer: FlipperServer, flipperServer: FlipperServer,
flipperServerConfig: FlipperServerConfig, flipperServerConfig: FlipperServerConfig,
@@ -42,8 +51,67 @@ export function initializeRenderHost(
writeTextToClipboard(text: string) { writeTextToClipboard(text: string) {
return navigator.clipboard.writeText(text); return navigator.clipboard.writeText(text);
}, },
async importFile() { async importFile(options?: {
throw new Error('Not implemented'); defaultPath?: string;
extensions?: string[];
title?: string;
encoding?: FileEncoding;
multi?: false;
}) {
return new Promise<FileDescriptor | FileDescriptor[] | undefined>(
(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}) { async exportFile(data: string, {defaultPath}: {defaultPath?: string}) {
const file = new File([data], defaultPath ?? 'unknown', { const file = new File([data], defaultPath ?? 'unknown', {

View File

@@ -43,17 +43,20 @@ export default function PluginPackageInstaller({
}: { }: {
onInstall: () => Promise<void>; onInstall: () => Promise<void>;
}) { }) {
const [path, setPath] = useState(''); const [content, setContent] = useState<string | undefined>();
const [isPathValid, setIsPathValid] = useState(false); const [isPathValid, setIsPathValid] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [inProgress, setInProgress] = useState(false); const [inProgress, setInProgress] = useState(false);
const onClick = async () => { const onClick = async () => {
if (!content) {
return;
}
setError(undefined); setError(undefined);
setInProgress(true); setInProgress(true);
try { try {
await getRenderHostInstance().flipperServer!.exec( await getRenderHostInstance().flipperServer?.exec(
'plugins-install-from-file', 'plugins-install-from-content',
path, content,
); );
await onInstall(); await onInstall();
} catch (e) { } catch (e) {
@@ -83,13 +86,13 @@ export default function PluginPackageInstaller({
<Toolbar> <Toolbar>
<FileSelector <FileSelector
label="Select a Flipper package or just drag and drop it here..." label="Select a Flipper package or just drag and drop it here..."
onChange={(newFile) => { encoding="base64"
onChange={async (newFile) => {
if (newFile) { if (newFile) {
// TODO: Fix me before implementing Browser Flipper. "path" is only availbale in Electron! setContent(newFile.data as string);
setPath(newFile.path!);
setIsPathValid(true); setIsPathValid(true);
} else { } else {
setPath(''); setContent(undefined);
setIsPathValid(false); setIsPathValid(false);
} }
setError(undefined); setError(undefined);

View File

@@ -108,8 +108,8 @@ export async function installPluginFromNpm(name: string) {
} }
} }
export async function installPluginFromFile( export async function installPluginFromFileOrBuffer(
packagePath: string, packagePath: string | Buffer,
): Promise<InstalledPluginDetails> { ): Promise<InstalledPluginDetails> {
const tmpDir = await promisify(tmp.dir)(); const tmpDir = await promisify(tmp.dir)();
try { try {

View File

@@ -139,6 +139,9 @@ export function createNetworkManager(
}) })
.then((res) => { .then((res) => {
if (res) { if (res) {
if (res.encoding !== 'utf-8' || typeof res.data !== 'string') {
return;
}
const importedRoutes = JSON.parse(res.data); const importedRoutes = JSON.parse(res.data);
importedRoutes?.forEach((importedRoute: Route) => { importedRoutes?.forEach((importedRoute: Route) => {
if (importedRoute != null) { if (importedRoute != null) {

View File

@@ -123,12 +123,9 @@ export function plugin(client: PluginClient<Events, Methods>) {
} }
async function loadFromFile() { async function loadFromFile() {
const file = await getFlipperLib().importFile(); const file = await getFlipperLib().importFile();
if (file?.path != undefined) { if (file && file.encoding === 'utf-8' && typeof file.data === 'string') {
const data = await getFlipperLib().remoteServerContext.fs.readFile( try {
file.path, const preferences = JSON.parse(file.data) as SharedPreferencesEntry;
{encoding: 'utf-8'},
);
const preferences = JSON.parse(data) as SharedPreferencesEntry;
const name = selectedPreferences.get(); const name = selectedPreferences.get();
if (name != null) { if (name != null) {
updateSharedPreferences({ updateSharedPreferences({
@@ -144,6 +141,13 @@ export function plugin(client: PluginClient<Events, Methods>) {
}); });
} }
} }
} 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',
);
} }
} }