diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js index 7908dfc09..08d011326 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js @@ -1,16 +1,13 @@ import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons'; -import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions'; +import { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; const RequestNotLoaded = ({ collection, item }) => { const dispatch = useDispatch(); - const handleLoadRequestViaWorker = () => { - !item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname })); - } - const handleLoadRequest = () => { - !item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname })); + const handleLoadLargeRequest = () => { + !item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname })); } return @@ -44,23 +41,14 @@ const RequestNotLoaded = ({ collection, item }) => { The request wasn't loaded due to its large size. Please try again with the following options: -
- -

(Runs in background)

-
-

(May cause the app to freeze temporarily while it runs)

+

(Uses a regex based parsing approach)

)} 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 2bb166c91..7e3a758d3 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1361,6 +1361,7 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => { }); }; +// todo: could be removed export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => { return new Promise(async (resolve, reject) => { const { ipcRenderer } = window; @@ -1368,6 +1369,7 @@ export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, }); }; +// todo: could be removed export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => { return new Promise(async (resolve, reject) => { const { ipcRenderer } = window; @@ -1375,6 +1377,13 @@ export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) }); }; +export const loadLargeRequest = ({ collectionUid, pathname }) => (dispatch, getState) => { + return new Promise(async (resolve, reject) => { + const { ipcRenderer } = window; + ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject); + }); +}; + export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => { dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' })); return new Promise(async (resolve, reject) => { diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index acdb8873b..665792cdb 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -20,6 +20,7 @@ const { setBrunoConfig } = require('../store/bruno-config'); const EnvironmentSecretsStore = require('../store/env-secrets'); const UiStateSnapshot = require('../store/ui-state-snapshot'); const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); +const { parseLargeRequestWithRedaction } = require('../utils/parse'); const MAX_FILE_SIZE = 2.5 * 1024 * 1024; @@ -344,11 +345,17 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { let seq; const folderBruFilePath = path.join(pathname, `folder.bru`); - if (fs.existsSync(folderBruFilePath)) { - let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); - let folderBruData = await parseFolder(folderBruFileContent); - name = folderBruData?.meta?.name || name; - seq = folderBruData?.meta?.seq; + try { + if (fs.existsSync(folderBruFilePath)) { + let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); + let folderBruData = await parseFolder(folderBruFileContent); + name = folderBruData?.meta?.name || name; + seq = folderBruData?.meta?.seq; + } + } + catch(error) { + console.error('Error occured while parsing folder.bru file!'); + console.error(error); } const directory = { @@ -462,10 +469,20 @@ const change = async (win, pathname, collectionUid, collectionPath) => { }; const bru = fs.readFileSync(pathname, 'utf8'); - file.data = await parseRequest(bru); + const fileStats = fs.statSync(pathname); - hydrateRequestWithUuid(file.data, pathname); - win.webContents.send('main:collection-tree-updated', 'change', file); + if (fileStats.size >= MAX_FILE_SIZE) { + const parsedData = await parseLargeRequestWithRedaction(bru); + file.data = parsedData; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'change', file); + } else { + file.data = await parseRequest(bru); + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'change', file); + } } catch (err) { console.error(err); } diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 8ca5ac2f9..0d51b2f92 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -19,6 +19,7 @@ const { } = require('@usebruno/filestore'); const brunoConverters = require('@usebruno/converters'); const { postmanToBruno } = brunoConverters; +const { parseLargeRequestWithRedaction } = require('../utils/parse'); const { writeFile, @@ -1057,6 +1058,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + // todo: could be removed ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => { let fileStats; try { @@ -1094,7 +1096,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = parseRequest(parseBruFileMeta(bruContent)); + const metaJson = parseBruFileMeta(bruContent); file.data = metaJson; file.partial = true; file.loading = false; @@ -1132,6 +1134,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + // todo: could be removed ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => { let fileStats; try { @@ -1145,7 +1148,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = parseRequest(parseBruFileMeta(bruContent)); + const metaJson = parseBruFileMeta(bruContent); file.data = metaJson; file.loading = true; file.partial = true; @@ -1169,7 +1172,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = parseRequest(parseBruFileMeta(bruContent)); + const metaJson = parseBruFileMeta(bruContent); file.data = metaJson; file.partial = true; file.loading = false; @@ -1181,6 +1184,56 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:load-large-request', async (event, { collectionUid, pathname }) => { + let fileStats; + if (!hasBruExtension(pathname)) { + return; + } + + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname) + } + }; + + try { + fileStats = fs.statSync(pathname); + + const bruContent = fs.readFileSync(pathname, 'utf8'); + const metaJson = parseBruFileMeta(bruContent); + + file.data = metaJson; + file.partial = false; + file.loading = true; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + + try { + const parsedData = await parseLargeRequestWithRedaction(bruContent); + + file.data = parsedData; + file.loading = false; + file.partial = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + } catch (parseError) { + file.data = metaJson; + file.partial = true; + file.loading = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + throw parseError; + } + } catch (error) { + return Promise.reject(error); + } + }); + ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => { const { size, diff --git a/packages/bruno-electron/src/utils/parse.js b/packages/bruno-electron/src/utils/parse.js new file mode 100644 index 000000000..655b9e6f7 --- /dev/null +++ b/packages/bruno-electron/src/utils/parse.js @@ -0,0 +1,42 @@ +const { parseRequestAndRedactBody, parseRequestViaWorker } = require('@usebruno/filestore'); + +/** + * Parses a large BRU request string by redacting body blocks, parsing the remainder, + * and then reinserting extracted body content into the parsed structure. + * @param {string} bruContent + * @returns {Promise} parsed request JSON + */ +async function parseLargeRequestWithRedaction(bruContent) { + const { bruFileStringWithRedactedBody, extractedBodyContent } = parseRequestAndRedactBody(bruContent); + const parsedData = await parseRequestViaWorker(bruFileStringWithRedactedBody); + + if (!parsedData.request) { + parsedData.request = {}; + } + if (!parsedData.request.body) { + parsedData.request.body = {}; + } + + if (extractedBodyContent.json) { + parsedData.request.body.json = extractedBodyContent.json; + } + if (extractedBodyContent.text) { + parsedData.request.body.text = extractedBodyContent.text; + } + if (extractedBodyContent.xml) { + parsedData.request.body.xml = extractedBodyContent.xml; + } + if (extractedBodyContent.sparql) { + parsedData.request.body.sparql = extractedBodyContent.sparql; + } + if (extractedBodyContent.graphql) { + if (!parsedData.request.body.graphql) { + parsedData.request.body.graphql = {}; + } + parsedData.request.body.graphql.query = extractedBodyContent.graphql; + } + + return parsedData; +} + +module.exports = { parseLargeRequestWithRedaction }; \ No newline at end of file diff --git a/packages/bruno-filestore/src/formats/bru/index.ts b/packages/bruno-filestore/src/formats/bru/index.ts index e71017cdf..f7bfff87c 100644 --- a/packages/bruno-filestore/src/formats/bru/index.ts +++ b/packages/bruno-filestore/src/formats/bru/index.ts @@ -47,8 +47,8 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); return transformedJson; - } catch (e) { - return Promise.reject(e); + } catch (error) { + throw error; } }; diff --git a/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/input.bru b/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/input.bru new file mode 100644 index 000000000..734e10d88 --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/input.bru @@ -0,0 +1,66 @@ +meta { + name: echo request + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: json + auth: none +} +body:json { + { + "hello": "world" + } +} + +body:text { + This is a text body +} +body:xml { + + John + 30 + +} + +body:sparql { + SELECT * WHERE { + ?subject ?predicate ?object . + } + LIMIT 10 +} +body:graphql { + { + launchesPast { + launch_site { + site_name + } + launch_success + } + } +} + +body:form-urlencoded { + apikey: secret + numbers: +91998877665 + ~message: hello +} + +body:multipart-form { + apikey: secret + numbers: +91998877665 + ~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:vars { + { + "limit": 5 + } +} diff --git a/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/output.bru b/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/output.bru new file mode 100644 index 000000000..ff66a2df8 --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/output.bru @@ -0,0 +1,35 @@ +meta { + name: echo request + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: json + auth: none +} + +body:form-urlencoded { + apikey: secret + numbers: +91998877665 + ~message: hello +} + +body:multipart-form { + apikey: secret + numbers: +91998877665 + ~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:vars { + { + "limit": 5 + } +} diff --git a/packages/bruno-filestore/src/formats/bru/tests/request-parse-and-redact-body-data.spec.js b/packages/bruno-filestore/src/formats/bru/tests/request-parse-and-redact-body-data.spec.js new file mode 100644 index 000000000..aaed33015 --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/tests/request-parse-and-redact-body-data.spec.js @@ -0,0 +1,44 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { bruRequestParseAndRedactBodyData } = require("../utils/request-parse-and-redact-body-data"); + +describe("parse and redact body data", () => { + it("should redact body blocks from the bru file string", () => { + const fixturesPath = `/fixtures/request-parse-and-redact-body-data`; + const inputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './input.bru'), 'utf8'); + const expectedOutputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './output.bru'), 'utf8'); + + const res = bruRequestParseAndRedactBodyData(inputBruString); + expect(res.bruFileStringWithRedactedBody).toBe(expectedOutputBruString); + expect(res.extractedBodyContent).toEqual({ + graphql: ` +{ + launchesPast { + launch_site { + site_name + } + launch_success + } +} + `.trim(), + json: ` +{ + "hello": "world" +} + `.trim(), + sparql: ` +SELECT * WHERE { + ?subject ?predicate ?object . +} +LIMIT 10 + `.trim(), + text: `This is a text body`, + xml: ` + + John + 30 + + `.trim() + }) + }); +}); \ No newline at end of file diff --git a/packages/bruno-filestore/src/formats/bru/utils/request-parse-and-redact-body-data.ts b/packages/bruno-filestore/src/formats/bru/utils/request-parse-and-redact-body-data.ts new file mode 100644 index 000000000..6e2313575 --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/utils/request-parse-and-redact-body-data.ts @@ -0,0 +1,77 @@ +/** + * Parses a .bru file and extracts body content while redacting it from the main content + * @param {string} bruFileContent - The raw content of the .bru file + * @returns {Object} Object containing redacted file content and extracted body data + */ +export const bruRequestParseAndRedactBodyData = (bruFileContent: string) => { + try { + // Define the patterns that indicate the start of different body types + const bodyTypePatterns = [ + "body:json {", + "body:text {", + "body:xml {", + "body:sparql {", + "body:graphql {" + ]; + + // Normalize line endings to LF + bruFileContent = (bruFileContent || '').replace(/\r\n/g, '\n'); + + const EOL = `\n`; + + /** + * Removes the leading 2-space indentation from each line of a string + * @param {string} indentedString - The string with leading spaces to remove + * @returns {string} The string with indentation removed + */ + const removeLeadingIndentation = (indentedString: string) => { + if (!indentedString || !indentedString.length) { + return indentedString || ''; + } + + return indentedString + .split(EOL) + .map((line) => line.replace(/^ /, '')) + .join(EOL); + }; + + // Split the file content into blocks + let fileContentBlocks = bruFileContent.split(`${EOL}}${EOL}`); + fileContentBlocks = fileContentBlocks.filter(Boolean).map(_ => _.trim()); + + // Extract body blocks and their content + const extractedBodyBlocks = fileContentBlocks + .filter(block => bodyTypePatterns.some(pattern => block.startsWith(pattern))) + .reduce((bodyContentMap: Record, bodyBlock) => { + // Extract the body type (json, text, xml, etc.) from the first line + const firstLine = bodyBlock.split(EOL)[0]; + const bodyType = firstLine.split(`body:`)[1].split(/\s/)[0]; + + // Extract the body content (everything between the opening and closing braces) + const bodyContentLines = bodyBlock.split(EOL).slice(1); + const rawBodyContent = bodyContentLines.join(EOL); + + // Remove indentation from the body content + const cleanBodyContent = removeLeadingIndentation(rawBodyContent); + + bodyContentMap[bodyType] = cleanBodyContent; + return bodyContentMap; + }, {}); + + // Filter out body blocks to get the remaining file content + const fileContentWithoutBodyBlocks = fileContentBlocks.filter(block => + !bodyTypePatterns.some(pattern => block.startsWith(pattern)) + ); + + return { + bruFileStringWithRedactedBody: fileContentWithoutBodyBlocks.join(`${EOL}}${EOL}${EOL}`).concat(`${EOL}}${EOL}`), + extractedBodyContent: extractedBodyBlocks + }; + } catch (error) { + console.error('Error parsing and redacting body data:', error); + return { + bruFileStringWithRedactedBody: bruFileContent, + extractedBodyContent: {} + }; + } +}; \ No newline at end of file diff --git a/packages/bruno-filestore/src/index.ts b/packages/bruno-filestore/src/index.ts index 2caf3ac4f..2e1ec26d5 100644 --- a/packages/bruno-filestore/src/index.ts +++ b/packages/bruno-filestore/src/index.ts @@ -15,6 +15,7 @@ import { ParsedCollection, ParsedEnvironment } from './types'; +import { bruRequestParseAndRedactBodyData } from './formats/bru/utils/request-parse-and-redact-body-data'; export const parseRequest = (content: string, options: ParseOptions = { format: 'bru' }): any => { if (options.format === 'bru') { @@ -23,6 +24,13 @@ export const parseRequest = (content: string, options: ParseOptions = { format: throw new Error(`Unsupported format: ${options.format}`); }; +export const parseRequestAndRedactBody = (content: string, options: ParseOptions = { format: 'bru' }): any => { + if (options.format === 'bru') { + return bruRequestParseAndRedactBodyData(content); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + export const stringifyRequest = (requestObj: ParsedRequest, options: StringifyOptions = { format: 'bru' }): string => { if (options.format === 'bru') { return jsonRequestToBru(requestObj);