From 276c9ce1b011576b6a849f5fd31338f821e571bc Mon Sep 17 00:00:00 2001 From: Martin Sefcik Date: Tue, 24 Oct 2023 17:04:29 +0200 Subject: [PATCH] added possibility to save response to file --- package-lock.json | 4 +- .../ResponseSave/StyledWrapper.js | 7 +++ .../ResponsePane/ResponseSave/index.js | 31 ++++++++++++ .../src/components/ResponsePane/index.js | 2 + packages/bruno-electron/package.json | 2 + .../bruno-electron/src/ipc/network/index.js | 48 +++++++++++++++++++ .../bruno-electron/src/utils/filesystem.js | 18 +++++++ 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseSave/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseSave/index.js diff --git a/package-lock.json b/package-lock.json index 5dddfd545..cc6e6e065 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16630,7 +16630,7 @@ }, "packages/bruno-electron": { "name": "bruno", - "version": "v0.27.1", + "version": "v0.27.2", "dependencies": { "@aws-sdk/credential-providers": "^3.425.0", "@usebruno/js": "0.9.1", @@ -16641,6 +16641,7 @@ "axios": "^1.5.1", "chai": "^4.3.7", "chokidar": "^3.5.3", + "content-disposition": "^0.5.4", "decomment": "^0.9.5", "dotenv": "^16.0.3", "electron-is-dev": "^2.0.0", @@ -21519,6 +21520,7 @@ "axios": "^1.5.1", "chai": "^4.3.7", "chokidar": "^3.5.3", + "content-disposition": "^0.5.4", "decomment": "^0.9.5", "dmg-license": "^1.0.11", "dotenv": "^16.0.3", diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSave/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ResponseSave/StyledWrapper.js new file mode 100644 index 000000000..964505d38 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSave/StyledWrapper.js @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + font-size: 0.8125rem; +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSave/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseSave/index.js new file mode 100644 index 000000000..07f779b4a --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSave/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import StyledWrapper from './StyledWrapper'; +import toast from 'react-hot-toast'; +import get from 'lodash/get'; + +const ResponseSave = ({ item }) => { + const { ipcRenderer } = window; + const response = item.response || {}; + + const saveResponseToFile = () => { + return new Promise((resolve, reject) => { + console.log(item); + ipcRenderer + .invoke('renderer:save-response-to-file', response, item.requestSent.url) + .then(resolve) + .catch((err) => { + toast.error(get(err, 'error.message') || 'Something went wrong!'); + reject(err); + }); + }); + }; + + return ( + + + + ); +}; +export default ResponseSave; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index dbae1550a..c9d5be92b 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -14,6 +14,7 @@ import Timeline from './Timeline'; import TestResults from './TestResults'; import TestResultsLabel from './TestResultsLabel'; import StyledWrapper from './StyledWrapper'; +import ResponseSave from 'src/components/ResponsePane/ResponseSave'; const ResponsePane = ({ rightPaneWidth, item, collection }) => { const dispatch = useDispatch(); @@ -115,6 +116,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { + ) : null} diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 83f6005c9..47753da5a 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -28,6 +28,7 @@ "axios": "^1.5.1", "chai": "^4.3.7", "chokidar": "^3.5.3", + "content-disposition": "^0.5.4", "decomment": "^0.9.5", "dotenv": "^16.0.3", "electron-is-dev": "^2.0.0", @@ -43,6 +44,7 @@ "is-valid-path": "^0.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "mime-types": "^2.1.35", "mustache": "^4.2.0", "nanoid": "3.3.4", "node-machine-id": "^1.1.12", diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index daace4792..5192fca0b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -6,6 +6,8 @@ const axios = require('axios'); const decomment = require('decomment'); const Mustache = require('mustache'); const FormData = require('form-data'); +const contentDispositionParser = require('content-disposition'); +const mime = require('mime-types'); const { ipcMain } = require('electron'); const { forOwn, extend, each, get, compact } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); @@ -24,6 +26,7 @@ const { SocksProxyAgent } = require('socks-proxy-agent'); const { makeAxiosInstance } = require('./axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); +const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem'); // override the default escape function to prevent escaping Mustache.escape = function (value) { @@ -838,6 +841,51 @@ const registerNetworkIpc = (mainWindow) => { } } ); + + // save response to file + ipcMain.handle('renderer:save-response-to-file', async (event, response, url) => { + try { + const getHeaderValue = (headerName) => { + if (response.headers) { + const header = response.headers.find((header) => header[0] === headerName); + if (header && header.length > 1) { + return header[1]; + } + } + }; + + const getFileNameFromContentDispositionHeader = () => { + const contentDisposition = getHeaderValue('content-disposition'); + try { + const disposition = contentDispositionParser.parse(contentDisposition); + return disposition && disposition.parameters['filename']; + } catch (error) {} + }; + + const getFileNameFromUrlPath = () => { + const lastPathLevel = new URL(url).pathname.split('/').pop(); + if (lastPathLevel && /\..+/.exec(lastPathLevel)) { + return lastPathLevel; + } + }; + + const getFileNameBasedOnContentTypeHeader = () => { + const contentType = getHeaderValue('content-type'); + const extension = (contentType && mime.extension(contentType)) || 'txt'; + return `response.${extension}`; + }; + + const fileName = + getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader(); + + const filePath = await chooseFileToSave(mainWindow, fileName); + if (filePath) { + await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64')); + } + } catch (error) { + return Promise.reject(error); + } + }); }; module.exports = registerNetworkIpc; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index b55dfd725..4f3ea980b 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -60,6 +60,14 @@ const writeFile = async (pathname, content) => { } }; +const writeBinaryFile = async (pathname, content) => { + try { + fs.writeFileSync(pathname, content); + } catch (err) { + return Promise.reject(err); + } +}; + const hasJsonExtension = (filename) => { if (!filename || typeof filename !== 'string') return false; return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`)); @@ -95,6 +103,14 @@ const browseDirectory = async (win) => { return isDirectory(resolvedPath) ? resolvedPath : false; }; +const chooseFileToSave = async (win, preferredFileName = '') => { + const { filePath } = await dialog.showSaveDialog(win, { + defaultPath: preferredFileName + }); + + return filePath; +}; + const searchForFiles = (dir, extension) => { let results = []; const files = fs.readdirSync(dir); @@ -126,10 +142,12 @@ module.exports = { isDirectory, normalizeAndResolvePath, writeFile, + writeBinaryFile, hasJsonExtension, hasBruExtension, createDirectory, browseDirectory, + chooseFileToSave, searchForFiles, searchForBruFiles, sanitizeDirectoryName