Add PVE.storage.Retrieve window and PVE.form.hashAlgorithmSelector.
Users are now able to download/retrieve any .iso/... file onto their
storages and verify file integrity with checksums.

Add new method: GET /nodes/{node}/urlmeta - returns url metadata

Signed-off-by: Lorenz Stechauner <l.stechau...@proxmox.com>
---
 PVE/API2/Nodes.pm                          |  97 +++++++
 www/manager6/Makefile                      |   1 +
 www/manager6/form/HashAlgorithmSelector.js |  16 ++
 www/manager6/storage/Browser.js            |   8 +
 www/manager6/storage/ContentView.js        | 281 +++++++++++++++++++--
 5 files changed, 378 insertions(+), 25 deletions(-)
 create mode 100644 www/manager6/form/HashAlgorithmSelector.js

diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index ba6621c6..c2407d99 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -11,6 +11,7 @@ use JSON;
 use POSIX qw(LONG_MAX);
 use Time::Local qw(timegm_nocheck);
 use Socket;
+use IO::Socket::SSL;
 
 use PVE::API2Tools;
 use PVE::APLInfo;
@@ -254,6 +255,7 @@ __PACKAGE__->register_method ({
            { name => 'tasks' },
            { name => 'termproxy' },
            { name => 'time' },
+           { name => 'urlmeta' },
            { name => 'version' },
            { name => 'vncshell' },
            { name => 'vzdump' },
@@ -1596,6 +1598,101 @@ __PACKAGE__->register_method({
        return $rpcenv->fork_worker('download', undef, $user, $worker);
     }});
 
+
+__PACKAGE__->register_method({
+    name => 'urlmeta',
+    path => 'urlmeta',
+    method => 'GET',
+    description => "Download templates and ISO images by using an URL.",
+    proxyto => 'node',
+    permissions => {
+       check => ['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
+    },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           url => {
+               description => "The URL to retrieve the file from.",
+               type => 'string',
+           },
+           insecure => {
+               description => "Allow TLS certificates to be invalid.",
+               type => 'boolean',
+               optional => 1,
+           }
+       },
+    },
+    returns => {
+       type => "object",
+       properties => {
+           filename => {
+               type => 'string',
+               optional => 1,
+           },
+           size => {
+               type => 'integer',
+               renderer => 'bytes',
+               optional => 1,
+           },
+           mimetype => {
+               type => 'string',
+               optional => 1,
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $url = $param->{url};
+
+       die "invalid https or http url"
+           if $url !~ qr!^https?://!;
+
+       my $ua = LWP::UserAgent->new();
+       $ua->ssl_opts(
+           verify_hostname => 0,
+           SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
+       ) if $param->{insecure};
+
+       my $req = HTTP::Request->new(HEAD => $url);
+       my $res = $ua->request($req);
+
+       die "invalid server response: '" . $res->status_line() . "'"
+           if ($res->code() != 200);
+
+       my $size = $res->header("Content-Length");
+       my $dispo = $res->header("Content-Disposition");
+       my $type = $res->header("Content-Type");
+
+       my $filename;
+
+       if ($dispo && $dispo =~ m/filename=(.+)/) {
+           $filename = $1;
+       } elsif ($url =~ m!^[^?]+/([^?/]*)(?:\?.*)?$!) {
+           $filename = $1;
+       }
+
+       # Content-Type: text/html; charset=utf-8
+       if ($type && $type =~ m/^([^;]+);/) {
+           $type = $1;
+       }
+
+       my $ret = {};
+
+       $ret->{filename} = $filename
+           if $filename;
+
+       $ret->{size} = $size + 0
+           if $size;
+
+       $ret->{mimetype} = $type
+           if $type;
+
+       return $ret;
+    }});
+
 __PACKAGE__->register_method({
     name => 'report',
     path => 'report',
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index afed3283..8e6557d8 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -38,6 +38,7 @@ JSSRC=                                                        
\
        form/GlobalSearchField.js                       \
        form/GroupSelector.js                           \
        form/GuestIDSelector.js                         \
+       form/HashAlgorithmSelector.js                   \
        form/HotplugFeatureSelector.js                  \
        form/IPProtocolSelector.js                      \
        form/IPRefSelector.js                           \
diff --git a/www/manager6/form/HashAlgorithmSelector.js 
b/www/manager6/form/HashAlgorithmSelector.js
new file mode 100644
index 00000000..5ae7a08b
--- /dev/null
+++ b/www/manager6/form/HashAlgorithmSelector.js
@@ -0,0 +1,16 @@
+Ext.define('PVE.form.hashAlgorithmSelector', {
+    extend: 'Proxmox.form.KVComboBox',
+    alias: ['widget.pveHashAlgorithmSelector'],
+    config: {
+       deleteEmpty: false,
+    },
+    comboItems: [
+       ['__default__', 'None'],
+       ['md5', 'MD5'],
+       ['sha1', 'SHA-1'],
+       ['sha224', 'SHA-224'],
+       ['sha256', 'SHA-256'],
+       ['sha384', 'SHA-384'],
+       ['sha512', 'SHA-512'],
+    ],
+});
diff --git a/www/manager6/storage/Browser.js b/www/manager6/storage/Browser.js
index 5fee94c7..da3e66c8 100644
--- a/www/manager6/storage/Browser.js
+++ b/www/manager6/storage/Browser.js
@@ -53,6 +53,9 @@ Ext.define('PVE.storage.Browser', {
            let plugin = res.plugintype;
            let contents = res.content.split(',');
 
+           let enableUpload = !!caps.storage['Datastore.AllocateTemplate'];
+           let enableRetrieve = !!(caps.nodes['Sys.Audit'] && 
caps.nodes['Sys.Modify']);
+
            if (contents.includes('backup')) {
                me.items.push({
                    xtype: 'pveStorageBackupView',
@@ -91,6 +94,8 @@ Ext.define('PVE.storage.Browser', {
                    itemId: 'contentIso',
                    content: 'iso',
                    pluginType: plugin,
+                   enableUploadButton: enableUpload,
+                   enableRetrieveButton: enableUpload && enableRetrieve,
                    useUploadButton: true,
                });
            }
@@ -101,6 +106,9 @@ Ext.define('PVE.storage.Browser', {
                    iconCls: 'fa fa-file-o lxc',
                    itemId: 'contentVztmpl',
                    pluginType: plugin,
+                   enableUploadButton: enableUpload,
+                   enableRetrieveButton: enableUpload && enableRetrieve,
+                   useUploadButton: true,
                });
            }
            if (contents.includes('snippets')) {
diff --git a/www/manager6/storage/ContentView.js 
b/www/manager6/storage/ContentView.js
index dd6df4b1..d01526e1 100644
--- a/www/manager6/storage/ContentView.js
+++ b/www/manager6/storage/ContentView.js
@@ -191,6 +191,213 @@ Ext.define('PVE.storage.Upload', {
     },
 });
 
+Ext.define('PVE.storage.Retrieve', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pveStorageRetrieve',
+
+    isCreate: true,
+
+    showTaskViewer: true,
+
+    title: gettext('Retrieve from URL'),
+    submitText: gettext('Download'),
+
+    id: 'retrieve',
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       urlChange: function(field) {
+           let me = Ext.getCmp('retrieve');
+           field = me.down('[name=url]');
+           field.setValidation("Waiting for response...");
+           field.validate();
+           me.setValues({size: ""});
+           Proxmox.Utils.API2Request({
+               url: `/nodes/${me.nodename}/urlmeta`,
+               method: 'GET',
+               params: {
+                   url: field.getValue(),
+                   insecure: me.getValues()['insecure'],
+               },
+               failure: function(res, opt) {
+                   field.setValidation(res.result.message);
+                   field.validate();
+                   me.setValues({
+                       size: "",
+                       mimetype: "",
+                   });
+               },
+               success: function(res, opt) {
+                   field.setValidation();
+                   field.validate();
+
+                   let data = res.result.data;
+                   me.setValues({
+                       filename: data.filename || "",
+                       size: data.size && Proxmox.Utils.format_size(data.size) 
|| "",
+                       mimetype: data.mimetype || "",
+                   });
+               },
+           });
+       },
+
+       hashChange: function(field) {
+           let cecksum = Ext.getCmp('retrieveChecksum');
+           if (field.getValue() === '__default__') {
+               cecksum.setDisabled(true);
+               cecksum.setValue("");
+               cecksum.allowBlank = true;
+           } else {
+               cecksum.setDisabled(false);
+               cecksum.allowBlank = false;
+           }
+       },
+
+       typeChange: function(field) {
+           let me = Ext.getCmp('retrieve');
+           let content = me.getValues()['content'];
+           let type = field.getValue();
+
+           const types = {
+               iso: [
+                   'application/octet-stream',
+                   'application/x-iso9660-image',
+                   'application/x-ima',
+               ],
+               vztmpl: [
+                   'application/octet-stream',
+                   'application/gzip',
+                   'application/tar',
+                   'application/tar+gzip',
+                   'application/x-gzip',
+                   'application/x-gtar',
+                   'application/x-tgz',
+                   'application/x-tar',
+               ],
+           };
+
+           if (type === "" || (types[content] && 
types[content].includes(type))) {
+               field.setValidation();
+               field.setDisabled(true);
+           } else {
+               field.setDisabled(false);
+               field.setValidation("Invalid type");
+           }
+           field.validate();
+       },
+    },
+
+    items: [
+       {
+           xtype: 'inputpanel',
+           waitMsgTarget: true,
+           border: false,
+           columnT: [
+               {
+                   xtype: 'textfield',
+                   name: 'url',
+                   allowBlank: false,
+                   fieldLabel: gettext('URL'),
+                   listeners: {
+                       change: {
+                           buffer: 500,
+                           fn: 'urlChange',
+                       },
+                   },
+               },
+               {
+                   xtype: 'textfield',
+                   name: 'filename',
+                   allowBlank: false,
+                   fieldLabel: gettext('File name'),
+               },
+           ],
+           column1: [
+               {
+                   xtype: 'pveContentTypeSelector',
+                   fieldLabel: gettext('Content'),
+                   name: 'content',
+                   allowBlank: false,
+               },
+           ],
+           column2: [
+               {
+                   xtype: 'textfield',
+                   name: 'size',
+                   disabled: true,
+                   fieldLabel: gettext('File size'),
+                   emptyText: gettext('unknown'),
+               },
+           ],
+           advancedColumn1: [
+               {
+                   xtype: 'textfield',
+                   name: 'checksum',
+                   fieldLabel: gettext('Checksum'),
+                   allowBlank: true,
+                   disabled: true,
+                   emptyText: gettext('none'),
+                   id: 'retrieveChecksum',
+               },
+               {
+                   xtype: 'pveHashAlgorithmSelector',
+                   name: 'checksumalg',
+                   fieldLabel: gettext('Hash algorithm'),
+                   allowBlank: true,
+                   hasNoneOption: true,
+                   value: '__default__',
+                   listeners: {
+                       change: 'hashChange',
+                   },
+               },
+           ],
+           advancedColumn2: [
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('MIME type'),
+                   name: 'mimetype',
+                   disabled: true,
+                   editable: false,
+                   emptyText: gettext('unknown'),
+                   listeners: {
+                       change: 'typeChange',
+                   },
+               },
+               {
+                   xtype: 'proxmoxcheckbox',
+                   name: 'insecure',
+                   fieldLabel: gettext('Trust invalid certificates'),
+                   uncheckedValue: 0,
+                   listeners: {
+                       change: 'urlChange',
+                   },
+               },
+           ],
+       },
+    ],
+
+    initComponent: function() {
+        var me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+       if (!me.storage) {
+           throw "no storage ID specified";
+       }
+
+       me.url = `/nodes/${me.nodename}/storage/${me.storage}/retrieve`;
+       me.method = 'POST';
+
+       let contentTypeSel = me.items[0].column1[0];
+       contentTypeSel.cts = me.contents;
+       contentTypeSel.value = me.contents[0] || '';
+
+        me.callParent();
+    },
+});
+
 Ext.define('PVE.storage.ContentView', {
     extend: 'Ext.grid.GridPanel',
 
@@ -249,36 +456,60 @@ Ext.define('PVE.storage.ContentView', {
 
        Proxmox.Utils.monStoreErrors(me, store);
 
-       let uploadButton = Ext.create('Proxmox.button.Button', {
-           text: gettext('Upload'),
-           handler: function() {
-               let win = Ext.create('PVE.storage.Upload', {
-                   nodename: nodename,
-                   storage: storage,
-                   contents: [content],
-               });
-               win.show();
-               win.on('destroy', reload);
-           },
-       });
-
-       let removeButton = Ext.create('Proxmox.button.StdRemoveButton', {
-           selModel: sm,
-           delay: 5,
-           callback: function() {
-               reload();
-           },
-           baseurl: baseurl + '/',
-       });
-
        if (!me.tbar) {
            me.tbar = [];
        }
        if (me.useUploadButton) {
-           me.tbar.push(uploadButton);
+           me.tbar.push(
+               {
+                   xtype: 'button',
+                   text: gettext('Upload'),
+                   disabled: !me.enableUploadButton,
+                   handler: function() {
+                       Ext.create('PVE.storage.Upload', {
+                           nodename: nodename,
+                           storage: storage,
+                           contents: [content],
+                           autoShow: true,
+                           listeners:{
+                               destroy: () => reload(),
+                           }
+                       });
+                   },
+               },
+               {
+                   xtype: 'button',
+                   text: gettext('Retrieve from URL'),
+                   disabled: !me.enableRetrieveButton,
+                   handler: function() {
+                       Ext.create('PVE.storage.Retrieve', {
+                           nodename: nodename,
+                           storage: storage,
+                           contents: [content],
+                           autoShow: true,
+                           listeners: {
+                               destroy: () => reload(),
+                           },
+                       });
+                   },
+               },
+               '-',
+           );
        }
-       if (!me.useCustomRemoveButton) {
-           me.tbar.push(removeButton);
+       if (me.useCustomRemoveButton) {
+           // custom remove button was inserted as first element
+           // -> place it at the end of tbar
+           me.tbar.push(me.tbar.shift());
+       } else {
+           me.tbar.push({
+               xtype: 'proxmoxStdRemoveButton',
+               selModel: sm,
+               delay: 5,
+               callback: function() {
+                   reload();
+               },
+               baseurl: baseurl + '/',
+           });
        }
        me.tbar.push(
            '->',
-- 
2.20.1



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to