diff --git a/runtime/src/js/downloader.js b/runtime/src/js/downloader.js
index 58413e4f3..71d95b789 100644
--- a/runtime/src/js/downloader.js
+++ b/runtime/src/js/downloader.js
@@ -82,7 +82,7 @@ async function fileDownloadPath(callerWindow, options, prompt=true) {
export function setupDownloader() {
// Listen for the renderer's request to show the open dialog
- ipcMain.handle('get-download-path', async (event, options, prompt=true) => {
+ ipcMain.handle('get-download-stream-path', async (event, options, prompt=true) => {
try {
const callerWindow = BrowserWindow.fromWebContents(event.sender);
const filePath = await fileDownloadPath(callerWindow, options, prompt);
@@ -97,25 +97,25 @@ export function setupDownloader() {
return filePath;
} catch (error) {
- writeServerLog(`Error in get-download-path: ${error}`);
+ writeServerLog(`Error in get-download-stream-path: ${error}`);
}
});
- ipcMain.on('download-data-save-total', (event, filePath, total) => {
+ ipcMain.on('download-stream-save-total', (event, filePath, total) => {
const item = downloadQueue[filePath];
if (item) {
item.setTotal(total);
}
});
- ipcMain.on('download-data-save-chunk', (event, filePath, chunk) => {
+ ipcMain.on('download-stream-save-chunk', (event, filePath, chunk) => {
const item = downloadQueue[filePath];
if (item) {
item.write(chunk);
}
});
- ipcMain.on('download-data-save-end', (event, filePath, openFile=false) => {
+ ipcMain.on('download-stream-save-end', (event, filePath, openFile=false) => {
const item = downloadQueue[filePath];
if (item) {
item.remove();
@@ -124,11 +124,18 @@ export function setupDownloader() {
});
// non-streaming direct download
- ipcMain.handle('download-base64-url', async (event, base64url, options, prompt=true, openFile=false) => {
+ ipcMain.handle('download-base64-url-data', async (event, base64url, options, prompt=true, openFile=false) => {
const callerWindow = BrowserWindow.fromWebContents(event.sender);
const filePath = await fileDownloadPath(callerWindow, options, prompt);
const buffer = Buffer.from(base64url.split(',')[1], 'base64');
fs.writeFileSync(filePath, buffer);
openFile && shell.openPath(filePath);
});
+
+ ipcMain.handle('download-text-data', async (event, text, options, prompt=true, openFile=false) => {
+ const callerWindow = BrowserWindow.fromWebContents(event.sender);
+ const filePath = await fileDownloadPath(callerWindow, options, prompt);
+ fs.writeFileSync(filePath, text);
+ openFile && shell.openPath(filePath);
+ });
}
\ No newline at end of file
diff --git a/runtime/src/js/pgadmin_preload.js b/runtime/src/js/pgadmin_preload.js
index 240c96a32..e28a215f7 100644
--- a/runtime/src/js/pgadmin_preload.js
+++ b/runtime/src/js/pgadmin_preload.js
@@ -10,6 +10,7 @@
const { contextBridge, ipcRenderer } = require('electron/renderer');
contextBridge.exposeInMainWorld('electronUI', {
+ focus: () => ipcRenderer.send('focus'),
onMenuClick: (callback) => ipcRenderer.on('menu-click', (_event, details) => callback(details)),
setMenus: (menus) => {
ipcRenderer.send('setMenus', menus);
@@ -25,9 +26,10 @@ contextBridge.exposeInMainWorld('electronUI', {
log: (text)=> ipcRenderer.send('log', text),
reloadApp: ()=>{ipcRenderer.send('reloadApp');},
// Download related functions
- getDownloadPath: (...args) => ipcRenderer.invoke('get-download-path', ...args),
- downloadDataSaveChunk: (...args) => ipcRenderer.send('download-data-save-chunk', ...args),
- downloadDataSaveTotal: (...args) => ipcRenderer.send('download-data-save-total', ...args),
- downloadDataSaveEnd: (...args) => ipcRenderer.send('download-data-save-end', ...args),
- downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url', ...args)
+ getDownloadStreamPath: (...args) => ipcRenderer.invoke('get-download-stream-path', ...args),
+ downloadStreamSaveChunk: (...args) => ipcRenderer.send('download-stream-save-chunk', ...args),
+ downloadStreamSaveTotal: (...args) => ipcRenderer.send('download-stream-save-total', ...args),
+ downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args),
+ downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args),
+ downloadTextData: (...args) => ipcRenderer.invoke('download-text-data', ...args)
});
\ No newline at end of file
diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx
index a18e4cdff..32a634c08 100644
--- a/web/pgadmin/dashboard/static/js/Dashboard.jsx
+++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx
@@ -40,7 +40,7 @@ import Replication from './Replication';
import { getExpandCell } from '../../../static/js/components/PgReactTableStyled';
import CodeMirror from '../../../static/js/components/ReactCodeMirror';
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
-import { downloadTextData } from '../../../static/js/download_utils';
+import DownloadUtils from '../../../static/js/DownloadUtils';
import RefreshButton from './components/RefreshButtons';
function parseData(data) {
@@ -451,7 +451,7 @@ function Dashboard({
let fileName = 'data-' + new Date().getTime() + extension;
try {
- downloadTextData(respData, fileName, `text/${type}`);
+ DownloadUtils.downloadTextData(respData, fileName, `text/${type}`);
} catch {
setSsMsg(gettext('Failed to download the logs.'));
}
diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx
index c189e2275..d2fae0a60 100644
--- a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx
+++ b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx
@@ -32,7 +32,7 @@ import Uploader from './Uploader';
import GridView from './GridView';
import convert from 'convert-units';
import PropTypes from 'prop-types';
-import { downloadBlob } from '../../../../../static/js/download_utils';
+import DownloadUtils from '../../../../../static/js/DownloadUtils';
import ErrorBoundary from '../../../../../static/js/helpers/ErrorBoundary';
import { MY_STORAGE } from './FileManagerConstants';
import _ from 'lodash';
@@ -311,7 +311,7 @@ export class FileManagerUtils {
'storage_folder': ss,
},
});
- downloadBlob(res.data, res.headers.filename);
+ DownloadUtils.downloadBlob(res.data, res.headers.filename);
}
setDialogView(view) {
diff --git a/web/pgadmin/static/js/BrowserComponent.jsx b/web/pgadmin/static/js/BrowserComponent.jsx
index d0e843de9..63ae4cda9 100644
--- a/web/pgadmin/static/js/BrowserComponent.jsx
+++ b/web/pgadmin/static/js/BrowserComponent.jsx
@@ -154,6 +154,7 @@ export default function BrowserComponent({pgAdmin}) {
useBeforeUnload({
enabled: confirmOnClose,
beforeClose: (forceClose)=>{
+ window.electronUI?.focus();
pgAdmin.Browser.notifier.confirm(
gettext('Quit pgAdmin 4'),
gettext('Are you sure you want to quit the application?'),
diff --git a/web/pgadmin/static/js/DownloadUtils.js b/web/pgadmin/static/js/DownloadUtils.js
new file mode 100644
index 000000000..e5a0dfdf3
--- /dev/null
+++ b/web/pgadmin/static/js/DownloadUtils.js
@@ -0,0 +1,125 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2025, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import usePreferences from '../../preferences/static/js/store';
+import { callFetch, parseApiError } from './api_instance';
+import { getBrowser, toPrettySize } from './utils';
+
+const DownloadUtils = {
+ downloadViaLink: function (url, fileName) {
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ },
+
+ downloadBlob: function (blob, fileName) {
+ const urlCreator = window.URL || window.webkitURL;
+ const downloadUrl = urlCreator.createObjectURL(blob);
+
+ this.downloadViaLink(downloadUrl, fileName);
+ window.URL.revokeObjectURL(downloadUrl);
+ },
+
+ downloadTextData: function (textData, fileName, fileType) {
+ const respBlob = new Blob([textData], {type : fileType});
+ this.downloadBlob(respBlob, fileName);
+ },
+
+ downloadBase64UrlData: function (downloadUrl, fileName) {
+ this.downloadViaLink(downloadUrl, fileName);
+ },
+
+ downloadFileStream: async function (allOptions, fileName, fileType, onProgress) {
+ const data = [];
+ const response = await callFetch(allOptions.url, allOptions.options);
+ if(!response.ok) {
+ throw new Error(parseApiError(await response.json()));
+ }
+ if (!response.body) {
+ throw new Error(response.statusText);
+ }
+
+ const reader = response.body.getReader();
+
+ let done = false;
+ let receivedLength = 0; // received bytes
+ while (!done) {
+ const { value, done: doneReading } = await reader.read();
+ done = doneReading;
+ if (value) {
+ data.push(value);
+ receivedLength += value.length;
+ onProgress?.(toPrettySize(receivedLength, 'B', 2));
+ }
+ }
+
+ const blob = new Blob(data, {type: fileType});
+ this.downloadBlob(blob, fileName);
+ }
+};
+
+// If we are in Electron, we will use the Electron API to download files.
+if(getBrowser().name == 'Electron') {
+ DownloadUtils.downloadTextData = async (textData, fileName, _fileType) =>{
+ const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
+ await window.electronUI.downloadTextData(textData, {
+ defaultPath: fileName,
+ }, prompt_for_download_location, automatically_open_downloaded_file);
+ };
+
+ DownloadUtils.downloadBase64UrlData = async (downloadUrl, fileName) => {
+ const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
+ await window.electronUI.downloadBase64UrlData(downloadUrl, {
+ defaultPath: fileName,
+ }, prompt_for_download_location, automatically_open_downloaded_file);
+ };
+
+ DownloadUtils.downloadFileStream = async (allOptions, fileName, _fileType, onProgress)=>{
+ const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
+ const filePath = await window.electronUI.getDownloadStreamPath({
+ defaultPath: fileName,
+ }, prompt_for_download_location);
+
+ // If the user cancels the download, we will not proceed
+ if(!filePath) {
+ return;
+ }
+
+ const response = await callFetch(allOptions.url, allOptions.options);
+ if(!response.ok) {
+ throw new Error(parseApiError(await response.json()));
+ }
+ if (!response.body) {
+ throw new Error(response.statusText);
+ }
+
+ const contentLength = response.headers.get('Content-Length');
+ window.electronUI.downloadStreamSaveTotal(filePath, contentLength ? parseInt(contentLength, 10) : null);
+
+ const reader = response.body.getReader();
+
+ let done = false;
+ let receivedLength = 0; // received bytes
+ while (!done) {
+ const { value, done: doneReading } = await reader.read();
+ done = doneReading;
+ if (value) {
+ window.electronUI.downloadStreamSaveChunk(filePath, value);
+ receivedLength += value.length;
+ onProgress?.(toPrettySize(receivedLength, 'B', 2));
+ }
+ }
+ window.electronUI.downloadStreamSaveEnd(filePath, automatically_open_downloaded_file);
+ };
+}
+
+export default DownloadUtils;
diff --git a/web/pgadmin/static/js/Explain/svg_download.js b/web/pgadmin/static/js/Explain/svg_download.js
index 1d1f724ae..078ec95fd 100644
--- a/web/pgadmin/static/js/Explain/svg_download.js
+++ b/web/pgadmin/static/js/Explain/svg_download.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import getApiInstance from '../api_instance';
-import { downloadTextData } from '../download_utils';
+import DownloadUtils from '../DownloadUtils';
function convertImageURLtoDataURI(api, image) {
return new Promise(function(resolve, reject) {
@@ -43,6 +43,6 @@ export function downloadSvg(svg, svgName) {
}
Promise.all(image_promises).then(function() {
- downloadTextData(svgElement.outerHTML, svgName, 'image/svg+xml');
+ DownloadUtils.downloadTextData(svgElement.outerHTML, svgName, 'image/svg+xml');
});
}
diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx
index 0a5758ac1..ea517f1f1 100644
--- a/web/pgadmin/static/js/components/FormComponents.jsx
+++ b/web/pgadmin/static/js/components/FormComponents.jsx
@@ -149,7 +149,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
;
return (
-
+
{
labelTooltip ?
diff --git a/web/pgadmin/static/js/download_utils.js b/web/pgadmin/static/js/download_utils.js
deleted file mode 100644
index 2114446df..000000000
--- a/web/pgadmin/static/js/download_utils.js
+++ /dev/null
@@ -1,121 +0,0 @@
-//////////////////////////////////////////////////////////////////////////
-//
-// pgAdmin 4 - PostgreSQL Tools
-//
-// Copyright (C) 2013 - 2025, The pgAdmin Development Team
-// This software is released under the PostgreSQL Licence
-//
-//////////////////////////////////////////////////////////////////////////
-
-import usePreferences from '../../preferences/static/js/store';
-import { callFetch, parseApiError } from './api_instance';
-import { getBrowser, toPrettySize } from './utils';
-
-// This function is used to download the base64 data
-// and create a link to download the file.
-export async function downloadBase64UrlData(downloadUrl, fileName) {
- if(getBrowser().name == 'Electron') {
- const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
- // In Electron, we use the electronUI to download the file.
- await window.electronUI.downloadBase64UrlData(downloadUrl, {
- defaultPath: fileName,
- }, prompt_for_download_location, automatically_open_downloaded_file);
- return;
- }
- // In other browsers, we create a link to download the file.
- let link = document.createElement('a');
- link.setAttribute('href', downloadUrl);
- link.setAttribute('download', fileName);
- link.style.setProperty('visibility ', 'hidden');
-
- document.body.appendChild(link);
- link.click();
- link.remove();
-}
-
-// This function is used to download the blob data
-export async function downloadBlob(blob, fileName) {
- const urlCreator = window.URL || window.webkitURL;
- const downloadUrl = urlCreator.createObjectURL(blob);
-
- downloadBase64UrlData(downloadUrl, fileName);
- window.URL.revokeObjectURL(downloadUrl);
-}
-
-// This function is used to download the text data
-export function downloadTextData(textData, fileName, fileType) {
- const respBlob = new Blob([textData], {type : fileType});
- downloadBlob(respBlob, fileName);
-}
-
-// This function is used to download the file from the given URL
-// and use streaming to download the file in chunks where there
-// is no limit on the file size.
-export async function downloadFileStream(allOptions, fileName, fileType, onProgress) {
- const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
-
- const start = async (filePath, writer) => {
- const response = await callFetch(allOptions.url, allOptions.options);
- if(!response.ok) {
- throw new Error(parseApiError(await response.json()));
- }
- if (!response.body) {
- throw new Error(response.statusText);
- }
-
- const contentLength = response.headers.get('Content-Length');
- writer.downloadDataSaveTotal(filePath, contentLength ? parseInt(contentLength, 10) : null);
-
- const reader = response.body.getReader();
-
- let done = false;
- let receivedLength = 0; // received bytes
- while (!done) {
- const { value, done: doneReading } = await reader.read();
- done = doneReading;
- if (value) {
- writer.downloadDataSaveChunk(filePath, value);
- receivedLength += value.length;
- onProgress?.(toPrettySize(receivedLength, 'B', 2));
- }
- }
- writer.downloadDataSaveEnd(filePath, automatically_open_downloaded_file);
- };
-
- let writer;
- let filePath = '';
- try {
- if(getBrowser().name != 'Electron') {
- // In other browsers, we use the blob to download the file.
- const data = [];
- writer = {
- downloadDataSaveChunk: (_fp, chunk) => {
- data.push(chunk);
- },
- downloadDataSaveTotal: () => {
- // This is not used in the browser
- },
- downloadDataSaveEnd: () => {
- // This is not used in the browser
- },
- };
- await start(filePath, writer);
- const blob = new Blob(data, {type: fileType});
- downloadBlob(blob, fileName);
- } else {
- writer = window.electronUI;
- filePath = await window.electronUI.getDownloadPath({
- defaultPath: fileName,
- }, prompt_for_download_location);
-
- // If the user cancels the download, we will not proceed
- if(!filePath) {
- return;
- }
- await start(filePath, writer);
- }
- } catch (error) {
- writer.downloadDataSaveEnd(filePath);
- throw new Error('Download failed: ' + error.message);
- }
-}
diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js
index 7c39f6674..40cfcfa66 100644
--- a/web/pgadmin/static/js/utils.js
+++ b/web/pgadmin/static/js/utils.js
@@ -596,7 +596,7 @@ export function getRandomColor() {
// Using this function instead of 'btoa' directly.
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
-function stringToBase64(str) {
+export function stringToBase64(str) {
return btoa(
Array.from(
new TextEncoder().encode(str),
diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx
index dabab6e03..429a0b0ad 100644
--- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx
+++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx
@@ -37,7 +37,7 @@ import pgAdmin from 'sources/pgadmin';
import { styled } from '@mui/material/styles';
import BeforeUnload from './BeforeUnload';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
-import { downloadBase64UrlData } from '../../../../../../static/js/download_utils';
+import DownloadUtils from '../../../../../../static/js/DownloadUtils';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@@ -760,7 +760,7 @@ export default class ERDTool extends React.Component {
}
toPng(this.canvasEle, {width, height})
.then((dataUrl)=>{
- downloadBase64UrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
+ DownloadUtils.downloadBase64UrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
}).catch((err)=>{
console.error(err);
let msg = gettext('Unknown error. Check console logs');
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx
index 4bcb5969e..08c86f994 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx
@@ -26,7 +26,7 @@ import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE,
LightenDarkenColor} from 'sources/chartjs';
import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
-import { downloadBase64UrlData } from '../../../../../../static/js/download_utils';
+import DownloadUtils from '../../../../../../static/js/DownloadUtils';
import { getChartColor } from '../../../../../../static/js/utils';
const StyledBox = styled(Box)(({theme}) => ({
@@ -381,7 +381,7 @@ export function GraphVisualiser({initColumns}) {
const onDownloadGraph = async ()=> {
let downloadUrl = chartObjRef.current.toBase64Image(),
fileName = 'graph_visualiser-' + new Date().getTime() + '.png';
- downloadBase64UrlData(downloadUrl, fileName);
+ DownloadUtils.downloadBase64UrlData(downloadUrl, fileName);
};
// This plugin is used to set the background color of the canvas. Very useful
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
index a8ee0d5d1..6c17b7e0e 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
@@ -22,7 +22,7 @@ import { LayoutDockerContext } from '../../../../../../static/js/helpers/Layout'
import { GeometryViewer } from './GeometryViewer';
import Explain from '../../../../../../static/js/Explain';
import { QuerySources } from './QueryHistory';
-import { downloadFileStream } from '../../../../../../static/js/download_utils';
+import DownloadUtils from '../../../../../../static/js/DownloadUtils';
import CopyData from '../QueryToolDataGrid/CopyData';
import moment from 'moment';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
@@ -516,7 +516,7 @@ export class ResultSetUtils {
async saveResultsToFile(fileName, onProgress) {
try {
this.hasQueryCommitted = false;
- await downloadFileStream({
+ await DownloadUtils.downloadFileStream({
url: url_for('sqleditor.query_tool_download', {
'trans_id': this.transId,
}),
diff --git a/web/regression/javascript/file_manager/FileManager.spec.js b/web/regression/javascript/file_manager/FileManager.spec.js
index 9a101a52f..ad43af7a4 100644
--- a/web/regression/javascript/file_manager/FileManager.spec.js
+++ b/web/regression/javascript/file_manager/FileManager.spec.js
@@ -16,7 +16,7 @@ import FileManager, { FileManagerUtils, getComparator } from '../../../pgadmin/m
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import getApiInstance from '../../../pgadmin/static/js/api_instance';
-import * as downloadUtils from '../../../pgadmin/static/js/download_utils';
+import DownloadUtils from '../../../pgadmin/static/js/DownloadUtils';
import userEvent from '@testing-library/user-event';
const files = [
@@ -345,9 +345,9 @@ describe('FileManagerUtils', ()=>{
});
it('downloadFile', async ()=>{
- jest.spyOn(downloadUtils, 'downloadBlob').mockImplementation(() => {});
+ jest.spyOn(DownloadUtils, 'downloadBlob').mockImplementation(() => {});
let row = {Filename: 'newfile1', Path: '/home/newfile1', 'storage_folder': 'my_storage'};
await fmObj.downloadFile(row);
- expect(downloadUtils.downloadBlob).toHaveBeenCalledWith('blobdata', 'newfile1');
+ expect(DownloadUtils.downloadBlob).toHaveBeenCalledWith('blobdata', 'newfile1');
});
});