Skip to content

Commit c6183c9

Browse files
Fixed more issues found while testing changes for large file download. #3369
1 parent dfd896d commit c6183c9

14 files changed

Lines changed: 163 additions & 149 deletions

File tree

runtime/src/js/downloader.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async function fileDownloadPath(callerWindow, options, prompt=true) {
8282

8383
export function setupDownloader() {
8484
// Listen for the renderer's request to show the open dialog
85-
ipcMain.handle('get-download-path', async (event, options, prompt=true) => {
85+
ipcMain.handle('get-download-stream-path', async (event, options, prompt=true) => {
8686
try {
8787
const callerWindow = BrowserWindow.fromWebContents(event.sender);
8888
const filePath = await fileDownloadPath(callerWindow, options, prompt);
@@ -97,25 +97,25 @@ export function setupDownloader() {
9797

9898
return filePath;
9999
} catch (error) {
100-
writeServerLog(`Error in get-download-path: ${error}`);
100+
writeServerLog(`Error in get-download-stream-path: ${error}`);
101101
}
102102
});
103103

104-
ipcMain.on('download-data-save-total', (event, filePath, total) => {
104+
ipcMain.on('download-stream-save-total', (event, filePath, total) => {
105105
const item = downloadQueue[filePath];
106106
if (item) {
107107
item.setTotal(total);
108108
}
109109
});
110110

111-
ipcMain.on('download-data-save-chunk', (event, filePath, chunk) => {
111+
ipcMain.on('download-stream-save-chunk', (event, filePath, chunk) => {
112112
const item = downloadQueue[filePath];
113113
if (item) {
114114
item.write(chunk);
115115
}
116116
});
117117

118-
ipcMain.on('download-data-save-end', (event, filePath, openFile=false) => {
118+
ipcMain.on('download-stream-save-end', (event, filePath, openFile=false) => {
119119
const item = downloadQueue[filePath];
120120
if (item) {
121121
item.remove();
@@ -124,11 +124,18 @@ export function setupDownloader() {
124124
});
125125

126126
// non-streaming direct download
127-
ipcMain.handle('download-base64-url', async (event, base64url, options, prompt=true, openFile=false) => {
127+
ipcMain.handle('download-base64-url-data', async (event, base64url, options, prompt=true, openFile=false) => {
128128
const callerWindow = BrowserWindow.fromWebContents(event.sender);
129129
const filePath = await fileDownloadPath(callerWindow, options, prompt);
130130
const buffer = Buffer.from(base64url.split(',')[1], 'base64');
131131
fs.writeFileSync(filePath, buffer);
132132
openFile && shell.openPath(filePath);
133133
});
134+
135+
ipcMain.handle('download-text-data', async (event, text, options, prompt=true, openFile=false) => {
136+
const callerWindow = BrowserWindow.fromWebContents(event.sender);
137+
const filePath = await fileDownloadPath(callerWindow, options, prompt);
138+
fs.writeFileSync(filePath, text);
139+
openFile && shell.openPath(filePath);
140+
});
134141
}

runtime/src/js/pgadmin_preload.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
const { contextBridge, ipcRenderer } = require('electron/renderer');
1111

1212
contextBridge.exposeInMainWorld('electronUI', {
13+
focus: () => ipcRenderer.send('focus'),
1314
onMenuClick: (callback) => ipcRenderer.on('menu-click', (_event, details) => callback(details)),
1415
setMenus: (menus) => {
1516
ipcRenderer.send('setMenus', menus);
@@ -25,9 +26,10 @@ contextBridge.exposeInMainWorld('electronUI', {
2526
log: (text)=> ipcRenderer.send('log', text),
2627
reloadApp: ()=>{ipcRenderer.send('reloadApp');},
2728
// Download related functions
28-
getDownloadPath: (...args) => ipcRenderer.invoke('get-download-path', ...args),
29-
downloadDataSaveChunk: (...args) => ipcRenderer.send('download-data-save-chunk', ...args),
30-
downloadDataSaveTotal: (...args) => ipcRenderer.send('download-data-save-total', ...args),
31-
downloadDataSaveEnd: (...args) => ipcRenderer.send('download-data-save-end', ...args),
32-
downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url', ...args)
29+
getDownloadStreamPath: (...args) => ipcRenderer.invoke('get-download-stream-path', ...args),
30+
downloadStreamSaveChunk: (...args) => ipcRenderer.send('download-stream-save-chunk', ...args),
31+
downloadStreamSaveTotal: (...args) => ipcRenderer.send('download-stream-save-total', ...args),
32+
downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args),
33+
downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args),
34+
downloadTextData: (...args) => ipcRenderer.invoke('download-text-data', ...args)
3335
});

web/pgadmin/dashboard/static/js/Dashboard.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import Replication from './Replication';
4040
import { getExpandCell } from '../../../static/js/components/PgReactTableStyled';
4141
import CodeMirror from '../../../static/js/components/ReactCodeMirror';
4242
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
43-
import { downloadTextData } from '../../../static/js/download_utils';
43+
import DownloadUtils from '../../../static/js/DownloadUtils';
4444
import RefreshButton from './components/RefreshButtons';
4545

4646
function parseData(data) {
@@ -451,7 +451,7 @@ function Dashboard({
451451
let fileName = 'data-' + new Date().getTime() + extension;
452452

453453
try {
454-
downloadTextData(respData, fileName, `text/${type}`);
454+
DownloadUtils.downloadTextData(respData, fileName, `text/${type}`);
455455
} catch {
456456
setSsMsg(gettext('Failed to download the logs.'));
457457
}

web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import Uploader from './Uploader';
3232
import GridView from './GridView';
3333
import convert from 'convert-units';
3434
import PropTypes from 'prop-types';
35-
import { downloadBlob } from '../../../../../static/js/download_utils';
35+
import DownloadUtils from '../../../../../static/js/DownloadUtils';
3636
import ErrorBoundary from '../../../../../static/js/helpers/ErrorBoundary';
3737
import { MY_STORAGE } from './FileManagerConstants';
3838
import _ from 'lodash';
@@ -311,7 +311,7 @@ export class FileManagerUtils {
311311
'storage_folder': ss,
312312
},
313313
});
314-
downloadBlob(res.data, res.headers.filename);
314+
DownloadUtils.downloadBlob(res.data, res.headers.filename);
315315
}
316316

317317
setDialogView(view) {

web/pgadmin/static/js/BrowserComponent.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export default function BrowserComponent({pgAdmin}) {
154154
useBeforeUnload({
155155
enabled: confirmOnClose,
156156
beforeClose: (forceClose)=>{
157+
window.electronUI?.focus();
157158
pgAdmin.Browser.notifier.confirm(
158159
gettext('Quit pgAdmin 4'),
159160
gettext('Are you sure you want to quit the application?'),
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//////////////////////////////////////////////////////////////////////////
2+
//
3+
// pgAdmin 4 - PostgreSQL Tools
4+
//
5+
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
6+
// This software is released under the PostgreSQL Licence
7+
//
8+
//////////////////////////////////////////////////////////////////////////
9+
10+
import usePreferences from '../../preferences/static/js/store';
11+
import { callFetch, parseApiError } from './api_instance';
12+
import { getBrowser, toPrettySize } from './utils';
13+
14+
const DownloadUtils = {
15+
downloadViaLink: function (url, fileName) {
16+
const link = document.createElement('a');
17+
link.href = url;
18+
link.download = fileName;
19+
document.body.appendChild(link);
20+
link.click();
21+
document.body.removeChild(link);
22+
},
23+
24+
downloadBlob: function (blob, fileName) {
25+
const urlCreator = window.URL || window.webkitURL;
26+
const downloadUrl = urlCreator.createObjectURL(blob);
27+
28+
this.downloadViaLink(downloadUrl, fileName);
29+
window.URL.revokeObjectURL(downloadUrl);
30+
},
31+
32+
downloadTextData: function (textData, fileName, fileType) {
33+
const respBlob = new Blob([textData], {type : fileType});
34+
this.downloadBlob(respBlob, fileName);
35+
},
36+
37+
downloadBase64UrlData: function (downloadUrl, fileName) {
38+
this.downloadViaLink(downloadUrl, fileName);
39+
},
40+
41+
downloadFileStream: async function (allOptions, fileName, fileType, onProgress) {
42+
const data = [];
43+
const response = await callFetch(allOptions.url, allOptions.options);
44+
if(!response.ok) {
45+
throw new Error(parseApiError(await response.json()));
46+
}
47+
if (!response.body) {
48+
throw new Error(response.statusText);
49+
}
50+
51+
const reader = response.body.getReader();
52+
53+
let done = false;
54+
let receivedLength = 0; // received bytes
55+
while (!done) {
56+
const { value, done: doneReading } = await reader.read();
57+
done = doneReading;
58+
if (value) {
59+
data.push(value);
60+
receivedLength += value.length;
61+
onProgress?.(toPrettySize(receivedLength, 'B', 2));
62+
}
63+
}
64+
65+
const blob = new Blob(data, {type: fileType});
66+
this.downloadBlob(blob, fileName);
67+
}
68+
};
69+
70+
// If we are in Electron, we will use the Electron API to download files.
71+
if(getBrowser().name == 'Electron') {
72+
DownloadUtils.downloadTextData = async (textData, fileName, _fileType) =>{
73+
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
74+
await window.electronUI.downloadTextData(textData, {
75+
defaultPath: fileName,
76+
}, prompt_for_download_location, automatically_open_downloaded_file);
77+
};
78+
79+
DownloadUtils.downloadBase64UrlData = async (downloadUrl, fileName) => {
80+
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
81+
await window.electronUI.downloadBase64UrlData(downloadUrl, {
82+
defaultPath: fileName,
83+
}, prompt_for_download_location, automatically_open_downloaded_file);
84+
};
85+
86+
DownloadUtils.downloadFileStream = async (allOptions, fileName, _fileType, onProgress)=>{
87+
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
88+
const filePath = await window.electronUI.getDownloadStreamPath({
89+
defaultPath: fileName,
90+
}, prompt_for_download_location);
91+
92+
// If the user cancels the download, we will not proceed
93+
if(!filePath) {
94+
return;
95+
}
96+
97+
const response = await callFetch(allOptions.url, allOptions.options);
98+
if(!response.ok) {
99+
throw new Error(parseApiError(await response.json()));
100+
}
101+
if (!response.body) {
102+
throw new Error(response.statusText);
103+
}
104+
105+
const contentLength = response.headers.get('Content-Length');
106+
window.electronUI.downloadStreamSaveTotal(filePath, contentLength ? parseInt(contentLength, 10) : null);
107+
108+
const reader = response.body.getReader();
109+
110+
let done = false;
111+
let receivedLength = 0; // received bytes
112+
while (!done) {
113+
const { value, done: doneReading } = await reader.read();
114+
done = doneReading;
115+
if (value) {
116+
window.electronUI.downloadStreamSaveChunk(filePath, value);
117+
receivedLength += value.length;
118+
onProgress?.(toPrettySize(receivedLength, 'B', 2));
119+
}
120+
}
121+
window.electronUI.downloadStreamSaveEnd(filePath, automatically_open_downloaded_file);
122+
};
123+
}
124+
125+
export default DownloadUtils;

web/pgadmin/static/js/Explain/svg_download.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88
//////////////////////////////////////////////////////////////
99
import getApiInstance from '../api_instance';
10-
import { downloadTextData } from '../download_utils';
10+
import DownloadUtils from '../DownloadUtils';
1111

1212
function convertImageURLtoDataURI(api, image) {
1313
return new Promise(function(resolve, reject) {
@@ -43,6 +43,6 @@ export function downloadSvg(svg, svgName) {
4343
}
4444

4545
Promise.all(image_promises).then(function() {
46-
downloadTextData(svgElement.outerHTML, svgName, 'image/svg+xml');
46+
DownloadUtils.downloadTextData(svgElement.outerHTML, svgName, 'image/svg+xml');
4747
});
4848
}

web/pgadmin/static/js/components/FormComponents.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
149149
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
150150
</InputLabel>;
151151
return (
152-
<StyledGrid container spacing={0} className={className} data-testid="form-input">
152+
<StyledGrid container spacing={0} className={className} data-testid="form-input" width="100%">
153153
<Grid size={{ lg: labelGridBasis, md: labelGridBasis, sm: 12, xs: 12 }}>
154154
{
155155
labelTooltip ?

0 commit comments

Comments
 (0)