diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 2bcbefe6a7b..a4f9961b9c9 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -11,7 +11,9 @@ export function createProtocol( runTimeParameterValues?: RunTimeParameterCreateData ): ResponsePromise { const formData = new FormData() - files.forEach(file => formData.append('files', file, file.name)) + files.forEach(file => { + formData.append('files', file, file.name) + }) if (protocolKey != null) formData.append('key', protocolKey) if (runTimeParameterValues != null) formData.append( diff --git a/app-shell/src/protocol-storage/index.ts b/app-shell/src/protocol-storage/index.ts index 7c202e9be92..53ec7148861 100644 --- a/app-shell/src/protocol-storage/index.ts +++ b/app-shell/src/protocol-storage/index.ts @@ -1,7 +1,6 @@ import fse from 'fs-extra' import path from 'path' import { shell } from 'electron' -import first from 'lodash/first' import { ADD_PROTOCOL, @@ -48,18 +47,6 @@ export const getParsedAnalysisFromPath = ( } } -export const getProtocolSrcFilePaths = ( - protocolKey: string -): Promise => { - const protocolDir = `${FileSystem.PROTOCOLS_DIRECTORY_PATH}/${protocolKey}` - return ensureDir(protocolDir) - .then(() => FileSystem.parseProtocolDirs([protocolDir])) - .then(storedProtocols => { - const storedProtocol = first(storedProtocols) - return storedProtocol?.srcFilePaths ?? [] - }) -} - // Revert a v7.0.0 pre-parity stop-gap solution. const migrateProtocolsFromTempDirectory = preParityMigrateProtocolsFrom( FileSystem.PRE_V7_PARITY_DIRECTORY_PATH, diff --git a/app-shell/src/usb.ts b/app-shell/src/usb.ts index d9edd69ef25..accdf5c00d7 100644 --- a/app-shell/src/usb.ts +++ b/app-shell/src/usb.ts @@ -1,8 +1,6 @@ import { ipcMain, IpcMainInvokeEvent } from 'electron' import axios, { AxiosRequestConfig } from 'axios' import FormData from 'form-data' -import fs from 'fs' -import path from 'path' import { fetchSerialPortList, @@ -12,7 +10,6 @@ import { } from '@opentrons/usb-bridge/node-client' import { createLogger } from './log' -import { getProtocolSrcFilePaths } from './protocol-storage' import { usbRequestsStart, usbRequestsStop } from './config/actions' import { SYSTEM_INFO_INITIALIZED, @@ -20,6 +17,7 @@ import { USB_DEVICE_REMOVED, } from './constants' +import type { IPCSafeFormData } from '@opentrons/app/src/redux/shell/types' import type { UsbDevice } from '@opentrons/app/src/redux/system-info/types' import type { PortInfo } from '@opentrons/usb-bridge/node-client' import type { Action, Dispatch } from './types' @@ -83,6 +81,17 @@ function isUsbDeviceOt3(device: UsbDevice): boolean { device.vendorId === parseInt(DEFAULT_VENDOR_ID, 16) ) } + +function reconstructFormData(ipcSafeFormData: IPCSafeFormData): FormData { + const result = new FormData() + ipcSafeFormData.forEach(entry => { + entry.type === 'file' + ? result.append(entry.name, Buffer.from(entry.value), entry.filename) + : result.append(entry.name, entry.value) + }) + return result +} + async function usbListener( _event: IpcMainInvokeEvent, config: AxiosRequestConfig @@ -92,21 +101,9 @@ async function usbListener( let formHeaders = {} // check for formDataProxy - if (data?.formDataProxy != null) { + if (data?.proxiedFormData != null) { // reconstruct FormData - const formData = new FormData() - const { protocolKey } = data.formDataProxy - - const srcFilePaths: string[] = await getProtocolSrcFilePaths(protocolKey) - - // create readable stream from file - srcFilePaths.forEach(srcFilePath => { - const readStream = fs.createReadStream(srcFilePath) - formData.append('files', readStream, path.basename(srcFilePath)) - }) - - formData.append('key', protocolKey) - + const formData = reconstructFormData(data.proxiedFormData) formHeaders = formData.getHeaders() data = formData } diff --git a/app/src/redux/shell/remote.ts b/app/src/redux/shell/remote.ts index 18508789ada..5717e5bdeaf 100644 --- a/app/src/redux/shell/remote.ts +++ b/app/src/redux/shell/remote.ts @@ -1,7 +1,11 @@ // access main process remote modules via attachments to `global` -import type { AxiosRequestConfig } from 'axios' -import type { ResponsePromise } from '@opentrons/api-client' -import type { Remote, NotifyTopic, NotifyResponseData } from './types' +import type { AxiosRequestConfig, AxiosResponse } from 'axios' +import type { + Remote, + NotifyTopic, + NotifyResponseData, + IPCSafeFormData, +} from './types' const emptyRemote: Remote = {} as any @@ -20,18 +24,40 @@ export const remote: Remote = new Proxy(emptyRemote, { }, }) -export function appShellRequestor( +// FormData and File objects can't be sent through invoke(). +// This converts them into simpler objects that can be. +// app-shell will convert them back. +async function proxyFormData(formData: FormData): Promise { + const result: IPCSafeFormData = [] + for (const [name, value] of formData.entries()) { + if (value instanceof File) { + result.push({ + type: 'file', + name, + // todo(mm, 2024-04-24): Send just the (full) filename instead of the file + // contents, to avoid the IPC message ballooning into several MB. + value: await value.arrayBuffer(), + filename: value.name, + }) + } else { + result.push({ type: 'string', name, value }) + } + } + + return result +} + +export async function appShellRequestor( config: AxiosRequestConfig -): ResponsePromise { +): Promise> { const { data } = config - // special case: protocol files and form data cannot be sent through invoke. proxy by protocolKey and handle in app-shell const formDataProxy = data instanceof FormData - ? { formDataProxy: { protocolKey: data.get('key') } } + ? { proxiedFormData: await proxyFormData(data) } : data const configProxy = { ...config, data: formDataProxy } - return remote.ipcRenderer.invoke('usb:request', configProxy) + return await remote.ipcRenderer.invoke('usb:request', configProxy) } interface CallbackStore { diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index b051ec43cbc..b16ac481e3d 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -164,3 +164,18 @@ export type ShellAction = | RobotMassStorageDeviceEnumerated | RobotMassStorageDeviceRemoved | NotifySubscribeAction + +export type IPCSafeFormDataEntry = + | { + type: 'string' + name: string + value: string + } + | { + type: 'file' + name: string + value: ArrayBuffer + filename: string + } + +export type IPCSafeFormData = IPCSafeFormDataEntry[]