diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js
index 797771bbb..d976a3e79 100644
--- a/packages/bruno-app/src/components/FilePickerEditor/index.js
+++ b/packages/bruno-app/src/components/FilePickerEditor/index.js
@@ -6,7 +6,7 @@ import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
-const FilePickerEditor = ({ value, onChange, collection }) => {
+const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => {
value = value || [];
const dispatch = useDispatch();
const filenames = value
@@ -20,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
- dispatch(browseFiles())
+ dispatch(browseFiles([],['']))
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -49,7 +49,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
if (filenames.length == 1) {
return filenames[0];
}
- return filenames.length + ' files selected';
+ return filenames.length + ' file(s) selected';
};
return filenames.length > 0 ? (
@@ -66,7 +66,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
) : (
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js
new file mode 100644
index 000000000..35adfcc1f
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js
@@ -0,0 +1,65 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-weight: 600;
+ table-layout: fixed;
+
+ thead,
+ td {
+ border: 1px solid ${(props) => props.theme.table.border};
+ }
+
+ thead {
+ color: ${(props) => props.theme.table.thead.color};
+ font-size: 0.8125rem;
+ user-select: none;
+ }
+ td {
+ padding: 6px 10px;
+
+ &:nth-child(1) {
+ width: 30%;
+ }
+
+ &:nth-child(2) {
+ width: 45%;
+ }
+
+ &:nth-child(3) {
+ width: 25%;
+ }
+
+ &:nth-child(4) {
+ width: 70px;
+ }
+ }
+ }
+
+ .btn-add-param {
+ font-size: 0.8125rem;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ border: solid 1px transparent;
+ outline: none !important;
+ color: ${(props) => props.theme.table.input.color};
+ background: transparent;
+
+ &:focus {
+ outline: none !important;
+ border: solid 1px transparent;
+ }
+ }
+
+ input[type='radio'] {
+ cursor: pointer;
+ position: relative;
+ top: 1px;
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/Binary/index.js
new file mode 100644
index 000000000..77bbda8d5
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Binary/index.js
@@ -0,0 +1,173 @@
+import React from 'react';
+import get from 'lodash/get';
+import cloneDeep from 'lodash/cloneDeep';
+import { IconTrash } from '@tabler/icons';
+import { useDispatch } from 'react-redux';
+import { useTheme } from 'providers/Theme';
+import {
+ addBinaryFile,
+ updateBinaryFile,
+ deleteBinaryFile
+} from 'providers/ReduxStore/slices/collections';
+import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import StyledWrapper from './StyledWrapper';
+import FilePickerEditor from 'components/FilePickerEditor';
+import SingleLineEditor from 'components/SingleLineEditor/index';
+import { isArray } from 'lodash';
+import path from 'node:path';
+import { useState } from 'react';
+
+const Binary = ({ item, collection }) => {
+ const dispatch = useDispatch();
+ const { storedTheme } = useTheme();
+ const params = item.draft ? get(item, 'draft.request.body.binaryFile') : get(item, 'request.body.binaryFile');
+
+ const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
+
+ const addFile = () => {
+ dispatch(
+ addBinaryFile({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ type: 'binaryFile',
+ value: [''],
+ })
+ );
+ };
+
+ const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
+ const handleRun = () => dispatch(sendRequest(item, collection.uid));
+
+ const handleParamChange = (e, _param, type) => {
+
+ const param = cloneDeep(_param);
+
+ switch (type) {
+
+ case 'value': {
+ param.value = isArray(e.target.value) && e.target.value.length > 0 ? e.target.value : [''];
+ param.name = param.value.length === 0 ? '': path.basename(param.value[0], path.extname(param.value[0]));
+ break;
+ }
+ case 'contentType': {
+ param.contentType = e.target.value;
+ break;
+ }
+ case 'enabled': {
+ param.enabled = e.target.checked;
+
+ setEnableFileUid(param.uid);
+
+ break;
+ }
+ }
+ dispatch(
+ updateBinaryFile({
+ param: param,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ const handleRemoveParams = (param) => {
+ dispatch(
+ deleteBinaryFile({
+ paramUid: param.uid,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ return (
+
+
+
+
+ File |
+ Content-Type |
+ Enabled |
+ |
+
+
+
+ {params && params.length
+ ? params.map((param, index) => {
+ return (
+
+ |
+
+ handleParamChange(
+ {
+ target: {
+ value: newValue
+ }
+ },
+ param,
+ 'value'
+ )
+ }
+ collection={collection}
+ />
+ |
+
+
+ handleParamChange(
+ {
+ target: {
+ value: newValue
+ }
+ },
+ param,
+ 'contentType'
+ )
+ }
+ onRun={handleRun}
+ collection={collection}
+ />
+ |
+
+
+ handleParamChange(e, param, 'enabled')}
+ />
+
+ |
+
+
+
+
+ |
+
+ );
+ })
+ : null}
+
+
+
+
+
+
+ );
+};
+export default Binary;
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
index 29b66d58d..95b3b6a55 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
@@ -128,6 +128,15 @@ const RequestBodyMode = ({ item, collection }) => {
SPARQL
Other
+ {
+ dropdownTippyRef.current.hide();
+ onModeChange('binaryFile');
+ }}
+ >
+ Binary File
+
{
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
index ca60c8662..9a71a4ac3 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
@@ -8,6 +8,7 @@ import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
+import Binary from '../Binary/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => {
);
}
+ if (bodyMode === 'binaryFile') {
+ return
+ }
+
if (bodyMode === 'formUrlEncoded') {
return ;
}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 75c6f2cb9..0737cb133 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -759,7 +759,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
xml: null,
sparql: null,
multipartForm: null,
- formUrlEncoded: null
+ formUrlEncoded: null,
+ binaryFile: null
},
auth: auth ?? {
mode: 'none'
@@ -1039,12 +1040,12 @@ export const browseDirectory = () => (dispatch, getState) => {
};
export const browseFiles =
- (filters = []) =>
+ (filters = [], properties = ['multiSelections']) =>
(dispatch, getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
- ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).then(resolve).catch(reject);
});
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 11f12026f..35ccf244c 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -18,6 +18,8 @@ import {
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
import toast from 'react-hot-toast';
+import mime from 'mime-types';
+import path from 'node:path';
const initialState = {
collections: [],
@@ -863,25 +865,89 @@ export const collectionsSlice = createSlice({
}
}
},
- moveMultipartFormParam: (state, action) => {
+ moveMultipartFormParam: (state, action) => {
+ // Ensure item.draft is a deep clone of item if not already present
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ // Extract payload data
+ const { updateReorderedItem } = action.payload;
+ const params = item.draft.request.body.multipartForm;
+
+ item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {
+ return params.find((param) => param.uid === uid);
+ });
+ },
+ addBinaryFile: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+ item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || [];
+
+ item.draft.request.body.binaryFile.push({
+ uid: uuid(),
+ type: action.payload.type,
+ name: '',
+ value: [''],
+ contentType: '',
+ enabled: false
+ });
+ }
+ }
+ },
+ updateBinaryFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
- // Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
- // Extract payload data
- const { updateReorderedItem } = action.payload;
- const params = item.draft.request.body.multipartForm;
-
- item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {
- return params.find((param) => param.uid === uid);
+ item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => {
+ p.enabled = false;
+ return p;
});
+
+ const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid);
+
+ if (param) {
+
+ const contentType = mime.contentType(path.extname(action.payload.param.value[0]));
+
+ param.type = action.payload.param.type;
+ param.name = action.payload.param.name;
+ param.value = action.payload.param.value;
+ param.contentType = action.payload.param.contentType || contentType || '';
+ param.enabled = action.payload.param.enabled;
+ }
+ }
+ }
+ },
+ deleteBinaryFile: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ item.draft.request.body.binaryFile = filter(
+ item.draft.request.body.binaryFile,
+ (p) => p.uid !== action.payload.paramUid
+ );
}
}
},
@@ -941,6 +1007,10 @@ export const collectionsSlice = createSlice({
item.draft.request.body.sparql = action.payload.content;
break;
}
+ case 'binaryFile': {
+ item.draft.request.body.binaryFile = action.payload.content;
+ break;
+ }
case 'formUrlEncoded': {
item.draft.request.body.formUrlEncoded = action.payload.content;
break;
@@ -1933,6 +2003,9 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
+ addBinaryFile,
+ updateBinaryFile,
+ deleteBinaryFile,
moveMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index 479fcd67a..19e4ea4de 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -14,6 +14,8 @@ const createContentType = (mode) => {
return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
+ case 'binaryFile':
+ return 'application/octet-stream';
default:
return '';
}
@@ -60,26 +62,48 @@ const createPostData = (body, type) => {
}
const contentType = createContentType(body.mode);
- if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
- return {
- mimeType: contentType,
- params: body[body.mode]
- .filter((param) => param.enabled)
- .map((param) => ({
- name: param.name,
- value: param.value,
- ...(param.type === 'file' && { fileName: param.value })
- }))
- };
- } else {
- return {
- mimeType: contentType,
- text: body[body.mode]
- };
+
+ switch (body.mode) {
+ case 'formUrlEncoded':
+ case 'multipartForm':
+ return {
+ mimeType: contentType,
+ params: body[body.mode]
+ .filter((param) => param.enabled)
+ .map((param) => ({
+ name: param.name,
+ value: param.value,
+ ...(param.type === 'file' && { fileName: param.value })
+ }))
+ };
+ case 'binaryFile':
+ const binary = {
+ mimeType: 'application/octet-stream',
+ // mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
+ params: body[body.mode]
+ .filter((param) => param.enabled)
+ .map((param) => ({
+ name: param.name,
+ value: param.value,
+ fileName: param.value
+ }))
+ };
+
+ console.log('curl-binary', binary);
+ return binary;
+ default:
+ return {
+ mimeType: contentType,
+ text: body[body.mode]
+ };
}
};
export const buildHarRequest = ({ request, headers, type }) => {
+
+ console.log('buildHarRequest', request, headers, type);
+
+ console.log('buildHarRequest-postData', createPostData(request.body, type));
return {
method: request.method,
url: encodeURI(request.url),
@@ -89,6 +113,7 @@ export const buildHarRequest = ({ request, headers, type }) => {
queryString: createQuery(request.params),
postData: createPostData(request.body, type),
headersSize: 0,
- bodySize: 0
+ bodySize: 0,
+ binary: true
};
};
diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js
index 5ef7b1b49..b7ca9f5ba 100644
--- a/packages/bruno-app/src/utils/collections/export.js
+++ b/packages/bruno-app/src/utils/collections/export.js
@@ -14,6 +14,7 @@ export const deleteUidsInItems = (items) => {
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
+ each(get(item, 'request.body.binaryFile'), (param) => delete param.uid);
}
if (item.items && item.items.length) {
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index bc6c731f4..96e91b78a 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -271,6 +271,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
});
};
+ const copyBinaryFileParams = (params = []) => {
+ return map(params, (param) => {
+ return {
+ uid: param.uid,
+ type: param.type,
+ name: param.name,
+ value: param.value,
+ contentType: param.contentType,
+ enabled: param.enabled
+ }
+ });
+ }
+
const copyItems = (sourceItems, destItems) => {
each(sourceItems, (si) => {
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
@@ -298,7 +311,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
graphql: si.request.body.graphql,
sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
- multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
+ multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
+ binaryFile: copyBinaryFileParams(si.request.body.binaryFile)
},
script: si.request.script,
vars: si.request.vars,
@@ -651,6 +665,10 @@ export const humanizeRequestBodyMode = (mode) => {
label = 'SPARQL';
break;
}
+ case 'binaryFile': {
+ label = 'Binary File';
+ break;
+ }
case 'formUrlEncoded': {
label = 'Form URL Encoded';
break;
@@ -751,6 +769,7 @@ export const refreshUidsInItem = (item) => {
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
+ each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid()));
return item;
};
@@ -761,11 +780,13 @@ export const deleteUidsInItem = (item) => {
const headers = get(item, 'request.headers', []);
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
+ const binaryFile = get(item, 'request.body.binaryFile', []);
params.forEach((param) => delete param.uid);
headers.forEach((header) => delete header.uid);
bodyFormUrlEncoded.forEach((param) => delete param.uid);
bodyMultipartForm.forEach((param) => delete param.uid);
+ binaryFile.forEach((param) => delete param.uid);
return item;
};
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js
index c1398ab14..5a2c62022 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.js
@@ -9,6 +9,7 @@
import parseCurlCommand from './parse-curl';
import * as querystring from 'query-string';
import * as jsesc from 'jsesc';
+import * as path from 'path';
function getContentType(headers = {}) {
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
@@ -99,9 +100,36 @@ function getMultipleDataString(request, parsedQueryString) {
function getFilesString(request) {
const data = {};
- data.files = {};
data.data = {};
+ if (request.isDataBinary){
+
+ let filePath = ''
+
+ if(request.data.startsWith('@')){
+ filePath = request.data.slice(1);
+ }else{
+ filePath = request.data;
+ }
+
+ const fileName = path.basename(filePath);
+
+ data.data = [
+ {
+ name: repr(fileName),
+ value: [repr(filePath)],
+ enabled: true,
+ contentType: request.headers['Content-Type'],
+ type: 'binaryFile'
+ }
+ ];
+
+ return data;
+
+ }
+
+ data.files = {};
+
for (const multipartKey in request.multipartUploads) {
const multipartValue = request.multipartUploads[multipartKey];
if (multipartValue.startsWith('@')) {
@@ -140,6 +168,7 @@ const curlToJson = (curlCommand) => {
requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
+ requestJson.isDataBinary = request.isDataBinary;
if (request.cookies) {
const cookies = {};
@@ -163,11 +192,11 @@ const curlToJson = (curlCommand) => {
requestJson.queries = getQueries(request);
}
- if (typeof request.data === 'string' || typeof request.data === 'number') {
- Object.assign(requestJson, getDataString(request));
- } else if (request.multipartUploads) {
+ else if (request.multipartUploads || request.isDataBinary) {
Object.assign(requestJson, getFilesString(request));
- }
+ } else if (typeof request.data === 'string' || typeof request.data === 'number') {
+ Object.assign(requestJson, getDataString(request));
+ }
if (request.insecure) {
requestJson.insecure = false;
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
index 2d9785154..6f8206139 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
@@ -86,4 +86,40 @@ describe('curlToJson', () => {
method: 'get'
});
});
+
+ it('should return a parse a curl with a post body with binary file type', () => {
+ const curlCommand = `curl 'https://www.usebruno.com'
+ -H 'Accept: application/json, text/plain, */*'
+ -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
+ -H 'Content-Type: application/json;charset=utf-8'
+ -H 'Origin: https://www.usebruno.com'
+ -H 'Referer: https://www.usebruno.com/'
+ --data-binary '@/path/to/file'
+ `;
+
+ const result = curlToJson(curlCommand);
+
+ expect(result).toEqual({
+ url: 'https://www.usebruno.com',
+ raw_url: 'https://www.usebruno.com',
+ method: 'post',
+ headers: {
+ Accept: 'application/json, text/plain, */*',
+ 'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
+ 'Content-Type': 'application/json;charset=utf-8',
+ Origin: 'https://www.usebruno.com',
+ Referer: 'https://www.usebruno.com/'
+ },
+ isDataBinary: true,
+ data: [
+ {
+ name: 'file',
+ value: ['/path/to/file'],
+ enabled: true,
+ contentType: 'application/json;charset=utf-8',
+ type: 'binaryFile'
+ }
+ ]
+ });
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js
index f486df56b..d91588178 100644
--- a/packages/bruno-app/src/utils/curl/index.js
+++ b/packages/bruno-app/src/utils/curl/index.js
@@ -50,14 +50,18 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
sparql: null,
multipartForm: null,
formUrlEncoded: null,
- graphql: null
+ graphql: null,
+ binaryFile: null
};
if (parsedBody && contentType && typeof contentType === 'string') {
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
- } else if (contentType.includes('application/json')) {
+ } else if (requestType === 'http-request' && request.isDataBinary) {
+ body.mode = 'binaryFile';
+ body.binaryFile = parsedBody;
+ }else if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = convertToCodeMirrorJson(parsedBody);
} else if (contentType.includes('xml')) {
diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js
index 88c4c7872..af187cc82 100644
--- a/packages/bruno-app/src/utils/importers/common.js
+++ b/packages/bruno-app/src/utils/importers/common.js
@@ -35,6 +35,7 @@ export const updateUidsInCollection = (_collection) => {
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
+ each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid()));
if (item.items && item.items.length) {
updateItemUids(item.items);
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 43d01153d..be98473ad 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -54,6 +54,7 @@ const hydrateRequestWithUuid = (request, pathname) => {
const assertions = _.get(request, 'request.assertions', []);
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
+ const binaryFile = _.get(request, 'request.body.binaryFile', []);
params.forEach((param) => (param.uid = uuid()));
headers.forEach((header) => (header.uid = uuid()));
@@ -62,6 +63,7 @@ const hydrateRequestWithUuid = (request, pathname) => {
assertions.forEach((assertion) => (assertion.uid = uuid()));
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
+ binaryFile.forEach((param) => (param.uid = uuid()));
return request;
};
@@ -241,7 +243,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
// Is this a folder.bru file?
if (path.basename(pathname) === 'folder.bru') {
- console.log('folder.bru file detected');
const file = {
meta: {
collectionUid,
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 898324892..c0391f4d6 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -54,9 +54,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// browse directory for file
- ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => {
+ ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => {
try {
- const filePaths = await browseFiles(mainWindow, filters);
+
+ const filePaths = await browseFiles(mainWindow, filters, properties);
return filePaths;
} catch (error) {
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 1865426e0..7495c94fa 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -570,19 +570,21 @@ const registerNetworkIpc = (mainWindow) => {
cancelTokenUid
});
- const collectionRoot = get(collection, 'root', {});
- const request = prepareRequest(item, collection);
- request.__bruno__executionMode = 'standalone';
- const envVars = getEnvVars(environment);
- const processEnvVars = getProcessEnvVars(collectionUid);
- const brunoConfig = getBrunoConfig(collectionUid);
- const scriptingConfig = get(brunoConfig, 'scripts', {});
- scriptingConfig.runtime = getJsSandboxRuntime(collection);
+ const abortController = new AbortController();
+
+ const collectionRoot = get(collection, 'root', {});
+ const request = await prepareRequest(item, collection, abortController);
+ request.__bruno__executionMode = 'standalone';
+ const envVars = getEnvVars(environment);
+ const processEnvVars = getProcessEnvVars(collectionUid);
+ const brunoConfig = getBrunoConfig(collectionUid);
+ const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = getJsSandboxRuntime(collection);
try {
- const controller = new AbortController();
- request.signal = controller.signal;
- saveCancelToken(cancelTokenUid, controller);
+ request.signal = abortController.signal;
+
+ saveCancelToken(cancelTokenUid, abortController);
await runPreRequest(
request,
@@ -612,7 +614,7 @@ const registerNetworkIpc = (mainWindow) => {
url: request.url,
method: request.method,
headers: request.headers,
- data: safeParseJSON(safeStringifyJSON(request.data)),
+ data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) ,
timestamp: Date.now()
},
collectionUid,
@@ -1031,7 +1033,7 @@ const registerNetworkIpc = (mainWindow) => {
...eventData
});
- const request = prepareRequest(item, collection);
+ const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'runner';
const requestUid = uuid();
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 6c7672e7d..297033c6d 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,8 +1,10 @@
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
+const fs = require('node:fs/promises');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
+const path = require('node:path');
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
@@ -174,7 +176,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
return axiosRequest;
};
-const prepareRequest = (item, collection) => {
+const prepareRequest = async (item, collection, abortController) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = get(collection, 'root', {});
const collectionPath = collection.pathname;
@@ -251,6 +253,36 @@ const prepareRequest = (item, collection) => {
axiosRequest.data = request.body.sparql;
}
+ if (request.body.mode === 'binaryFile') {
+
+ if (!contentTypeDefined) {
+ axiosRequest.headers['content-type'] = 'application/octet-stream';
+ }
+
+ if (request.body.binaryFile && request.body.binaryFile.length > 0) {
+
+ axiosRequest.headers['content-type'] = request.body.binaryFile[0].contentType;
+
+ let filePath = request.body.binaryFile[0].value[0];
+
+ if (filePath && filePath !== '') {
+
+ if (!path.isAbsolute(filePath)) {
+
+ filePath = path.join(collectionPath, filePath);
+ }
+
+ const file = await fs.readFile(filePath, abortController)
+
+ axiosRequest.data = file
+
+ if(axiosRequest.headers['content-type'].includes('application/json')) {
+ axiosRequest.data = JSON.parse(file)
+ }
+ }
+ }
+ }
+
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index d2f74d10e..671b628d5 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -121,9 +121,9 @@ const browseDirectory = async (win) => {
return isDirectory(resolvedPath) ? resolvedPath : false;
};
-const browseFiles = async (win, filters) => {
+const browseFiles = async (win, filters, properties) => {
const { filePaths } = await dialog.showOpenDialog(win, {
- properties: ['openFile', 'multiSelections'],
+ properties: ['openFile', ...properties],
filters
});
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 2fe5fb472..e7b0c5772 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
- bodyforms = bodyformurlencoded | bodymultipart
+ bodyforms = bodyformurlencoded | bodymultipart | bodybinaryfile
params = paramspath | paramsquery
nl = "\\r"? "\\n"
@@ -102,7 +102,8 @@ const grammar = ohm.grammar(`Bru {
bodyformurlencoded = "body:form-urlencoded" dictionary
bodymultipart = "body:multipart-form" dictionary
-
+ bodybinaryfile = "body:binary-file" dictionary
+
script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
scriptres = "script:post-response" st* "{" nl* textblock tagend
@@ -173,6 +174,19 @@ const multipartExtractContentType = (pair) => {
}
};
+const binaryFileExtractContentType = (pair) => {
+ if (_.isString(pair.value)) {
+ const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
+ if (match != null && match.length > 2) {
+ pair.value = match[1];
+ pair.contentType = match[2];
+ } else {
+ pair.contentType = '';
+ }
+ }
+};
+
+
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
@@ -190,6 +204,23 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
});
};
+const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => {
+ const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
+
+ return pairs.map((pair) => {
+ binaryFileExtractContentType(pair);
+
+ if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
+ let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
+ pair.type = 'binaryFile';
+ pair.value = filestr != '' ? filestr.split('|') : [''];
+ }
+
+ return pair;
+ });
+};
+
+
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -574,6 +605,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ bodybinaryfile(_1, dictionary) {
+ return {
+ body: {
+ binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast)
+ }
+ };
+ },
body(_1, _2, _3, _4, textblock, _5) {
return {
http: {
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 62b31c2f9..c4e2ba323 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -313,6 +313,32 @@ ${indentString(body.sparql)}
bru += '\n}\n\n';
}
+
+ if (body && body.binaryFile && body.binaryFile.length) {
+ bru += `body:binary-file {`;
+ const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile));
+
+ if (binaryFiles.length) {
+ bru += `\n${indentString(
+ binaryFiles
+ .map((item) => {
+ const enabled = item.enabled ? '' : '~';
+ const contentType =
+ item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
+
+ if (item.type === 'binaryFile') {
+ let filestr = item.value[0] || '';
+ const value = `@file(${filestr})`;
+ return `${enabled}${item.name}: ${value}${contentType}`;
+ }
+ })
+ .join('\n')
+ )}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+
if (body && body.graphql && body.graphql.query) {
bru += `body:graphql {\n`;
bru += `${indentString(body.graphql.query)}`;
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index 1a3efeab7..5f7183f34 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -102,6 +102,11 @@ body:multipart-form {
~message: hello
}
+body:binary-file {
+ file: @file(path/to/file.json) @contentType(application/json)
+ ~file2: @file(path/to/file2.json) @contentType(application/json)
+}
+
body:graphql {
{
launchesPast {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 9c8ed143d..ad7a45495 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -137,6 +137,22 @@
"enabled": false,
"type": "text"
}
+ ],
+ "binaryFile" : [
+ {
+ "name": "file",
+ "value": ["path/to/file.json"],
+ "enabled": true,
+ "type": "binaryFile",
+ "contentType": "application/json"
+ },
+ {
+ "name": "file2",
+ "value": ["path/to/file2.json"],
+ "enabled": false,
+ "type": "binaryFile",
+ "contentType": "application/json"
+ }
]
},
"vars": {
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index b6e044ae4..19d98afbb 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -74,9 +74,21 @@ const multipartFormSchema = Yup.object({
.noUnknown(true)
.strict();
+
+const binaryFileSchema = Yup.object({
+ uid: uidSchema,
+ type: Yup.string().oneOf(['binaryFile']).required('type is required'),
+ name: Yup.string().nullable(),
+ value: Yup.array().of(Yup.string().nullable()).nullable(),
+ contentType: Yup.string().nullable(),
+ enabled: Yup.boolean()
+})
+ .noUnknown(true)
+ .strict();
+
const requestBodySchema = Yup.object({
mode: Yup.string()
- .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
+ .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile'])
.required('mode is required'),
json: Yup.string().nullable(),
text: Yup.string().nullable(),
@@ -84,7 +96,8 @@ const requestBodySchema = Yup.object({
sparql: Yup.string().nullable(),
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: Yup.array().of(multipartFormSchema).nullable(),
- graphql: graphqlBodySchema.nullable()
+ graphql: graphqlBodySchema.nullable(),
+ binaryFile: Yup.array().of(binaryFileSchema).nullable()
})
.noUnknown(true)
.strict();
diff --git a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru
new file mode 100644
index 000000000..93275971f
--- /dev/null
+++ b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru
@@ -0,0 +1,27 @@
+meta {
+ name: binary-files-types
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{host}}/api/binaryFile/binary-file-types
+ body: binaryFile
+ auth: none
+}
+
+body:binary-file {
+ file1: @file() @contentType()
+ file2: @file(binaryFile/binary-file.json) @contentType()
+ file3: @file(binaryFile/binary-file.json) @contentType(application/json)
+}
+
+assert {
+ res.status: eq 200
+ res.body.find(p=>p.name === 'file1').value[0]: isUndefined
+ res.body.find(p=>p.name === 'file1').contentType: isUndefined
+ res.body.find(p=>p.name === 'file2').value[0]: eq binaryFile/binary-file.json
+ res.body.find(p=>p.name === 'file2').contentType: eq isUndefined
+ res.body.find(p=>p.name === 'file3').value[0]: eq binaryFile/binary-file.json
+ res.body.find(p=>p.name === 'file3').contentType: eq application/json
+}
diff --git a/packages/bruno-tests/collection/binaryFile/binary-file.json b/packages/bruno-tests/collection/binaryFile/binary-file.json
new file mode 100644
index 000000000..2ff269bff
--- /dev/null
+++ b/packages/bruno-tests/collection/binaryFile/binary-file.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "bruno-testing",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
\ No newline at end of file