mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
Compare commits
4 Commits
fix/websoc
...
revert-392
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20077dea9 | ||
|
|
7659122bef | ||
|
|
a075cc71d4 | ||
|
|
364b0f523a |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -24043,7 +24043,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
|
||||
@@ -6,10 +6,10 @@ 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
|
||||
const filenames = (isSingleFilePicker ? [value] : value || [])
|
||||
.filter((v) => v != null && v != '')
|
||||
.map((v) => {
|
||||
const separator = isWindowsOS() ? '\\' : '/';
|
||||
@@ -20,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
const title = filenames.map((v) => `- ${v}`).join('\n');
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseFiles())
|
||||
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
|
||||
.then((filePaths) => {
|
||||
// If file is in the collection's directory, then we use relative path
|
||||
// Otherwise, we use the absolute path
|
||||
@@ -34,7 +34,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
return filePath;
|
||||
});
|
||||
|
||||
onChange(filePaths);
|
||||
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -42,14 +42,14 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange([]);
|
||||
onChange(isSingleFilePicker ? '' : []);
|
||||
};
|
||||
|
||||
const renderButtonText = (filenames) => {
|
||||
if (filenames.length == 1) {
|
||||
return filenames[0];
|
||||
}
|
||||
return filenames.length + ' files selected';
|
||||
return filenames.length + ' file(s) selected';
|
||||
};
|
||||
|
||||
return filenames.length > 0 ? (
|
||||
@@ -66,9 +66,9 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
|
||||
Select Files
|
||||
{isSingleFilePicker ? 'Select File' : 'Select Files'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePickerEditor;
|
||||
export default FilePickerEditor;
|
||||
@@ -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;
|
||||
173
packages/bruno-app/src/components/RequestPane/Binary/index.js
Normal file
173
packages/bruno-app/src/components/RequestPane/Binary/index.js
Normal file
@@ -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 (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><div className="flex items-center justify-center">File</div></td>
|
||||
<td><div className="flex items-center justify-center">Content-Type</div></td>
|
||||
<td><div className="flex items-center justify-center">Enabled</div></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
<FilePickerEditor
|
||||
isSingleFilePicker={true}
|
||||
value={param.value}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
className='flex items-center justify-center'
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<input
|
||||
key={param.uid}
|
||||
type="radio"
|
||||
name="enabled"
|
||||
checked={enabledFileUid === param.uid || param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-1 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
|
||||
+ Add File
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Binary;
|
||||
@@ -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;
|
||||
164
packages/bruno-app/src/components/RequestPane/FileBody/index.js
Normal file
164
packages/bruno-app/src/components/RequestPane/FileBody/index.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { get, cloneDeep, isArray } from 'lodash';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFileParam, updateFileParam, deleteFileParam } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
|
||||
const FileBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
|
||||
|
||||
const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
|
||||
|
||||
const addFile = () => {
|
||||
dispatch(
|
||||
addFileParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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 'filePath': {
|
||||
param.filePath = e.target.filePath;
|
||||
param.contentType = "";
|
||||
break;
|
||||
}
|
||||
case 'contentType': {
|
||||
param.contentType = e.target.contentType;
|
||||
break;
|
||||
}
|
||||
case 'selected': {
|
||||
param.selected = e.target.selected;
|
||||
setEnableFileUid(param.uid)
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateFileParam({
|
||||
param: param,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveParams = (param) => {
|
||||
dispatch(
|
||||
deleteFileParam({
|
||||
paramUid: param.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">File</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">Content-Type</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">Selected</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
<FilePickerEditor
|
||||
isSingleFilePicker={true}
|
||||
value={param.filePath}
|
||||
onChange={(path) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
filePath: path
|
||||
}
|
||||
},
|
||||
param,
|
||||
'filePath'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
className="flex items-center justify-center"
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
contentType: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<input
|
||||
key={param.uid}
|
||||
type="radio"
|
||||
name="selected"
|
||||
checked={enabledFileUid === param.uid || param.selected}
|
||||
tabIndex="-1"
|
||||
className="mr-1 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'selected')}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
|
||||
+ Add File
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default FileBody;
|
||||
@@ -128,6 +128,15 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
SPARQL
|
||||
</div>
|
||||
<div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('file');
|
||||
}}
|
||||
>
|
||||
File / Binary
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -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 FileBody from '../FileBody/index';
|
||||
|
||||
const RequestBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyMode === 'file') {
|
||||
return <FileBody item={item} collection={collection}/>
|
||||
}
|
||||
|
||||
if (bodyMode === 'formUrlEncoded') {
|
||||
return <FormUrlEncodedParams item={item} collection={collection} />;
|
||||
}
|
||||
|
||||
@@ -758,7 +758,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
xml: null,
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null
|
||||
formUrlEncoded: null,
|
||||
file: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
@@ -1038,14 +1039,17 @@ export const browseDirectory = () => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
export const browseFiles =
|
||||
(filters = []) =>
|
||||
(dispatch, getState) => {
|
||||
(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', filters, properties)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
@@ -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: [],
|
||||
@@ -873,25 +875,159 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
addFileParam: (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.file = item.draft.request.body.file || [];
|
||||
|
||||
item.draft.request.body.file.push({
|
||||
uid: uuid(),
|
||||
filePath: '',
|
||||
contentType: '',
|
||||
selected: false
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFileParam: (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);
|
||||
}
|
||||
|
||||
const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
|
||||
|
||||
if (param) {
|
||||
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
|
||||
param.filePath = action.payload.param.filePath;
|
||||
param.contentType = action.payload.param.contentType || contentType || '';
|
||||
param.selected = action.payload.param.selected;
|
||||
|
||||
item.draft.request.body.file = item.draft.request.body.file.map((p) => {
|
||||
p.selected = p.uid === param.uid;
|
||||
return p;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFileParam: (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.file = filter(
|
||||
item.draft.request.body.file,
|
||||
(p) => p.uid !== action.payload.paramUid
|
||||
);
|
||||
|
||||
if (item.draft.request.body.file.length > 0) {
|
||||
item.draft.request.body.file[0].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -951,6 +1087,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.body.sparql = action.payload.content;
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
item.draft.request.body.file = action.payload.content;
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
item.draft.request.body.formUrlEncoded = action.payload.content;
|
||||
break;
|
||||
@@ -1952,6 +2092,9 @@ export const {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam,
|
||||
addFileParam,
|
||||
updateFileParam,
|
||||
deleteFileParam,
|
||||
moveMultipartFormParam,
|
||||
updateRequestAuthMode,
|
||||
updateRequestBodyMode,
|
||||
|
||||
@@ -14,6 +14,8 @@ const createContentType = (mode) => {
|
||||
return 'application/json';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
case 'file':
|
||||
return 'application/octet-stream';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -60,26 +62,60 @@ 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':
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: new URLSearchParams(
|
||||
body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.reduce((acc, param) => {
|
||||
acc[param.name] = param.value;
|
||||
return acc;
|
||||
}, {})
|
||||
).toString(),
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value
|
||||
}))
|
||||
};
|
||||
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 'file':
|
||||
const binary = {
|
||||
mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.selected)
|
||||
.map((param) => ({
|
||||
value: param.filePath,
|
||||
}))
|
||||
};
|
||||
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 +125,7 @@ export const buildHarRequest = ({ request, headers, type }) => {
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body, type),
|
||||
headersSize: 0,
|
||||
bodySize: 0
|
||||
bodySize: 0,
|
||||
binary: true
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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.file'), (param) => delete param.uid);
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
|
||||
@@ -281,6 +281,17 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
});
|
||||
};
|
||||
|
||||
const copyFileParams = (params = []) => {
|
||||
return map(params, (param) => {
|
||||
return {
|
||||
uid: param.uid,
|
||||
filePath: param.filePath,
|
||||
contentType: param.contentType,
|
||||
selected: param.selected
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const copyItems = (sourceItems, destItems) => {
|
||||
each(sourceItems, (si) => {
|
||||
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
|
||||
@@ -308,7 +319,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),
|
||||
file: copyFileParams(si.request.body.file)
|
||||
},
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
@@ -661,6 +673,10 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
label = 'SPARQL';
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
label = 'File / Binary';
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
label = 'Form URL Encoded';
|
||||
break;
|
||||
@@ -761,6 +777,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.file'), (param) => (param.uid = uuid()));
|
||||
|
||||
return item;
|
||||
};
|
||||
@@ -771,11 +788,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 file = get(item, 'request.body.file', []);
|
||||
|
||||
params.forEach((param) => delete param.uid);
|
||||
headers.forEach((header) => delete header.uid);
|
||||
bodyFormUrlEncoded.forEach((param) => delete param.uid);
|
||||
bodyMultipartForm.forEach((param) => delete param.uid);
|
||||
file.forEach((param) => delete param.uid);
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
@@ -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,8 +100,28 @@ 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;
|
||||
}
|
||||
|
||||
data.data = [
|
||||
{
|
||||
filePath: repr(filePath),
|
||||
contentType: request.headers['Content-Type'],
|
||||
selected: true,
|
||||
}
|
||||
];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
data.files = {};
|
||||
|
||||
for (const multipartKey in request.multipartUploads) {
|
||||
const multipartValue = request.multipartUploads[multipartKey];
|
||||
@@ -140,6 +161,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 = {};
|
||||
@@ -161,12 +183,10 @@ const curlToJson = (curlCommand) => {
|
||||
|
||||
if (request.query) {
|
||||
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) {
|
||||
|
||||
@@ -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: 'file'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,14 +50,18 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null,
|
||||
graphql: null
|
||||
graphql: null,
|
||||
file: 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 = 'file';
|
||||
body.file = parsedBody;
|
||||
}else if (contentType.includes('application/json')) {
|
||||
body.mode = 'json';
|
||||
body.json = convertToCodeMirrorJson(parsedBody);
|
||||
} else if (contentType.includes('xml')) {
|
||||
|
||||
@@ -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.file'), (param) => (param.uid = uuid()));
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
|
||||
@@ -62,13 +62,11 @@ 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 (_, filters, properties) => {
|
||||
try {
|
||||
const filePaths = await browseFiles(mainWindow, filters);
|
||||
|
||||
return filePaths;
|
||||
return await browseFiles(mainWindow, filters, properties);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { chooseFileToSave, writeFile } = require('../../utils/filesystem');
|
||||
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
|
||||
const {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
@@ -575,16 +575,19 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const request = prepareRequest(item, collection);
|
||||
const abortController = new AbortController();
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
|
||||
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,
|
||||
@@ -614,7 +617,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: safeParseJSON(safeStringifyJSON(request.data)),
|
||||
data: request.mode == 'file'? "<request body redacted>": safeParseJSON(safeStringifyJSON(request.data)),
|
||||
timestamp: Date.now()
|
||||
},
|
||||
collectionUid,
|
||||
@@ -1036,7 +1039,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
const request = prepareRequest(item, collection);
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
const requestUid = uuid();
|
||||
@@ -1371,7 +1374,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (encoding === 'utf-8') {
|
||||
await writeFile(filePath, data);
|
||||
} else {
|
||||
await writeBinaryFile(filePath, data);
|
||||
await writeFile(filePath, data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const { get, each, filter, find } = 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,31 @@ const prepareRequest = (item, collection) => {
|
||||
axiosRequest.data = request.body.sparql;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'file') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
|
||||
}
|
||||
|
||||
const file = find(request.body.file, (param) => param.selected);
|
||||
if (file) {
|
||||
let { filePath, contentType } = file;
|
||||
|
||||
axiosRequest.headers['content-type'] = contentType;
|
||||
if (filePath) {
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(collectionPath, filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath);
|
||||
axiosRequest.data = fileContent;
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.body.mode === 'formUrlEncoded') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
|
||||
@@ -69,16 +69,6 @@ function normalizeWslPath(pathname) {
|
||||
}
|
||||
|
||||
const writeFile = async (pathname, content) => {
|
||||
try {
|
||||
fs.writeFileSync(pathname, content, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const writeBinaryFile = async (pathname, content) => {
|
||||
try {
|
||||
fs.writeFileSync(pathname, content);
|
||||
} catch (err) {
|
||||
@@ -121,9 +111,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
|
||||
});
|
||||
|
||||
@@ -265,7 +255,6 @@ module.exports = {
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
writeFile,
|
||||
writeBinaryFile,
|
||||
hasJsonExtension,
|
||||
hasBruExtension,
|
||||
createDirectory,
|
||||
|
||||
@@ -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 | bodyfile
|
||||
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
|
||||
|
||||
bodyfile = "body: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 fileExtractContentType = (pair) => {
|
||||
if (_.isString(pair.value)) {
|
||||
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
|
||||
if (match && match.length > 2) {
|
||||
pair.value = match[1].trim();
|
||||
pair.contentType = match[2].trim();
|
||||
} else {
|
||||
pair.contentType = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
|
||||
@@ -190,6 +204,27 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
|
||||
});
|
||||
};
|
||||
|
||||
const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
return pairs.map((pair) => {
|
||||
fileExtractContentType(pair);
|
||||
|
||||
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
|
||||
let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
pair.filePath = filePath;
|
||||
pair.selected = pair.enabled
|
||||
|
||||
// Remove pair.value as it only contains the file path reference
|
||||
delete pair.value;
|
||||
// Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
|
||||
delete pair.name;
|
||||
delete pair.enabled;
|
||||
}
|
||||
|
||||
return pair;
|
||||
});
|
||||
};
|
||||
|
||||
const concatArrays = (objValue, srcValue) => {
|
||||
if (_.isArray(objValue) && _.isArray(srcValue)) {
|
||||
return objValue.concat(srcValue);
|
||||
@@ -574,6 +609,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
bodyfile(_1, dictionary) {
|
||||
return {
|
||||
body: {
|
||||
file: mapPairListToKeyValPairsFile(dictionary.ast)
|
||||
}
|
||||
};
|
||||
},
|
||||
body(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
http: {
|
||||
@@ -708,3 +750,4 @@ const parser = (input) => {
|
||||
};
|
||||
|
||||
module.exports = parser;
|
||||
|
||||
@@ -2,8 +2,8 @@ const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('../../v1/src/utils');
|
||||
|
||||
const enabled = (items = []) => items.filter((item) => item.enabled);
|
||||
const disabled = (items = []) => items.filter((item) => !item.enabled);
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -313,6 +313,29 @@ ${indentString(body.sparql)}
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (body && body.file && body.file.length) {
|
||||
bru += `body:file {`;
|
||||
const files = enabled(body.file, "selected").concat(disabled(body.file, "selected"));
|
||||
|
||||
if (files.length) {
|
||||
bru += `\n${indentString(
|
||||
files
|
||||
.map((item) => {
|
||||
const selected = item.selected ? '' : '~';
|
||||
const contentType =
|
||||
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
const filePath = item.filePath || '';
|
||||
const value = `@file(${filePath})`;
|
||||
const itemName = "file";
|
||||
return `${selected}${itemName}: ${value}${contentType}`;
|
||||
})
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (body && body.graphql && body.graphql.query) {
|
||||
bru += `body:graphql {\n`;
|
||||
bru += `${indentString(body.graphql.query)}`;
|
||||
|
||||
@@ -102,6 +102,12 @@ body:multipart-form {
|
||||
~message: hello
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(path/to/file.json) @contentType(application/json)
|
||||
file: @file(path/to/file.json) @contentType(application/json)
|
||||
~file: @file(path/to/file2.json) @contentType(application/json)
|
||||
}
|
||||
|
||||
body:graphql {
|
||||
{
|
||||
launchesPast {
|
||||
|
||||
@@ -137,6 +137,23 @@
|
||||
"enabled": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"file" : [
|
||||
{
|
||||
"filePath": "path/to/file.json",
|
||||
"contentType": "application/json",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"filePath": "path/to/file.json",
|
||||
"contentType": "application/json",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"filePath": "path/to/file2.json",
|
||||
"contentType": "application/json",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"vars": {
|
||||
|
||||
@@ -74,9 +74,19 @@ const multipartFormSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
|
||||
const fileSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
filePath: Yup.string().nullable(),
|
||||
contentType: Yup.string().nullable(),
|
||||
selected: 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', 'file'])
|
||||
.required('mode is required'),
|
||||
json: Yup.string().nullable(),
|
||||
text: Yup.string().nullable(),
|
||||
@@ -84,7 +94,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(),
|
||||
file: Yup.array().of(fileSchema).nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "bruno-testing",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: echo bom json
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/api/echo/bom-json-test
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
BIN
packages/bruno-tests/collection/echo/echo file body/bin.bru
Normal file
BIN
packages/bruno-tests/collection/echo/echo file body/bin.bru
Normal file
Binary file not shown.
40
packages/bruno-tests/collection/echo/echo file body/bru.bru
Normal file
40
packages/bruno-tests/collection/echo/echo file body/bru.bru
Normal file
@@ -0,0 +1,40 @@
|
||||
meta {
|
||||
name: bru
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: file
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(ping.bru) @contentType(bru)
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should return bru file body contents", function() {
|
||||
const data = res.getBody();
|
||||
const expectedData = `meta {
|
||||
name: ping
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
`;
|
||||
expect(res.getBody()).to.eql(expectedData);
|
||||
});
|
||||
|
||||
|
||||
test("should return proper header", function() {
|
||||
const contentType = res.getHeader('content-type');
|
||||
expect(contentType).to.eql('bru');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: image
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: file
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(bruno.png) @contentType(image/png)
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should return proper header", function() {
|
||||
const contentType = res.getHeader('content-type');
|
||||
expect(contentType).to.eql('image/png');
|
||||
});
|
||||
}
|
||||
55
packages/bruno-tests/collection/echo/echo file body/json.bru
Normal file
55
packages/bruno-tests/collection/echo/echo file body/json.bru
Normal file
@@ -0,0 +1,55 @@
|
||||
meta {
|
||||
name: json
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: file
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(file.json) @contentType(application/json; charset=utf-8)
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should return json file body contents buffer", function() {
|
||||
const data = res.getBody();
|
||||
expect(res.getBody()).to.eql({
|
||||
"type": "Buffer",
|
||||
"data": [
|
||||
123,
|
||||
10,
|
||||
32,
|
||||
32,
|
||||
34,
|
||||
104,
|
||||
101,
|
||||
108,
|
||||
108,
|
||||
111,
|
||||
34,
|
||||
58,
|
||||
32,
|
||||
34,
|
||||
98,
|
||||
114,
|
||||
117,
|
||||
110,
|
||||
111,
|
||||
34,
|
||||
10,
|
||||
125,
|
||||
10
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test("should return proper header", function() {
|
||||
const contentType = res.getHeader('content-type');
|
||||
expect(contentType).to.eql('application/json; charset=utf-8');
|
||||
});
|
||||
|
||||
}
|
||||
30
packages/bruno-tests/collection/echo/echo file body/text.bru
Normal file
30
packages/bruno-tests/collection/echo/echo file body/text.bru
Normal file
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: text
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: file
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(file.txt) @contentType(text/plain; charset=utf-8)
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should return json file body contents", function() {
|
||||
const data = res.getBody();
|
||||
const expectedData = `file.txt
|
||||
|
||||
hello, bruno`;
|
||||
expect(res.getBody()).to.eql(expectedData);
|
||||
});
|
||||
|
||||
test("should return proper header", function() {
|
||||
const contentType = res.getHeader('content-type');
|
||||
expect(contentType).to.eql('text/plain; charset=utf-8');
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo json
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo numbers
|
||||
type: http
|
||||
seq: 1
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo plaintext
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo xml parsed(self closing tags)
|
||||
type: http
|
||||
seq: 6
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo xml parsed
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: echo xml raw
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
|
||||
BIN
packages/bruno-tests/collection/file.bin
Normal file
BIN
packages/bruno-tests/collection/file.bin
Normal file
Binary file not shown.
3
packages/bruno-tests/collection/file.txt
Normal file
3
packages/bruno-tests/collection/file.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
file.txt
|
||||
|
||||
hello, bruno
|
||||
@@ -19,6 +19,17 @@ router.post('/xml-raw', (req, res) => {
|
||||
return res.send(req.rawBody);
|
||||
});
|
||||
|
||||
router.post('/bin', (req, res) => {
|
||||
const rawBody = req.body;
|
||||
|
||||
if (!rawBody || rawBody.length === 0) {
|
||||
return res.status(400).send('No data received');
|
||||
}
|
||||
|
||||
res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream');
|
||||
res.send(rawBody);
|
||||
});
|
||||
|
||||
router.get('/bom-json-test', (req, res) => {
|
||||
const jsonData = {
|
||||
message: 'Hello!',
|
||||
|
||||
@@ -10,6 +10,7 @@ const multipartRouter = require('./multipart');
|
||||
const app = new express();
|
||||
const port = process.env.PORT || 8080;
|
||||
|
||||
app.use(express.raw({type: '*/*', limit: '100mb'}));
|
||||
app.use(cors());
|
||||
app.use(xmlParser());
|
||||
app.use(bodyParser.text());
|
||||
|
||||
Reference in New Issue
Block a user