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 {
data,
name: fileName,
path: filePath,
};
}),
);

View File

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

View File

@@ -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 {

View File

@@ -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;
}),
);

View File

@@ -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) =>

View File

@@ -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<PluginSource> {
@@ -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(

View File

@@ -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": {

View File

@@ -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<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}) {
const file = new File([data], defaultPath ?? 'unknown', {

View File

@@ -43,17 +43,20 @@ export default function PluginPackageInstaller({
}: {
onInstall: () => Promise<void>;
}) {
const [path, setPath] = useState('');
const [content, setContent] = useState<string | undefined>();
const [isPathValid, setIsPathValid] = useState(false);
const [error, setError] = useState<Error>();
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({
<Toolbar>
<FileSelector
label="Select a Flipper package or just drag and drop it here..."
onChange={(newFile) => {
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);

View File

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

View File

@@ -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) {

View File

@@ -123,12 +123,9 @@ export function plugin(client: PluginClient<Events, Methods>) {
}
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;
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({
@@ -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',
);
}
}