]>
Commit | Line | Data |
---|---|---|
1 | Ext.define('PVE.storage.Upload', { | |
2 | extend: 'Ext.window.Window', | |
3 | alias: 'widget.pveStorageUpload', | |
4 | ||
5 | resizable: false, | |
6 | ||
7 | modal: true, | |
8 | ||
9 | initComponent: function() { | |
10 | var me = this; | |
11 | ||
12 | if (!me.nodename) { | |
13 | throw "no node name specified"; | |
14 | } | |
15 | if (!me.storage) { | |
16 | throw "no storage ID specified"; | |
17 | } | |
18 | ||
19 | let baseurl = `/nodes/${me.nodename}/storage/${me.storage}/upload`; | |
20 | ||
21 | let pbar = Ext.create('Ext.ProgressBar', { | |
22 | text: 'Ready', | |
23 | hidden: true, | |
24 | }); | |
25 | ||
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 | ||
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, | |
58 | anchor: '100%', | |
59 | }, | |
60 | items: [ | |
61 | { | |
62 | xtype: 'pveContentTypeSelector', | |
63 | cts: me.contents, | |
64 | fieldLabel: gettext('Content'), | |
65 | name: 'content', | |
66 | value: defaultContent, | |
67 | allowBlank: false, | |
68 | listeners: { | |
69 | change: function(cmp, newValue, oldValue) { | |
70 | fileField.setAccept(newValue); | |
71 | }, | |
72 | }, | |
73 | }, | |
74 | fileField, | |
75 | pbar, | |
76 | ], | |
77 | }); | |
78 | ||
79 | let form = me.formPanel.getForm(); | |
80 | ||
81 | let doStandardSubmit = function() { | |
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); | |
91 | }, | |
92 | }); | |
93 | }; | |
94 | ||
95 | let updateProgress = function(per, bytes) { | |
96 | var text = (per * 100).toFixed(2) + '%'; | |
97 | if (bytes) { | |
98 | text += " (" + Proxmox.Utils.format_size(bytes) + ')'; | |
99 | } | |
100 | pbar.updateProgress(per, text); | |
101 | }; | |
102 | ||
103 | let abortBtn = Ext.create('Ext.Button', { | |
104 | text: gettext('Abort'), | |
105 | disabled: true, | |
106 | handler: function() { | |
107 | me.close(); | |
108 | }, | |
109 | }); | |
110 | ||
111 | let submitBtn = Ext.create('Ext.Button', { | |
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 | ||
138 | let xhr = new XMLHttpRequest(); | |
139 | me.xhr = xhr; | |
140 | ||
141 | xhr.addEventListener("load", function(e) { | |
142 | if (xhr.status === 200) { | |
143 | me.close(); | |
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); | |
152 | } | |
153 | Ext.Msg.alert(gettext('Error'), msg, btn => me.close()); | |
154 | }, false); | |
155 | ||
156 | xhr.addEventListener("error", function(e) { | |
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()); | |
160 | }); | |
161 | ||
162 | xhr.upload.addEventListener("progress", function(evt) { | |
163 | if (evt.lengthComputable) { | |
164 | let percentComplete = evt.loaded / evt.total; | |
165 | updateProgress(percentComplete, evt.loaded); | |
166 | } | |
167 | }, false); | |
168 | ||
169 | xhr.open("POST", `/api2/json${baseurl}`, true); | |
170 | xhr.send(fd); | |
171 | }, | |
172 | }); | |
173 | ||
174 | form.on('validitychange', (f, valid) => submitBtn.setDisabled(!valid)); | |
175 | ||
176 | Ext.apply(me, { | |
177 | title: gettext('Upload'), | |
178 | items: me.formPanel, | |
179 | buttons: [abortBtn, submitBtn], | |
180 | listeners: { | |
181 | close: function() { | |
182 | if (me.xhr) { | |
183 | me.xhr.abort(); | |
184 | delete me.xhr; | |
185 | } | |
186 | }, | |
187 | }, | |
188 | }); | |
189 | ||
190 | me.callParent(); | |
191 | }, | |
192 | }); | |
193 | ||
194 | Ext.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 | ||
394 | Ext.define('PVE.storage.ContentView', { | |
395 | extend: 'Ext.grid.GridPanel', | |
396 | ||
397 | alias: 'widget.pveStorageContentView', | |
398 | ||
399 | viewConfig: { | |
400 | trackOver: false, | |
401 | loadMask: false, | |
402 | }, | |
403 | initComponent: function() { | |
404 | var me = this; | |
405 | ||
406 | if (!me.nodename) { | |
407 | me.nodename = me.pveSelNode.data.node; | |
408 | if (!me.nodename) { | |
409 | throw "no node name specified"; | |
410 | } | |
411 | } | |
412 | const nodename = me.nodename; | |
413 | ||
414 | if (!me.storage) { | |
415 | me.storage = me.pveSelNode.data.storage; | |
416 | if (!me.storage) { | |
417 | throw "no storage ID specified"; | |
418 | } | |
419 | } | |
420 | const storage = me.storage; | |
421 | ||
422 | var content = me.content; | |
423 | if (!content) { | |
424 | throw "no content type specified"; | |
425 | } | |
426 | ||
427 | const baseurl = `/nodes/${nodename}/storage/${storage}/content`; | |
428 | let store = me.store = Ext.create('Ext.data.Store', { | |
429 | model: 'pve-storage-content', | |
430 | proxy: { | |
431 | type: 'proxmox', | |
432 | url: '/api2/json' + baseurl, | |
433 | extraParams: { | |
434 | content: content, | |
435 | }, | |
436 | }, | |
437 | sorters: { | |
438 | property: 'volid', | |
439 | order: 'DESC', | |
440 | }, | |
441 | }); | |
442 | ||
443 | if (!me.sm) { | |
444 | me.sm = Ext.create('Ext.selection.RowModel', {}); | |
445 | } | |
446 | let sm = me.sm; | |
447 | ||
448 | let reload = () => store.load(); | |
449 | ||
450 | Proxmox.Utils.monStoreErrors(me, store); | |
451 | ||
452 | if (!me.tbar) { | |
453 | me.tbar = []; | |
454 | } | |
455 | if (me.useUploadButton) { | |
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 | ); | |
487 | } | |
488 | if (!me.useCustomRemoveButton) { | |
489 | me.tbar.push({ | |
490 | xtype: 'proxmoxStdRemoveButton', | |
491 | selModel: sm, | |
492 | delay: 5, | |
493 | callback: () => reload(), | |
494 | baseurl: baseurl + '/', | |
495 | }); | |
496 | } | |
497 | me.tbar.push( | |
498 | '->', | |
499 | gettext('Search') + ':', | |
500 | ' ', | |
501 | { | |
502 | xtype: 'textfield', | |
503 | width: 200, | |
504 | enableKeyEvents: true, | |
505 | emptyText: gettext('Name, Format'), | |
506 | listeners: { | |
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 | }, | |
540 | ); | |
541 | ||
542 | let availableColumns = { | |
543 | 'name': { | |
544 | header: gettext('Name'), | |
545 | flex: 2, | |
546 | sortable: true, | |
547 | renderer: PVE.Utils.render_storage_content, | |
548 | dataIndex: 'text', | |
549 | }, | |
550 | 'notes': { | |
551 | header: gettext('Notes'), | |
552 | flex: 1, | |
553 | renderer: Ext.htmlEncode, | |
554 | dataIndex: 'notes', | |
555 | }, | |
556 | 'date': { | |
557 | header: gettext('Date'), | |
558 | width: 150, | |
559 | dataIndex: 'vdate', | |
560 | }, | |
561 | 'format': { | |
562 | header: gettext('Format'), | |
563 | width: 100, | |
564 | dataIndex: 'format', | |
565 | }, | |
566 | 'size': { | |
567 | header: gettext('Size'), | |
568 | width: 100, | |
569 | renderer: Proxmox.Utils.format_size, | |
570 | dataIndex: 'size', | |
571 | }, | |
572 | }; | |
573 | ||
574 | if (me.hideColumns) { | |
575 | me.hideColumns.forEach(key => delete availableColumns[key]); | |
576 | } | |
577 | if (!me.hasNotesColumn) { | |
578 | delete availableColumns.notes; | |
579 | } | |
580 | if (me.extraColumns && typeof me.extraColumns === 'object') { | |
581 | Object.assign(availableColumns, me.extraColumns); | |
582 | } | |
583 | const columns = Object.values(availableColumns); | |
584 | ||
585 | Ext.apply(me, { | |
586 | store: store, | |
587 | selModel: sm, | |
588 | tbar: me.tbar, | |
589 | columns: columns, | |
590 | listeners: { | |
591 | activate: reload, | |
592 | }, | |
593 | }); | |
594 | ||
595 | me.callParent(); | |
596 | }, | |
597 | }, function() { | |
598 | Ext.define('pve-storage-content', { | |
599 | extend: 'Ext.data.Model', | |
600 | fields: [ | |
601 | 'volid', 'content', 'format', 'size', 'used', 'vmid', | |
602 | 'channel', 'id', 'lun', 'notes', 'verification', | |
603 | { | |
604 | name: 'text', | |
605 | convert: function(value, record) { | |
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) { | |
609 | return value; | |
610 | } | |
611 | return PVE.Utils.render_storage_content(value, {}, record); | |
612 | }, | |
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) { | |
627 | let date = match[1].replace(/_/g, '-'); | |
628 | let time = match[2].replace(/_/g, ':'); | |
629 | return date + " " + time; | |
630 | } | |
631 | } | |
632 | if (record.data.ctime) { | |
633 | let ctime = new Date(record.data.ctime * 1000); | |
634 | return Ext.Date.format(ctime, 'Y-m-d H:i:s'); | |
635 | } | |
636 | return ''; | |
637 | }, | |
638 | }, | |
639 | ], | |
640 | idProperty: 'volid', | |
641 | }); | |
642 | }); |