diff --git a/package-lock.json b/package-lock.json index f53c786a9..2cfe22abb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index c824f13a8..7203bb816 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -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", diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index d976a3e79..9387800d2 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -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, isSingleFilePicker = false}) => { +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, isSingleFilePicker = fa 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, isSingleFilePicker = fa return filePath; }); - onChange(filePaths); + onChange(isSingleFilePicker ? filePaths[0] : filePaths); }) .catch((error) => { console.error(error); @@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa }; const clear = () => { - onChange([]); + onChange(isSingleFilePicker ? '' : []); }; const renderButtonText = (filenames) => { @@ -66,9 +66,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa ) : ( ); }; -export default FilePickerEditor; +export default FilePickerEditor; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js new file mode 100644 index 000000000..35adfcc1f --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/FileBody/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/FileBody/index.js b/packages/bruno-app/src/components/RequestPane/FileBody/index.js new file mode 100644 index 000000000..ae1646330 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/FileBody/index.js @@ -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 ( + + + + + + + + + + + + {params && params.length + ? params.map((param, index) => { + return ( + + + + + + + ); + }) + : null} + +
+
File
+
+
Content-Type
+
+
Selected
+
+ + handleParamChange( + { + target: { + filePath: path + } + }, + param, + 'filePath' + ) + } + collection={collection} + /> + + + handleParamChange( + { + target: { + contentType: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleParamChange(e, param, 'selected')} + /> +
+
+
+ +
+
+
+ +
+
+ ); +}; +export default FileBody; \ No newline at end of file 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 95b3b6a55..db73597df 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -132,10 +132,10 @@ const RequestBodyMode = ({ item, collection }) => { className="dropdown-item" onClick={() => { dropdownTippyRef.current.hide(); - onModeChange('binaryFile'); + onModeChange('file'); }} > - Binary File + File / Binary
{ const dispatch = useDispatch(); @@ -63,8 +63,8 @@ const RequestBody = ({ item, collection }) => { ); } - if (bodyMode === 'binaryFile') { - return + if (bodyMode === 'file') { + return } if (bodyMode === 'formUrlEncoded') { 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 f089dbddc..d251112c7 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,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => { sparql: null, multipartForm: null, formUrlEncoded: null, - binaryFile: null + file: null }, auth: auth ?? { mode: 'none' @@ -1040,13 +1040,16 @@ export const browseDirectory = () => (dispatch, getState) => { export const browseFiles = (filters = [], properties = ['multiSelections']) => - (dispatch, getState) => { + (_dispatch, _getState) => { const { ipcRenderer } = window; return new Promise((resolve, reject) => { - ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).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(); 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 bdecc1543..dcf130065 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -961,6 +961,76 @@ export const collectionsSlice = createSlice({ } } }, + 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; + } + } + } + }, updateRequestAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1017,8 +1087,8 @@ export const collectionsSlice = createSlice({ item.draft.request.body.sparql = action.payload.content; break; } - case 'binaryFile': { - item.draft.request.body.binaryFile = action.payload.content; + case 'file': { + item.draft.request.body.file = action.payload.content; break; } case 'formUrlEncoded': { @@ -2022,9 +2092,9 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, - addBinaryFile, - updateBinaryFile, - deleteBinaryFile, + addFileParam, + updateFileParam, + deleteFileParam, moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 19e4ea4de..a4128b0c0 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -14,7 +14,7 @@ const createContentType = (mode) => { return 'application/json'; case 'multipartForm': return 'multipart/form-data'; - case 'binaryFile': + case 'file': return 'application/octet-stream'; default: return ''; @@ -65,6 +65,23 @@ const createPostData = (body, type) => { 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, @@ -76,20 +93,15 @@ const createPostData = (body, type) => { ...(param.type === 'file' && { fileName: param.value }) })) }; - case 'binaryFile': + case 'file': const binary = { - mimeType: 'application/octet-stream', - // mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, + mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, params: body[body.mode] - .filter((param) => param.enabled) + .filter((param) => param.selected) .map((param) => ({ - name: param.name, - value: param.value, - fileName: param.value + value: param.filePath, })) }; - - console.log('curl-binary', binary); return binary; default: return { diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index b7ca9f5ba..3d15fdd07 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -14,7 +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); + each(get(item, 'request.body.file'), (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 309c245eb..eb53cfb48 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -281,15 +281,13 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; - const copyBinaryFileParams = (params = []) => { + const copyFileParams = (params = []) => { return map(params, (param) => { return { uid: param.uid, - type: param.type, - name: param.name, - value: param.value, + filePath: param.filePath, contentType: param.contentType, - enabled: param.enabled + selected: param.selected } }); } @@ -322,7 +320,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} sparql: si.request.body.sparql, formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.request.body.multipartForm), - binaryFile: copyBinaryFileParams(si.request.body.binaryFile) + file: copyFileParams(si.request.body.file) }, script: si.request.script, vars: si.request.vars, @@ -675,8 +673,8 @@ export const humanizeRequestBodyMode = (mode) => { label = 'SPARQL'; break; } - case 'binaryFile': { - label = 'Binary File'; + case 'file': { + label = 'File / Binary'; break; } case 'formUrlEncoded': { @@ -779,7 +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.binaryFile'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.file'), (param) => (param.uid = uuid())); return item; }; @@ -790,13 +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 binaryFile = get(item, 'request.body.binaryFile', []); + 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); - binaryFile.forEach((param) => delete param.uid); + file.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 5a2c62022..69305b370 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -101,31 +101,24 @@ function getFilesString(request) { const data = {}; data.data = {}; + if (request.isDataBinary) { + let filePath = ''; - if (request.isDataBinary){ - - let filePath = '' - - if(request.data.startsWith('@')){ + if (request.data.startsWith('@')) { filePath = request.data.slice(1); - }else{ + } else { filePath = request.data; } - const fileName = path.basename(filePath); - data.data = [ { - name: repr(fileName), - value: [repr(filePath)], - enabled: true, + filePath: repr(filePath), contentType: request.headers['Content-Type'], - type: 'binaryFile' + selected: true, } ]; return data; - } data.files = {}; @@ -190,13 +183,11 @@ const curlToJson = (curlCommand) => { if (request.query) { requestJson.queries = getQueries(request); - } - - else if (request.multipartUploads || request.isDataBinary) { + } 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 6f8206139..4c3194c2e 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 @@ -117,7 +117,7 @@ describe('curlToJson', () => { value: ['/path/to/file'], enabled: true, contentType: 'application/json;charset=utf-8', - type: 'binaryFile' + type: 'file' } ] }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index d91588178..ad4f1edf6 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -51,7 +51,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque multipartForm: null, formUrlEncoded: null, graphql: null, - binaryFile: null + file: null }; if (parsedBody && contentType && typeof contentType === 'string') { @@ -59,8 +59,8 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque body.mode = 'graphql'; body.graphql = parseGraphQL(parsedBody); } else if (requestType === 'http-request' && request.isDataBinary) { - body.mode = 'binaryFile'; - body.binaryFile = parsedBody; + body.mode = 'file'; + body.file = parsedBody; }else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index af187cc82..9d370a455 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -35,7 +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())); + each(get(item, 'request.body.file'), (param) => (param.uid = uuid())); if (item.items && item.items.length) { updateItemUids(item.items); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index cdf488e75..cb73d2dc6 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,14 +62,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // browse directory for file - ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => { + ipcMain.handle('renderer:browse-files', async (_, filters, properties) => { try { - - const filePaths = await browseFiles(mainWindow, filters, properties); - - return filePaths; + return await browseFiles(mainWindow, filters, properties); } catch (error) { - return Promise.reject(error); + throw error; } }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dbc36d846..b76113e42 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -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, @@ -587,7 +587,6 @@ const registerNetworkIpc = (mainWindow) => { try { request.signal = abortController.signal; - saveCancelToken(cancelTokenUid, abortController); await runPreRequest( @@ -618,7 +617,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) , + data: request.mode == 'file'? "": safeParseJSON(safeStringifyJSON(request.data)), timestamp: Date.now() }, collectionUid, @@ -1375,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) { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 297033c6d..137b74440 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,4 +1,4 @@ -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'); @@ -253,31 +253,26 @@ const prepareRequest = async (item, collection, abortController) => { axiosRequest.data = request.body.sparql; } - if (request.body.mode === 'binaryFile') { - + if (request.body.mode === 'file') { if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/octet-stream'; + axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads } - - 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 !== '') { - + + 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); } - - const file = await fs.readFile(filePath, abortController) - - axiosRequest.data = file - - if(axiosRequest.headers['content-type'].includes('application/json')) { - axiosRequest.data = JSON.parse(file) + + try { + const fileContent = await fs.readFile(filePath); + axiosRequest.data = fileContent; + } catch (error) { + console.error('Error reading file:', error); } } } diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index d37742be4..4bf4c9970 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -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) { @@ -265,7 +255,6 @@ module.exports = { isWSLPath, normalizeWslPath, writeFile, - writeBinaryFile, hasJsonExtension, hasBruExtension, createDirectory, diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e7b0c5772..26a670681 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 | bodybinaryfile + bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery nl = "\\r"? "\\n" @@ -102,7 +102,7 @@ const grammar = ohm.grammar(`Bru { bodyformurlencoded = "body:form-urlencoded" dictionary bodymultipart = "body:multipart-form" dictionary - bodybinaryfile = "body:binary-file" dictionary + bodyfile = "body:file" dictionary script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend @@ -174,12 +174,12 @@ const multipartExtractContentType = (pair) => { } }; -const binaryFileExtractContentType = (pair) => { +const fileExtractContentType = (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]; + if (match && match.length > 2) { + pair.value = match[1].trim(); + pair.contentType = match[2].trim(); } else { pair.contentType = ''; } @@ -204,23 +204,27 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = }); }; -const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { +const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); - return pairs.map((pair) => { - binaryFileExtractContentType(pair); + fileExtractContentType(pair); if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { - let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); - pair.type = 'binaryFile'; - pair.value = filestr != '' ? filestr.split('|') : ['']; + 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); @@ -605,10 +609,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, - bodybinaryfile(_1, dictionary) { + bodyfile(_1, dictionary) { return { body: { - binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast) + file: mapPairListToKeyValPairsFile(dictionary.ast) } }; }, @@ -746,3 +750,4 @@ const parser = (input) => { }; module.exports = parser; + \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index c4e2ba323..12aaa1894 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -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,24 +313,21 @@ ${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 (body && body.binaryFile && body.binaryFile.length) { - bru += `body:binary-file {`; - const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile)); - - if (binaryFiles.length) { + if (files.length) { bru += `\n${indentString( - binaryFiles + files .map((item) => { - const enabled = item.enabled ? '' : '~'; + const selected = item.selected ? '' : '~'; 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}`; - } + const filePath = item.filePath || ''; + const value = `@file(${filePath})`; + const itemName = "file"; + return `${selected}${itemName}: ${value}${contentType}`; }) .join('\n') )}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 5f7183f34..ad66c64e8 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -102,9 +102,10 @@ body:multipart-form { ~message: hello } -body:binary-file { +body:file { file: @file(path/to/file.json) @contentType(application/json) - ~file2: @file(path/to/file2.json) @contentType(application/json) + file: @file(path/to/file.json) @contentType(application/json) + ~file: @file(path/to/file2.json) @contentType(application/json) } body:graphql { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index ad7a45495..1cfe98809 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -138,20 +138,21 @@ "type": "text" } ], - "binaryFile" : [ + "file" : [ { - "name": "file", - "value": ["path/to/file.json"], - "enabled": true, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true }, { - "name": "file2", - "value": ["path/to/file2.json"], - "enabled": false, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true + }, + { + "filePath": "path/to/file2.json", + "contentType": "application/json", + "selected": false } ] }, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 19d98afbb..e0d05d167 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -75,20 +75,18 @@ const multipartFormSchema = Yup.object({ .strict(); -const binaryFileSchema = Yup.object({ +const fileSchema = 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(), + filePath: Yup.string().nullable(), contentType: Yup.string().nullable(), - enabled: Yup.boolean() + selected: Yup.boolean() }) .noUnknown(true) .strict(); const requestBodySchema = Yup.object({ mode: Yup.string() - .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile']) + .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file']) .required('mode is required'), json: Yup.string().nullable(), text: Yup.string().nullable(), @@ -97,7 +95,7 @@ const requestBodySchema = Yup.object({ formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), graphql: graphqlBodySchema.nullable(), - binaryFile: Yup.array().of(binaryFileSchema).nullable() + file: Yup.array().of(fileSchema).nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/echo/echo bom json.bru b/packages/bruno-tests/collection/echo/echo bom json.bru deleted file mode 100644 index 117c329ba..000000000 --- a/packages/bruno-tests/collection/echo/echo bom json.bru +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/bin.bru b/packages/bruno-tests/collection/echo/echo file body/bin.bru new file mode 100644 index 000000000..fcb87d9c0 Binary files /dev/null and b/packages/bruno-tests/collection/echo/echo file body/bin.bru differ diff --git a/packages/bruno-tests/collection/echo/echo file body/bru.bru b/packages/bruno-tests/collection/echo/echo file body/bru.bru new file mode 100644 index 000000000..8fb60ef28 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/bru.bru @@ -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'); + }); +} diff --git a/packages/bruno-tests/collection/echo/echo file body/image.bru b/packages/bruno-tests/collection/echo/echo file body/image.bru new file mode 100644 index 000000000..01c5d408b --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/image.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'); + }); +} diff --git a/packages/bruno-tests/collection/echo/echo file body/json.bru b/packages/bruno-tests/collection/echo/echo file body/json.bru new file mode 100644 index 000000000..42412038a --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/json.bru @@ -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'); + }); + +} diff --git a/packages/bruno-tests/collection/echo/echo file body/text.bru b/packages/bruno-tests/collection/echo/echo file body/text.bru new file mode 100644 index 000000000..663c150f5 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/text.bru @@ -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'); + }); +} diff --git a/packages/bruno-tests/collection/echo/echo json.bru b/packages/bruno-tests/collection/echo/echo json.bru index 1749eda6b..0fa135fe8 100644 --- a/packages/bruno-tests/collection/echo/echo json.bru +++ b/packages/bruno-tests/collection/echo/echo json.bru @@ -1,7 +1,7 @@ meta { name: echo json type: http - seq: 2 + seq: 3 } post { diff --git a/packages/bruno-tests/collection/echo/echo numbers.bru b/packages/bruno-tests/collection/echo/echo numbers.bru index 8f68fe558..0d6126909 100644 --- a/packages/bruno-tests/collection/echo/echo numbers.bru +++ b/packages/bruno-tests/collection/echo/echo numbers.bru @@ -1,7 +1,7 @@ meta { name: echo numbers type: http - seq: 1 + seq: 2 } post { diff --git a/packages/bruno-tests/collection/echo/echo plaintext.bru b/packages/bruno-tests/collection/echo/echo plaintext.bru index 56a23d345..3ace3bb2b 100644 --- a/packages/bruno-tests/collection/echo/echo plaintext.bru +++ b/packages/bruno-tests/collection/echo/echo plaintext.bru @@ -1,7 +1,7 @@ meta { name: echo plaintext type: http - seq: 3 + seq: 4 } post { diff --git a/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru b/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru index d337cebb3..b378fb0c8 100644 --- a/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru +++ b/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru @@ -1,7 +1,7 @@ meta { name: echo xml parsed(self closing tags) type: http - seq: 6 + seq: 7 } post { diff --git a/packages/bruno-tests/collection/echo/echo xml parsed.bru b/packages/bruno-tests/collection/echo/echo xml parsed.bru index acd24a292..71363b71c 100644 --- a/packages/bruno-tests/collection/echo/echo xml parsed.bru +++ b/packages/bruno-tests/collection/echo/echo xml parsed.bru @@ -1,7 +1,7 @@ meta { name: echo xml parsed type: http - seq: 4 + seq: 5 } post { diff --git a/packages/bruno-tests/collection/echo/echo xml raw.bru b/packages/bruno-tests/collection/echo/echo xml raw.bru index 6a02ac238..0f8bec255 100644 --- a/packages/bruno-tests/collection/echo/echo xml raw.bru +++ b/packages/bruno-tests/collection/echo/echo xml raw.bru @@ -1,7 +1,7 @@ meta { name: echo xml raw type: http - seq: 5 + seq: 6 } post { diff --git a/packages/bruno-tests/collection/file.bin b/packages/bruno-tests/collection/file.bin new file mode 100644 index 000000000..8de62184f Binary files /dev/null and b/packages/bruno-tests/collection/file.bin differ diff --git a/packages/bruno-tests/collection/file.txt b/packages/bruno-tests/collection/file.txt new file mode 100644 index 000000000..033b8756d --- /dev/null +++ b/packages/bruno-tests/collection/file.txt @@ -0,0 +1,3 @@ +file.txt + +hello, bruno \ No newline at end of file diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index ba9b403ae..89a9208e0 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -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!', diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index a09cb434b..a482fc128 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -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());