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 {
|
return {
|
||||||
data,
|
data,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
path: filePath,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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': (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -123,27 +123,31 @@ 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 name = selectedPreferences.get();
|
||||||
);
|
if (name != null) {
|
||||||
const preferences = JSON.parse(data) as SharedPreferencesEntry;
|
updateSharedPreferences({
|
||||||
const name = selectedPreferences.get();
|
name: name,
|
||||||
if (name != null) {
|
preferences: preferences.preferences,
|
||||||
updateSharedPreferences({
|
|
||||||
name: name,
|
|
||||||
preferences: preferences.preferences,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const key in preferences.preferences) {
|
|
||||||
await client.send('setSharedPreference', {
|
|
||||||
sharedPreferencesName: name,
|
|
||||||
preferenceName: key,
|
|
||||||
preferenceValue: preferences.preferences[key],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user