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:
committed by
Facebook GitHub Bot
parent
2f5f4911e5
commit
ff6f98fc0d
@@ -134,7 +134,6 @@ export async function initializeElectron(
|
||||
return {
|
||||
data,
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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': (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user