]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/storage/ContentView.js
fix #1710: ui: storage: add download from url button
[pve-manager.git] / www / manager6 / storage / ContentView.js
CommitLineData
4a580e60
DM
1Ext.define('PVE.storage.Upload', {
2 extend: 'Ext.window.Window',
3f90858a 3 alias: 'widget.pveStorageUpload',
4a580e60
DM
4
5 resizable: false,
6
7 modal: true,
8
1d8306e4 9 initComponent: function() {
4a580e60
DM
10 var me = this;
11
4a580e60
DM
12 if (!me.nodename) {
13 throw "no node name specified";
14 }
177de3de 15 if (!me.storage) {
4a580e60
DM
16 throw "no storage ID specified";
17 }
18
1d8306e4 19 let baseurl = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
4a580e60 20
1d8306e4 21 let pbar = Ext.create('Ext.ProgressBar', {
4a580e60 22 text: 'Ready',
1d8306e4 23 hidden: true,
4a580e60
DM
24 });
25
c128543f
FE
26 let acceptedExtensions = {
27 iso: ".img, .iso",
28 vztmpl: ".tar.gz, .tar.xz",
29 };
30
31 let defaultContent = me.contents[0] || '';
32
33 let fileField = Ext.create('Ext.form.field.File', {
34 name: 'filename',
35 buttonText: gettext('Select File...'),
36 allowBlank: false,
37 setAccept: function(content) {
38 let acceptString = acceptedExtensions[content] || '';
39 this.fileInputEl.set({
40 accept: acceptString,
41 });
42 },
43 listeners: {
44 afterrender: function(cmp) {
45 cmp.setAccept(defaultContent);
46 },
47 },
48 });
49
4a580e60
DM
50 me.formPanel = Ext.create('Ext.form.Panel', {
51 method: 'POST',
52 waitMsgTarget: true,
53 bodyPadding: 10,
54 border: false,
55 width: 300,
56 fieldDefaults: {
57 labelWidth: 100,
1d8306e4 58 anchor: '100%',
4a580e60
DM
59 },
60 items: [
61 {
62 xtype: 'pveContentTypeSelector',
6c1a2b86 63 cts: me.contents,
4a580e60
DM
64 fieldLabel: gettext('Content'),
65 name: 'content',
c128543f 66 value: defaultContent,
518a3974
SR
67 allowBlank: false,
68 listeners: {
c128543f
FE
69 change: function(cmp, newValue, oldValue) {
70 fileField.setAccept(newValue);
71 },
72 },
4a580e60 73 },
c128543f 74 fileField,
1d8306e4
TL
75 pbar,
76 ],
4a580e60
DM
77 });
78
1d8306e4 79 let form = me.formPanel.getForm();
4a580e60 80
1d8306e4 81 let doStandardSubmit = function() {
4a580e60
DM
82 form.submit({
83 url: "/api2/htmljs" + baseurl,
84 waitMsg: gettext('Uploading file...'),
85 success: function(f, action) {
86 me.close();
87 },
88 failure: function(f, action) {
89 var msg = PVE.Utils.extractFormActionError(action);
90 Ext.Msg.alert(gettext('Error'), msg);
1d8306e4 91 },
4a580e60
DM
92 });
93 };
94
1d8306e4 95 let updateProgress = function(per, bytes) {
4a580e60
DM
96 var text = (per * 100).toFixed(2) + '%';
97 if (bytes) {
e7ade592 98 text += " (" + Proxmox.Utils.format_size(bytes) + ')';
4a580e60
DM
99 }
100 pbar.updateProgress(per, text);
101 };
177de3de 102
1d8306e4 103 let abortBtn = Ext.create('Ext.Button', {
4a580e60
DM
104 text: gettext('Abort'),
105 disabled: true,
106 handler: function() {
107 me.close();
1d8306e4 108 },
4a580e60
DM
109 });
110
1d8306e4 111 let submitBtn = Ext.create('Ext.Button', {
4a580e60
DM
112 text: gettext('Upload'),
113 disabled: true,
114 handler: function(button) {
115 var fd;
116 try {
117 fd = new FormData();
118 } catch (err) {
119 doStandardSubmit();
120 return;
121 }
122
123 button.setDisabled(true);
124 abortBtn.setDisabled(false);
125
126 var field = form.findField('content');
127 fd.append("content", field.getValue());
128 field.setDisabled(true);
129
130 field = form.findField('filename');
131 var file = field.fileInputEl.dom;
132 fd.append("filename", file.files[0]);
133 field.setDisabled(true);
134
135 pbar.setVisible(true);
136 updateProgress(0);
137
1d8306e4
TL
138 let xhr = new XMLHttpRequest();
139 me.xhr = xhr;
4a580e60 140
177de3de 141 xhr.addEventListener("load", function(e) {
1d8306e4 142 if (xhr.status === 200) {
4a580e60 143 me.close();
1d8306e4
TL
144 return;
145 }
146 let err = Ext.htmlEncode(xhr.statusText);
147 let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
148 if (xhr.responseText !== "") {
149 let result = Ext.decode(xhr.responseText);
150 result.message = msg;
151 msg = Proxmox.Utils.extractRequestError(result, true);
177de3de 152 }
1d8306e4 153 Ext.Msg.alert(gettext('Error'), msg, btn => me.close());
4a580e60
DM
154 }, false);
155
156 xhr.addEventListener("error", function(e) {
1d8306e4
TL
157 let err = e.target.status.toString();
158 let msg = `Error '${err}' occurred while receiving the document.`;
159 Ext.Msg.alert(gettext('Error'), msg, btn => me.close());
4a580e60 160 });
177de3de 161
4a580e60 162 xhr.upload.addEventListener("progress", function(evt) {
177de3de 163 if (evt.lengthComputable) {
1d8306e4 164 let percentComplete = evt.loaded / evt.total;
4a580e60 165 updateProgress(percentComplete, evt.loaded);
177de3de 166 }
4a580e60
DM
167 }, false);
168
1d8306e4 169 xhr.open("POST", `/api2/json${baseurl}`, true);
177de3de 170 xhr.send(fd);
1d8306e4 171 },
4a580e60
DM
172 });
173
1d8306e4 174 form.on('validitychange', (f, valid) => submitBtn.setDisabled(!valid));
4a580e60 175
1d8306e4
TL
176 Ext.apply(me, {
177 title: gettext('Upload'),
4a580e60 178 items: me.formPanel,
1d8306e4 179 buttons: [abortBtn, submitBtn],
4a580e60
DM
180 listeners: {
181 close: function() {
1d8306e4
TL
182 if (me.xhr) {
183 me.xhr.abort();
184 delete me.xhr;
4a580e60 185 }
1d8306e4
TL
186 },
187 },
4a580e60
DM
188 });
189
190 me.callParent();
1d8306e4 191 },
4a580e60
DM
192});
193
af3c0a92
LS
194Ext.define('PVE.storage.DownloadUrl', {
195 extend: 'Proxmox.window.Edit',
196 alias: 'widget.pveStorageDownloadUrl',
197 mixins: ['Proxmox.Mixin.CBind'],
198
199 isCreate: true,
200
201 method: 'POST',
202
203 showTaskViewer: true,
204
205 title: gettext('Download from URL'),
206 submitText: gettext('Download'),
207
208 cbindData: function(initialConfig) {
209 var me = this;
210 return {
211 nodename: me.nodename,
212 storage: me.storage,
213 content: me.content,
214 };
215 },
216
217 cbind: {
218 url: '/nodes/{nodename}/storage/{storage}/download-url',
219 },
220
221 controller: {
222 xclass: 'Ext.app.ViewController',
223
224 urlChange: function(field) {
225 let me = this;
226 let view = me.getView();
227 field = view.down('[name=url]');
228 field.setValidation(gettext("Please check URL"));
229 field.validate();
230 view.setValues({
231 size: gettext("unknown"),
232 mimetype: gettext("unknown"),
233 });
234 },
235
236 urlCheck: function(field) {
237 let me = this;
238 let view = me.getView();
239 field = view.down('[name=url]');
240 view.setValues({
241 size: gettext("unknown"),
242 mimetype: gettext("unknown"),
243 });
244 Proxmox.Utils.API2Request({
245 url: `/nodes/${view.nodename}/query-url-metadata`,
246 method: 'GET',
247 params: {
248 url: field.getValue(),
249 'verify-certificates': view.getValues()['verify-certificates'],
250 },
251 waitMsgTarget: view,
252 failure: function(res, opt) {
253 field.setValidation(res.result.message);
254 field.validate();
255 },
256 success: function(res, opt) {
257 field.setValidation();
258 field.validate();
259
260 let data = res.result.data;
261 view.setValues({
262 filename: data.filename || "",
263 size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("unknown"),
264 mimetype: data.mimetype || gettext("unknown"),
265 });
266 },
267 });
268 },
269
270 hashChange: function(field) {
271 let checksum = Ext.getCmp('downloadUrlChecksum');
272 if (field.getValue() === '__default__') {
273 checksum.setDisabled(true);
274 checksum.setValue("");
275 checksum.allowBlank = true;
276 } else {
277 checksum.setDisabled(false);
278 checksum.allowBlank = false;
279 }
280 },
281 },
282
283 items: [
284 {
285 xtype: 'inputpanel',
286 border: false,
287 columnT: [
288 {
289 xtype: 'fieldcontainer',
290 layout: 'hbox',
291 fieldLabel: gettext('URL'),
292 items: [
293 {
294 xtype: 'textfield',
295 name: 'url',
296 allowBlank: false,
297 flex: 1,
298 listeners: {
299 change: 'urlChange',
300 },
301 },
302 {
303 xtype: 'button',
304 name: 'check',
305 text: gettext('Check'),
306 margin: '0 0 0 5',
307 listeners: {
308 click: 'urlCheck',
309 },
310 },
311 ],
312 },
313 {
314 xtype: 'textfield',
315 name: 'filename',
316 allowBlank: false,
317 fieldLabel: gettext('File name'),
318 },
319 ],
320 column1: [
321 {
322 xtype: 'displayfield',
323 name: 'size',
324 fieldLabel: gettext('File size'),
325 value: gettext('unknown'),
326 },
327 ],
328 column2: [
329 {
330 xtype: 'displayfield',
331 name: 'mimetype',
332 fieldLabel: gettext('MIME type'),
333 value: gettext('unknown'),
334 },
335 ],
336 advancedColumn1: [
337 {
338 xtype: 'pveHashAlgorithmSelector',
339 name: 'checksum-algorithm',
340 fieldLabel: gettext('Hash algorithm'),
341 allowBlank: true,
342 hasNoneOption: true,
343 value: '__default__',
344 listeners: {
345 change: 'hashChange',
346 },
347 },
348 {
349 xtype: 'textfield',
350 name: 'checksum',
351 fieldLabel: gettext('Checksum'),
352 allowBlank: true,
353 disabled: true,
354 emptyText: gettext('none'),
355 id: 'downloadUrlChecksum',
356 },
357 ],
358 advancedColumn2: [
359 {
360 xtype: 'proxmoxcheckbox',
361 name: 'verify-certificates',
362 fieldLabel: gettext('Verify certificates'),
363 uncheckedValue: 0,
364 checked: true,
365 listeners: {
366 change: 'urlChange',
367 },
368 },
369 ],
370 },
371 {
372 xtype: 'hiddenfield',
373 name: 'content',
374 cbind: {
375 value: '{content}',
376 },
377 },
378 ],
379
380 initComponent: function() {
381 var me = this;
382
383 if (!me.nodename) {
384 throw "no node name specified";
385 }
386 if (!me.storage) {
387 throw "no storage ID specified";
388 }
389
390 me.callParent();
391 },
392});
393
4a580e60
DM
394Ext.define('PVE.storage.ContentView', {
395 extend: 'Ext.grid.GridPanel',
396
3f90858a 397 alias: 'widget.pveStorageContentView',
4a580e60 398
c90539de
DC
399 viewConfig: {
400 trackOver: false,
1d8306e4 401 loadMask: false,
c90539de 402 },
1d8306e4 403 initComponent: function() {
4a580e60
DM
404 var me = this;
405
dbeddeb3
FE
406 if (!me.nodename) {
407 me.nodename = me.pveSelNode.data.node;
408 if (!me.nodename) {
409 throw "no node name specified";
410 }
4a580e60 411 }
1d8306e4 412 const nodename = me.nodename;
4a580e60 413
dbeddeb3
FE
414 if (!me.storage) {
415 me.storage = me.pveSelNode.data.storage;
416 if (!me.storage) {
417 throw "no storage ID specified";
418 }
4a580e60 419 }
1d8306e4 420 const storage = me.storage;
4a580e60 421
e8b422bc
FE
422 var content = me.content;
423 if (!content) {
424 throw "no content type specified";
425 }
426
1d8306e4
TL
427 const baseurl = `/nodes/${nodename}/storage/${storage}/content`;
428 let store = me.store = Ext.create('Ext.data.Store', {
4a580e60 429 model: 'pve-storage-content',
4a580e60 430 proxy: {
56a353b9 431 type: 'proxmox',
e8b422bc
FE
432 url: '/api2/json' + baseurl,
433 extraParams: {
434 content: content,
435 },
4a580e60 436 },
177de3de
DC
437 sorters: {
438 property: 'volid',
1d8306e4
TL
439 order: 'DESC',
440 },
4a580e60
DM
441 });
442
dbeddeb3
FE
443 if (!me.sm) {
444 me.sm = Ext.create('Ext.selection.RowModel', {});
445 }
1d8306e4 446 let sm = me.sm;
4a580e60 447
1d8306e4 448 let reload = () => store.load();
4a580e60 449
e7ade592 450 Proxmox.Utils.monStoreErrors(me, store);
4a580e60 451
9ce0c258
FE
452 if (!me.tbar) {
453 me.tbar = [];
454 }
8798c35b 455 if (me.useUploadButton) {
af3c0a92
LS
456 me.tbar.unshift(
457 {
458 xtype: 'button',
459 text: gettext('Upload'),
460 disabled: !me.enableUploadButton,
461 handler: function() {
462 Ext.create('PVE.storage.Upload', {
463 nodename: nodename,
464 storage: storage,
465 contents: [content],
466 autoShow: true,
467 taskDone: () => reload(),
468 });
469 },
470 },
471 {
472 xtype: 'button',
473 text: gettext('Download from URL'),
474 disabled: !me.enableDownloadUrlButton,
475 handler: function() {
476 Ext.create('PVE.storage.DownloadUrl', {
477 nodename: nodename,
478 storage: storage,
479 content: content,
480 autoShow: true,
481 taskDone: () => reload(),
482 });
483 },
484 },
485 '-',
486 );
8798c35b 487 }
f5e17f15 488 if (!me.useCustomRemoveButton) {
af3c0a92
LS
489 me.tbar.push({
490 xtype: 'proxmoxStdRemoveButton',
491 selModel: sm,
492 delay: 5,
493 callback: () => reload(),
494 baseurl: baseurl + '/',
495 });
f5e17f15 496 }
9ce0c258 497 me.tbar.push(
9ce0c258 498 '->',
1d8306e4
TL
499 gettext('Search') + ':',
500 ' ',
9ce0c258
FE
501 {
502 xtype: 'textfield',
503 width: 200,
504 enableKeyEvents: true,
c8a71b9e 505 emptyText: gettext('Name, Format'),
9ce0c258 506 listeners: {
c8a71b9e
TL
507 keyup: {
508 buffer: 500,
509 fn: function(field) {
510 store.clearFilter(true);
511 store.filter([
512 {
513 property: 'text',
514 value: field.getValue(),
515 anyMatch: true,
516 caseSensitive: false,
517 },
518 ]);
519 },
520 },
521 change: function(field, newValue, oldValue) {
522 if (newValue !== this.originalValue) {
523 this.triggers.clear.setVisible(true);
524 }
525 },
526 },
527 triggers: {
528 clear: {
529 cls: 'pmx-clear-trigger',
530 weight: -1,
531 hidden: true,
532 handler: function() {
533 this.triggers.clear.setVisible(false);
534 this.setValue(this.originalValue);
535 store.clearFilter();
536 },
537 },
538 },
539 },
9ce0c258
FE
540 );
541
6ecc7420 542 let availableColumns = {
7ef0f4c3
FE
543 'name': {
544 header: gettext('Name'),
545 flex: 2,
546 sortable: true,
547 renderer: PVE.Utils.render_storage_content,
1d8306e4 548 dataIndex: 'text',
7ef0f4c3 549 },
ef402242
DC
550 'notes': {
551 header: gettext('Notes'),
7ef0f4c3
FE
552 flex: 1,
553 renderer: Ext.htmlEncode,
ef402242 554 dataIndex: 'notes',
7ef0f4c3
FE
555 },
556 'date': {
557 header: gettext('Date'),
558 width: 150,
1d8306e4 559 dataIndex: 'vdate',
7ef0f4c3
FE
560 },
561 'format': {
562 header: gettext('Format'),
563 width: 100,
1d8306e4 564 dataIndex: 'format',
7ef0f4c3
FE
565 },
566 'size': {
567 header: gettext('Size'),
568 width: 100,
569 renderer: Proxmox.Utils.format_size,
1d8306e4 570 dataIndex: 'size',
7ef0f4c3
FE
571 },
572 };
573
6ecc7420
TL
574 if (me.hideColumns) {
575 me.hideColumns.forEach(key => delete availableColumns[key]);
ebea3f45
TL
576 }
577 if (!me.hasNotesColumn) {
ef402242 578 delete availableColumns.notes;
7ef0f4c3 579 }
ebea3f45
TL
580 if (me.extraColumns && typeof me.extraColumns === 'object') {
581 Object.assign(availableColumns, me.extraColumns);
582 }
6ecc7420 583 const columns = Object.values(availableColumns);
7ef0f4c3 584
9ce0c258
FE
585 Ext.apply(me, {
586 store: store,
587 selModel: sm,
588 tbar: me.tbar,
7ef0f4c3 589 columns: columns,
4a580e60 590 listeners: {
1d8306e4
TL
591 activate: reload,
592 },
4a580e60
DM
593 });
594
595 me.callParent();
1d8306e4 596 },
4a580e60 597}, function() {
4a580e60
DM
598 Ext.define('pve-storage-content', {
599 extend: 'Ext.data.Model',
177de3de
DC
600 fields: [
601 'volid', 'content', 'format', 'size', 'used', 'vmid',
ef402242 602 'channel', 'id', 'lun', 'notes', 'verification',
177de3de
DC
603 {
604 name: 'text',
4a580e60 605 convert: function(value, record) {
86cc7049
DC
606 // check for volid, because if you click on a grouping header,
607 // it calls convert (but with an empty volid)
608 if (value || record.data.volid === null) {
4a580e60
DM
609 return value;
610 }
611 return PVE.Utils.render_storage_content(value, {}, record);
1d8306e4 612 },
12d50fcd
TL
613 },
614 {
615 name: 'vdate',
616 convert: function(value, record) {
617 // check for volid, because if you click on a grouping header,
618 // it calls convert (but with an empty volid)
619 if (value || record.data.volid === null) {
620 return value;
621 }
622 let t = record.data.content;
623 if (t === "backup") {
624 let v = record.data.volid;
625 let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/);
626 if (match) {
a4a86fe9 627 let date = match[1].replace(/_/g, '-');
12d50fcd
TL
628 let time = match[2].replace(/_/g, ':');
629 return date + " " + time;
630 }
631 }
a4a86fe9
DM
632 if (record.data.ctime) {
633 let ctime = new Date(record.data.ctime * 1000);
1d8306e4 634 return Ext.Date.format(ctime, 'Y-m-d H:i:s');
a4a86fe9 635 }
12d50fcd 636 return '';
1d8306e4 637 },
12d50fcd 638 },
4a580e60 639 ],
1d8306e4 640 idProperty: 'volid',
4a580e60 641 });
4a580e60 642});