Files
proxmox-git-pve-manager/www/manager6/window/UploadToStorage.js
T
dualfroz f353e94052 fix #6475: ui: upload: only show the file name of the selected file
For privacy reasons, browsers do not expose the real path of a selected
file and prefix its name with 'C:\fakepath\' instead, on every
platform. The file field of the upload window shows this value as is,
which confuses users on Linux or macOS clients.

Show the name of the selected file object instead. Only the displayed
value changes, the upload itself uses the file object and is not
affected.

Signed-off-by: Michal Fox <me@dualfroz.com>
Reviewed-by: Max R. Carrara <m.carrara@proxmox.com>
Tested-by: Max R. Carrara <m.carrara@proxmox.com>
Link: https://lore.proxmox.com/20260926084935.7-1-me@dualfroz.com
2026-09-30 11:27:01 +02:00

333 lines
11 KiB
JavaScript

Ext.define('PVE.window.UploadToStorage', {
extend: 'Ext.window.Window',
alias: 'widget.pveStorageUpload',
mixins: ['Proxmox.Mixin.CBind'],
resizable: false,
modal: true,
title: gettext('Upload'),
acceptedExtensions: {
import: ['.ova', '.qcow2', '.raw', '.vmdk'],
iso: ['.img', '.iso'],
vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
},
// accepted for file selection, will be renamed to real extension
extensionAliases: {
import: {
'.img': '.raw',
},
},
cbindData: function (initialConfig) {
const me = this;
const ext = me.acceptedExtensions[me.content] || [];
me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
let fileSelectorExt = ext.concat(Object.keys(me.extensionAliases[me.content] ?? {}));
return {
extensions: fileSelectorExt.join(', '),
filenameRegex: new RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
};
},
viewModel: {
data: {
size: '-',
mimetype: '-',
filename: '',
},
},
controller: {
submit: function (button) {
const view = this.getView();
const form = this.lookup('formPanel').getForm();
const abortBtn = this.lookup('abortBtn');
const pbar = this.lookup('progressBar');
const updateProgress = function (per, bytes) {
let text = (per * 100).toFixed(2) + '%';
if (bytes) {
text += ' (' + Proxmox.Utils.format_size(bytes) + ')';
}
pbar.updateProgress(per, text);
};
const fd = new FormData();
button.setDisabled(true);
abortBtn.setDisabled(false);
fd.append('content', view.content);
const fileField = form.findField('file');
const file = fileField.fileInputEl.dom.files[0];
fileField.setDisabled(true);
const filenameField = form.findField('filename');
const filename = filenameField.getValue();
filenameField.setDisabled(true);
const algorithmField = form.findField('checksum-algorithm');
algorithmField.setDisabled(true);
if (algorithmField.getValue() !== '__default__') {
fd.append('checksum-algorithm', algorithmField.getValue());
const checksumField = form.findField('checksum');
fd.append('checksum', checksumField.getValue()?.trim());
checksumField.setDisabled(true);
}
fd.append('filename', file, filename);
pbar.setVisible(true);
updateProgress(0);
const xhr = new XMLHttpRequest();
view.xhr = xhr;
xhr.addEventListener(
'load',
function (e) {
if (xhr.status === 200) {
view.hide();
const result = JSON.parse(xhr.response);
const upid = result.data;
Ext.create('Proxmox.window.TaskViewer', {
autoShow: true,
upid: upid,
taskDone: view.taskDone,
listeners: {
destroy: function () {
view.close();
},
},
});
return;
}
const err = Ext.htmlEncode(xhr.statusText);
let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
if (xhr.responseText !== '') {
const result = Ext.decode(xhr.responseText);
result.message = msg;
msg = Proxmox.Utils.extractRequestError(result, true);
}
Ext.Msg.alert(gettext('Error'), msg, (btn) => view.close());
},
false,
);
xhr.addEventListener('error', function (e) {
const err = e.target.status.toString();
const msg = `Error '${err}' occurred while receiving the document.`;
Ext.Msg.alert(gettext('Error'), msg, (btn) => view.close());
});
xhr.upload.addEventListener(
'progress',
function (evt) {
if (evt.lengthComputable) {
const percentComplete = evt.loaded / evt.total;
updateProgress(percentComplete, evt.loaded);
}
},
false,
);
xhr.open('POST', `/api2/json${view.url}`, true);
xhr.send(fd);
},
validitychange: function (f, valid) {
const submitBtn = this.lookup('submitBtn');
submitBtn.setDisabled(!valid);
},
fileChange: function (input) {
const me = this;
const vm = me.getViewModel();
const view = me.getView();
let name = input.value.replace(/^.*(\/|\\)/, '');
for (const [alias, real] of Object.entries(view.extensionAliases[view.content] ?? {})) {
if (name.endsWith(alias)) {
name += real;
}
}
const fileInput = input.fileInputEl.dom;
vm.set('filename', name);
vm.set(
'size',
(fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-',
);
vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
// browsers hide the real path behind 'C:\fakepath\', so only show the file name
input.setRawValue(fileInput.files[0]?.name ?? '');
},
hashChange: function (field, value) {
const checksum = this.lookup('downloadUrlChecksum');
if (value === '__default__') {
checksum.setDisabled(true);
checksum.setValue('');
} else {
checksum.setDisabled(false);
}
},
},
items: [
{
xtype: 'form',
reference: 'formPanel',
method: 'POST',
waitMsgTarget: true,
bodyPadding: 10,
border: false,
width: 400,
fieldDefaults: {
labelWidth: 100,
anchor: '100%',
},
items: [
{
xtype: 'filefield',
name: 'file',
buttonText: gettext('Select File'),
allowBlank: false,
fieldLabel: gettext('File'),
cbind: {
accept: '{extensions}',
},
listeners: {
change: 'fileChange',
},
},
{
xtype: 'textfield',
name: 'filename',
allowBlank: false,
fieldLabel: gettext('File name'),
bind: {
value: '{filename}',
},
cbind: {
regex: '{filenameRegex}',
},
regexText: gettext('Wrong file extension'),
},
{
xtype: 'displayfield',
name: 'size',
fieldLabel: gettext('File size'),
bind: {
value: '{size}',
},
},
{
xtype: 'displayfield',
name: 'mimetype',
fieldLabel: gettext('MIME type'),
bind: {
value: '{mimetype}',
},
},
{
xtype: 'pveHashAlgorithmSelector',
name: 'checksum-algorithm',
fieldLabel: gettext('Hash algorithm'),
allowBlank: true,
hasNoneOption: true,
value: '__default__',
listeners: {
change: 'hashChange',
},
},
{
xtype: 'textfield',
name: 'checksum',
fieldLabel: gettext('Checksum'),
allowBlank: false,
disabled: true,
emptyText: gettext('none'),
reference: 'downloadUrlChecksum',
},
{
xtype: 'displayfield',
userCls: 'pmx-hint',
value: gettext(
"Uploads are stored temporarily in '/var/tmp/', make sure there is enough free space.",
),
},
{
xtype: 'progressbar',
text: 'Ready',
hidden: true,
reference: 'progressBar',
},
{
xtype: 'hiddenfield',
name: 'content',
cbind: {
value: '{content}',
},
},
],
listeners: {
validitychange: 'validitychange',
},
},
],
buttons: [
{
xtype: 'button',
text: gettext('Abort'),
reference: 'abortBtn',
disabled: true,
handler: function () {
const me = this;
me.up('pveStorageUpload').close();
},
},
{
text: gettext('Upload'),
reference: 'submitBtn',
disabled: true,
handler: 'submit',
},
],
listeners: {
close: function () {
const me = this;
if (me.xhr) {
me.xhr.abort();
delete me.xhr;
}
},
},
initComponent: function () {
const me = this;
if (!me.nodename) {
throw 'no node name specified';
}
if (!me.storage) {
throw 'no storage ID specified';
}
if (!me.acceptedExtensions[me.content]) {
throw 'content type not supported';
}
me.callParent();
},
});