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 0a079ee27..7c37256e9 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -7,6 +7,8 @@ const path = require('path');
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');
@@ -25,6 +27,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) {
@@ -860,6 +863,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