Files
dualfroz 11c2368e04 fix #8015: ui: download url: let the user choose the .img image format
Like for the upload, '.raw' was appended to the name of '.img' images
downloaded as import content, which breaks importing images in other
formats, such as the qcow2 based Ubuntu cloud images.

After querying the URL of such an image, show a format selector and
append the extension of the selected format to the file name instead.

Signed-off-by: Michal Fox <me@dualfroz.com>
Link: https://lore.proxmox.com/20260929135840.2748665-3-me@dualfroz.com
2026-09-30 11:39:38 +02:00

294 lines
9.7 KiB
JavaScript

Ext.define('PVE.window.DownloadUrlToStorage', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pveStorageDownloadUrl',
mixins: ['Proxmox.Mixin.CBind'],
isCreate: true,
method: 'POST',
showTaskViewer: true,
title: gettext('Download from URL'),
submitText: gettext('Download'),
cbindData: function (initialConfig) {
var me = this;
return {
nodename: me.nodename,
storage: me.storage,
content: me.content,
};
},
cbind: {
url: '/nodes/{nodename}/storage/{storage}/download-url',
},
viewModel: {
data: {
size: '-',
mimetype: '-',
enableQuery: true,
needsFormat: false,
},
},
controller: {
xclass: 'Ext.app.ViewController',
urlChange: function (field) {
this.resetMetaInfo();
this.setQueryEnabled();
},
setQueryEnabled: function () {
this.getViewModel().set('enableQuery', true);
},
resetMetaInfo: function () {
let vm = this.getViewModel();
vm.set('size', '-');
vm.set('mimetype', '-');
},
urlCheck: function (field) {
let me = this;
let view = me.getView();
const queryParam = view.getValues();
me.getViewModel().set('enableQuery', false);
me.resetMetaInfo();
let urlField = view.down('[name=url]');
Proxmox.Utils.API2Request({
url: `/nodes/${view.nodename}/query-url-metadata`,
method: 'GET',
params: {
url: queryParam.url,
'verify-certificates': queryParam['verify-certificates'],
},
waitMsgTarget: view,
failure: (res) => {
urlField.setValidation(res.result.message);
urlField.validate();
Ext.MessageBox.alert(gettext('Error'), res.htmlStatus);
// re-enable so one can directly requery, e.g., if it was just a network hiccup
me.setQueryEnabled();
},
success: function (res, opt) {
urlField.setValidation();
urlField.validate();
let data = res.result.data;
let filename = data.filename || '';
let compression = '__default__';
let needsFormat = false;
if (view.content === 'iso') {
const matches = filename.match(/^(.+)\.(gz|lzo|zst|bz2)$/i);
if (matches) {
filename = matches[1];
compression = matches[2].toLowerCase();
}
} else if (view.content === 'import') {
// the extension does not tell the image format, the user has to select it
needsFormat = filename.endsWith('.img');
}
me.queriedName = filename;
me.getViewModel().set('needsFormat', needsFormat);
me.lookup('imageFormat').setValue(null);
view.setValues({
filename,
compression,
size:
(data.size && Proxmox.Utils.format_size(data.size)) ||
gettext('Unknown'),
mimetype: data.mimetype || gettext('Unknown'),
});
},
});
},
formatChange: function (field, format) {
if (format) {
this.getView().setValues({ filename: `${this.queriedName}.${format}` });
}
},
hashChange: function (field) {
let checksum = Ext.getCmp('downloadUrlChecksum');
if (field.getValue() === '__default__') {
checksum.setDisabled(true);
checksum.setValue('');
checksum.allowBlank = true;
} else {
checksum.setDisabled(false);
checksum.allowBlank = false;
}
},
},
items: [
{
xtype: 'inputpanel',
border: false,
onGetValues: function (values) {
if (typeof values.checksum === 'string') {
values.checksum = values.checksum.trim();
}
return values;
},
columnT: [
{
xtype: 'fieldcontainer',
layout: 'hbox',
fieldLabel: gettext('URL'),
items: [
{
xtype: 'textfield',
name: 'url',
emptyText: gettext('Enter URL to download'),
allowBlank: false,
flex: 1,
listeners: {
change: 'urlChange',
},
},
{
xtype: 'button',
name: 'check',
text: gettext('Query URL'),
margin: '0 0 0 5',
bind: {
disabled: '{!enableQuery}',
},
listeners: {
click: 'urlCheck',
},
},
],
},
{
xtype: 'textfield',
name: 'filename',
allowBlank: false,
fieldLabel: gettext('File name'),
emptyText: gettext('Please (re-)query URL to get meta information'),
getSubmitValue: function () {
return this.processRawValue(this.getRawValue())?.trim();
},
},
{
xtype: 'pveDiskFormatSelector',
reference: 'imageFormat',
fieldLabel: gettext('Format'),
submitValue: false,
allowBlank: false,
hidden: true,
disabled: true,
bind: {
hidden: '{!needsFormat}',
disabled: '{!needsFormat}',
},
listeners: {
change: 'formatChange',
},
},
],
column1: [
{
xtype: 'displayfield',
name: 'size',
fieldLabel: gettext('File size'),
bind: {
value: '{size}',
},
},
],
column2: [
{
xtype: 'displayfield',
name: 'mimetype',
fieldLabel: gettext('MIME type'),
bind: {
value: '{mimetype}',
},
},
],
advancedColumn1: [
{
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: true,
disabled: true,
emptyText: gettext('none'),
id: 'downloadUrlChecksum',
},
],
advancedColumn2: [
{
xtype: 'proxmoxcheckbox',
name: 'verify-certificates',
fieldLabel: gettext('Verify certificates'),
uncheckedValue: 0,
checked: true,
listeners: {
change: 'setQueryEnabled',
},
},
{
xtype: 'proxmoxKVComboBox',
name: 'compression',
fieldLabel: gettext('Decompression algorithm'),
allowBlank: true,
hasNoneOption: true,
deleteEmpty: false,
value: '__default__',
comboItems: [
['__default__', Proxmox.Utils.NoneText],
['lzo', 'LZO'],
['gz', 'GZIP'],
['zst', 'ZSTD'],
['bz2', 'BZIP2'],
],
cbind: {
hidden: (get) => get('content') !== 'iso',
},
},
],
},
{
xtype: 'hiddenfield',
name: 'content',
cbind: {
value: '{content}',
},
},
],
initComponent: function () {
var me = this;
if (!me.nodename) {
throw 'no node name specified';
}
if (!me.storage) {
throw 'no storage ID specified';
}
me.callParent();
},
});