]>
git.proxmox.com Git - proxmox-backup.git/blob - www/datastore/Content.js
1 Ext
.define('pbs-data-store-snapshots', {
2 extend
: 'Ext.data.Model',
9 dateFormat
: 'timestamp',
16 { name
: 'size', type
: 'int', allowNull
: true },
20 calculate: function(data
) {
28 data
.files
.forEach(file
=> {
29 if (file
.filename
=== 'index.json.blob') return; // is never encrypted
30 let mode
= PBS
.Utils
.cryptmap
.indexOf(file
['crypt-mode']);
32 crypt
[file
['crypt-mode']]++;
37 return PBS
.Utils
.calculateCryptMode(crypt
);
41 name
: 'matchesFilter',
48 Ext
.define('PBS.DataStoreContent', {
49 extend
: 'Ext.tree.Panel',
50 alias
: 'widget.pbsDataStoreContent',
54 title
: gettext('Content'),
57 xclass
: 'Ext.app.ViewController',
59 init: function(view
) {
60 if (!view
.datastore
) {
61 throw "no datastore specified";
64 this.store
= Ext
.create('Ext.data.Store', {
65 model
: 'pbs-data-store-snapshots',
66 groupField
: 'backup-group',
68 this.store
.on('load', this.onLoad
, this);
70 view
.getStore().setSorters([
75 Proxmox
.Utils
.monStoreErrors(view
, this.store
);
79 let view
= this.getView();
81 if (!view
.store
|| !this.store
) {
82 console
.warn('cannot reload, no store(s)');
86 let url
= `/api2/json/admin/datastore/${view.datastore}/snapshots`;
89 timeout
: 300*1000, // 5 minutes, we should make that api call faster
96 getRecordGroups: function(records
) {
99 for (const item
of records
) {
100 var btype
= item
.data
["backup-type"];
101 let group
= btype
+ "/" + item
.data
["backup-id"];
103 if (groups
[group
] !== undefined) {
107 var cls
= PBS
.Utils
.get_type_icon_cls(btype
);
109 console
.warn(`got unknown backup-type '${btype}'`);
110 continue; // FIXME: auto render? what do?
116 iconCls
: "fa " + cls
,
118 backup_type
: item
.data
["backup-type"],
119 backup_id
: item
.data
["backup-id"],
127 updateGroupNotes
: async
function(view
) {
129 let { result
: { data
: groups
} } = await Proxmox
.Async
.api2({
130 url
: `/api2/extjs/admin/datastore/${view.datastore}/groups`,
133 for (const group
of groups
) {
134 map
[`${group["backup-type"]}/${group["backup-id"]}`] = group
.comment
;
136 view
.getRootNode().cascade(node
=> {
137 if (node
.parentNode
&& node
.parentNode
.id
=== 'root') {
138 let group
= `${node.data.backup_type}/${node.data.backup_id}`;
139 node
.set('comment', map
[group
], { dirty
: false });
147 onLoad: function(store
, records
, success
, operation
) {
149 let view
= this.getView();
152 Proxmox
.Utils
.setErrorMask(view
, Proxmox
.Utils
.getResponseErrorMessage(operation
.getError()));
156 let groups
= this.getRecordGroups(records
);
161 view
.getSelection().some(function(item
) {
162 let id
= item
.data
.text
;
163 if (item
.data
.leaf
) {
164 id
= item
.parentNode
.data
.text
+ id
;
170 view
.getRootNode().cascadeBy({
172 if (item
.isExpanded() && !item
.data
.leaf
) {
173 let id
= item
.data
.text
;
182 for (const item
of records
) {
183 let group
= item
.data
["backup-type"] + "/" + item
.data
["backup-id"];
184 let children
= groups
[group
].children
;
186 let data
= item
.data
;
188 data
.text
= group
+ '/' + PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
190 data
.cls
= 'no-leaf-icons';
191 data
.matchesFilter
= true;
193 data
.expanded
= !!expanded
[data
.text
];
196 for (const file
of data
.files
) {
197 file
.text
= file
.filename
;
198 file
['crypt-mode'] = PBS
.Utils
.cryptmap
.indexOf(file
['crypt-mode']);
199 file
.fingerprint
= data
.fingerprint
;
201 file
.matchesFilter
= true;
203 data
.children
.push(file
);
209 let nowSeconds
= Date
.now() / 1000;
211 for (const [name
, group
] of Object
.entries(groups
)) {
225 for (let item
of group
.children
) {
226 crypt
[PBS
.Utils
.cryptmap
[item
['crypt-mode']]]++;
227 if (item
["backup-time"] > last_backup
&& item
.size
!== null) {
228 last_backup
= item
["backup-time"];
229 group
["backup-time"] = last_backup
;
230 group
.files
= item
.files
;
231 group
.size
= item
.size
;
232 group
.owner
= item
.owner
;
233 verify
.lastFailed
= item
.verification
&& item
.verification
.state
!== 'ok';
235 if (!item
.verification
) {
238 if (item
.verification
.state
=== 'ok') {
243 let task
= Proxmox
.Utils
.parse_task_upid(item
.verification
.upid
);
244 item
.verification
.lastTime
= task
.starttime
;
245 if (nowSeconds
- task
.starttime
> 30 * 24 * 60 * 60) {
250 group
.verification
= verify
;
251 group
.count
= group
.children
.length
;
252 group
.matchesFilter
= true;
253 crypt
.count
= group
.count
;
254 group
['crypt-mode'] = PBS
.Utils
.calculateCryptMode(crypt
);
255 group
.expanded
= !!expanded
[name
];
256 children
.push(group
);
264 if (!children
.length
) {
265 view
.setEmptyText(gettext('No Snapshots found'));
268 this.updateGroupNotes(view
);
270 if (selected
!== undefined) {
271 let selection
= view
.getRootNode().findChildBy(function(item
) {
272 let id
= item
.data
.text
;
273 if (item
.data
.leaf
) {
274 id
= item
.parentNode
.data
.text
+ id
;
276 return selected
=== id
;
279 view
.setSelection(selection
);
280 view
.getView().focusRow(selection
);
284 Proxmox
.Utils
.setErrorMask(view
, false);
285 if (view
.getStore().getFilters().length
> 0) {
286 let searchBox
= me
.lookup("searchbox");
287 let searchvalue
= searchBox
.getValue();
288 me
.search(searchBox
, searchvalue
);
292 onChangeOwner: function(view
, rI
, cI
, item
, e
, rec
) {
293 view
= this.getView();
295 if (!rec
|| !rec
.data
|| rec
.parentNode
.id
!== 'root' || !view
.datastore
) {
301 let win
= Ext
.create('PBS.BackupGroupChangeOwner', {
302 datastore
: view
.datastore
,
303 backup_type
: data
.backup_type
,
304 backup_id
: data
.backup_id
,
308 win
.on('destroy', this.reload
, this);
311 onPrune: function(view
, rI
, cI
, item
, e
, rec
) {
312 view
= this.getView();
314 if (!(rec
&& rec
.data
)) return;
316 if (rec
.parentNode
.id
!== 'root') return;
318 if (!view
.datastore
) return;
320 let win
= Ext
.create('PBS.DataStorePrune', {
321 datastore
: view
.datastore
,
322 backup_type
: data
.backup_type
,
323 backup_id
: data
.backup_id
,
325 win
.on('destroy', this.reload
, this);
329 verifyAll: function() {
330 var view
= this.getView();
332 Proxmox
.Utils
.API2Request({
333 url
: `/admin/datastore/${view.datastore}/verify`,
335 failure: function(response
) {
336 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
338 success: function(response
, options
) {
339 Ext
.create('Proxmox.window.TaskViewer', {
340 upid
: response
.result
.data
,
346 pruneAll: function() {
348 let view
= me
.getView();
350 if (!view
.datastore
) return;
352 Ext
.create('Proxmox.window.Edit', {
353 title
: `Prune Datastore '${view.datastore}'`,
354 onlineHelp
: 'maintenance_pruning',
360 showTaskViewer
: true,
362 taskDone
: () => me
.reload(),
364 url
: `/api2/extjs/admin/datastore/${view.datastore}/prune-datastore`,
368 xtype
: 'pbsPruneInputPanel',
375 onVerify: function(view
, rI
, cI
, item
, e
, rec
) {
379 if (!view
.datastore
) return;
381 if (!(rec
&& rec
.data
)) return;
386 if (rec
.parentNode
.id
!== 'root') {
388 "backup-type": data
["backup-type"],
389 "backup-id": data
["backup-id"],
390 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
394 "backup-type": data
.backup_type
,
395 "backup-id": data
.backup_id
,
399 Proxmox
.Utils
.API2Request({
401 url
: `/admin/datastore/${view.datastore}/verify`,
403 failure: function(response
) {
404 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
406 success: function(response
, options
) {
407 Ext
.create('Proxmox.window.TaskViewer', {
408 upid
: response
.result
.data
,
409 taskDone
: () => me
.reload(),
415 onNotesEdit: function(view
, data
, isGroup
) {
418 let url
= `/admin/datastore/${view.datastore}/`;
419 url
+= isGroup
? 'group-notes' : 'notes';
424 "backup-type": data
.backup_type
,
425 "backup-id": data
.backup_id
,
429 "backup-type": data
["backup-type"],
430 "backup-id": data
["backup-id"],
431 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
435 Ext
.create('PBS.window.NotesEdit', {
438 apiCallDone
: () => me
.reload(), // FIXME: do something more efficient?
439 extraRequestParams
: params
,
443 forgetGroup: function(data
) {
445 let view
= me
.getView();
447 Ext
.create('Proxmox.window.SafeDestroy', {
448 url
: `/admin/datastore/${view.datastore}/groups`,
450 "backup-type": data
.backup_type
,
451 "backup-id": data
.backup_id
,
457 taskName
: 'forget-group',
459 destroy
: () => me
.reload(),
464 forgetSnapshot: function(data
) {
466 let view
= me
.getView();
469 title
: gettext('Confirm'),
470 icon
: Ext
.Msg
.WARNING
,
471 message
: Ext
.String
.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
472 buttons
: Ext
.Msg
.YESNO
,
474 callback: function(btn
) {
479 Proxmox
.Utils
.API2Request({
480 url
: `/admin/datastore/${view.datastore}/snapshots`,
482 "backup-type": data
["backup-type"],
483 "backup-id": data
["backup-id"],
484 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
488 failure: function(response
, opts
) {
489 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
491 callback
: me
.reload
.bind(me
),
497 onForget: function(view
, rI
, cI
, item
, e
, rec
) {
499 view
= this.getView();
501 if (!(rec
&& rec
.data
)) return;
503 if (!view
.datastore
) return;
505 if (rec
.parentNode
.id
!== 'root') {
506 me
.forgetSnapshot(data
);
508 me
.forgetGroup(data
);
512 downloadFile: function(tV
, rI
, cI
, item
, e
, rec
) {
514 let view
= me
.getView();
516 if (!(rec
&& rec
.data
)) return;
517 let data
= rec
.parentNode
.data
;
519 let file
= rec
.data
.filename
;
521 'backup-id': data
['backup-id'],
522 'backup-type': data
['backup-type'],
523 'backup-time': (data
['backup-time'].getTime()/1000).toFixed(0),
527 let idx
= file
.lastIndexOf('.');
528 let filename
= file
.slice(0, idx
);
529 let atag
= document
.createElement('a');
530 params
['file-name'] = file
;
531 atag
.download
= filename
;
532 let url
= new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`,
533 window
.location
.origin
);
534 for (const [key
, value
] of Object
.entries(params
)) {
535 url
.searchParams
.append(key
, value
);
537 atag
.href
= url
.href
;
541 openPxarBrowser: function(tv
, rI
, Ci
, item
, e
, rec
) {
543 let view
= me
.getView();
545 if (!(rec
&& rec
.data
)) return;
546 let data
= rec
.parentNode
.data
;
548 let id
= data
['backup-id'];
549 let time
= data
['backup-time'];
550 let type
= data
['backup-type'];
551 let timetext
= PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
553 Ext
.create('Proxmox.window.FileBrowser', {
554 title
: `${type}/${id}/${timetext}`,
555 listURL
: `/api2/json/admin/datastore/${view.datastore}/catalog`,
556 downloadURL
: `/api2/json/admin/datastore/${view.datastore}/pxar-file-download`,
559 'backup-time': (time
.getTime()/1000).toFixed(0),
562 archive
: rec
.data
.filename
,
566 filter: function(item
, value
) {
567 if (item
.data
.text
.indexOf(value
) !== -1) {
571 if (item
.data
.owner
&& item
.data
.owner
.indexOf(value
) !== -1) {
578 search: function(tf
, value
) {
580 let view
= me
.getView();
581 let store
= view
.getStore();
582 if (!value
&& value
!== 0) {
584 store
.getRoot().collapseChildren(true);
585 tf
.triggers
.clear
.setVisible(false);
588 tf
.triggers
.clear
.setVisible(true);
589 if (value
.length
< 2) return;
590 Proxmox
.Utils
.setErrorMask(view
, true);
591 // we do it a little bit later for the error mask to work
592 setTimeout(function() {
594 store
.getRoot().collapseChildren(true);
597 store
.getRoot().cascadeBy({
598 before: function(item
) {
599 if (me
.filter(item
, value
)) {
600 item
.set('matchesFilter', true);
601 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
602 item
.parentNode
.childmatches
= true;
608 after: function(item
) {
609 if (me
.filter(item
, value
) || item
.id
=== 'root' || item
.childmatches
) {
610 item
.set('matchesFilter', true);
611 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
612 item
.parentNode
.childmatches
= true;
614 if (item
.childmatches
) {
618 item
.set('matchesFilter', false);
620 delete item
.childmatches
;
625 store
.filter((item
) => !!item
.get('matchesFilter'));
626 Proxmox
.Utils
.setErrorMask(view
, false);
632 activate: function() {
634 // only load on first activate to not load every tab switch
636 me
.getController().reload();
643 getRowClass: function(record
, index
) {
644 let verify
= record
.get('verification');
645 if (verify
&& verify
.lastFailed
) {
646 return 'proxmox-invalid-row';
655 header
: gettext("Backup Group"),
660 text
: gettext('Comment'),
661 dataIndex
: 'comment',
663 renderer
: (v
, meta
, record
) => {
664 let data
= record
.data
;
665 if (!data
|| data
.leaf
) {
668 if (v
=== undefined || v
=== null) {
671 v
= Ext
.String
.htmlEncode(v
);
672 let icon
= 'x-action-col-icon fa fa-fw fa-pencil pointer';
674 return `<span class="snapshot-comment-column">${v}</span>
675 <i data-qtip="${gettext('Edit')}" style="float: right; margin: 0px;" class="${icon}"></i>`;
678 afterrender: function(component
) {
679 // a bit of a hack, but relatively easy, cheap and works out well.
680 // more efficient to use one handler for the whole column than for each icon
681 component
.on('click', function(tree
, cell
, rowI
, colI
, e
, rec
) {
683 if (el
.tagName
!== "I" || !el
.classList
.contains("fa-pencil")) {
686 let view
= tree
.up();
687 let controller
= view
.controller
;
688 controller
.onNotesEdit(view
, rec
.data
, rec
.parentNode
.id
=== 'root');
691 dblclick: function(tree
, el
, row
, col
, ev
, rec
) {
692 let data
= rec
.data
|| {};
696 let view
= tree
.up();
697 let controller
= view
.controller
;
698 controller
.onNotesEdit(view
, rec
.data
, rec
.parentNode
.id
=== 'root');
703 header
: gettext('Actions'),
704 xtype
: 'actioncolumn',
710 getTip
: (v
, m
, rec
) => Ext
.String
.format(gettext("Verify '{0}'"), v
),
711 getClass
: (v
, m
, rec
) => rec
.data
.leaf
? 'pmx-hidden' : 'pve-icon-verify-lettering',
712 isActionDisabled
: (v
, r
, c
, i
, rec
) => !!rec
.data
.leaf
,
715 handler
: 'onChangeOwner',
716 getClass
: (v
, m
, rec
) => rec
.parentNode
.id
==='root' ? 'fa fa-user' : 'pmx-hidden',
717 getTip
: (v
, m
, rec
) => Ext
.String
.format(gettext("Change owner of '{0}'"), v
),
718 isActionDisabled
: (v
, r
, c
, i
, rec
) => rec
.parentNode
.id
!=='root',
722 getTip
: (v
, m
, rec
) => Ext
.String
.format(gettext("Prune '{0}'"), v
),
723 getClass
: (v
, m
, rec
) => rec
.parentNode
.id
==='root' ? 'fa fa-scissors' : 'pmx-hidden',
724 isActionDisabled
: (v
, r
, c
, i
, rec
) => rec
.parentNode
.id
!=='root',
728 getTip
: (v
, m
, rec
) => rec
.parentNode
.id
!=='root'
729 ? Ext
.String
.format(gettext("Permanently forget snapshot '{0}'"), v
)
730 : Ext
.String
.format(gettext("Permanently forget group '{0}'"), v
),
731 getClass
: (v
, m
, rec
) => !rec
.data
.leaf
? 'fa critical fa-trash-o' : 'pmx-hidden',
732 isActionDisabled
: (v
, r
, c
, i
, rec
) => !!rec
.data
.leaf
,
735 handler
: 'downloadFile',
736 getTip
: (v
, m
, rec
) => Ext
.String
.format(gettext("Download '{0}'"), v
),
737 getClass
: (v
, m
, rec
) => rec
.data
.leaf
&& rec
.data
.filename
? 'fa fa-download' : 'pmx-hidden',
738 isActionDisabled
: (v
, r
, c
, i
, rec
) => !rec
.data
.leaf
|| !rec
.data
.filename
|| rec
.data
['crypt-mode'] > 2,
741 handler
: 'openPxarBrowser',
742 tooltip
: gettext('Browse'),
743 getClass
: (v
, m
, rec
) => {
745 if (data
.leaf
&& data
.filename
&& data
.filename
.endsWith('pxar.didx')) {
746 return 'fa fa-folder-open-o';
750 isActionDisabled
: (v
, r
, c
, i
, rec
) => {
752 return !(data
.leaf
&&
754 data
.filename
.endsWith('pxar.didx') &&
755 data
['crypt-mode'] < 3);
762 header
: gettext('Backup Time'),
764 dataIndex
: 'backup-time',
765 format
: 'Y-m-d H:i:s',
769 header
: gettext("Size"),
772 renderer
: (v
, meta
, record
) => {
773 if (record
.data
.text
=== 'client.log.blob' && v
=== undefined) {
776 if (v
=== undefined || v
=== null) {
777 meta
.tdCls
= "x-grid-row-loading";
780 return Proxmox
.Utils
.format_size(v
);
784 xtype
: 'numbercolumn',
786 header
: gettext("Count"),
793 header
: gettext("Owner"),
798 header
: gettext('Encrypted'),
799 dataIndex
: 'crypt-mode',
800 renderer
: (v
, meta
, record
) => {
801 if (record
.data
.size
=== undefined || record
.data
.size
=== null) {
807 let iconCls
= PBS
.Utils
.cryptIconCls
[v
] || '';
810 iconTxt
= `<i class="fa fa-fw fa-${iconCls}"></i> `;
813 if (v
!== PBS
.Utils
.cryptmap
.indexOf('none') && record
.data
.fingerprint
!== undefined) {
814 tip
= "Key: " + PBS
.Utils
.renderKeyID(record
.data
.fingerprint
);
816 let txt
= (iconTxt
+ PBS
.Utils
.cryptText
[v
]) || Proxmox
.Utils
.unknownText
;
817 if (record
.parentNode
.id
=== 'root' || tip
=== undefined) {
820 return `<span data-qtip="${tip}">${txt}</span>`;
825 header
: gettext('Verify State'),
827 dataIndex
: 'verification',
829 sorter
: (arec
, brec
) => {
830 let a
= arec
.data
.verification
|| { ok
: 0, outdated
: 0, failed
: 0 };
831 let b
= brec
.data
.verification
|| { ok
: 0, outdated
: 0, failed
: 0 };
832 if (a
.failed
=== b
.failed
) {
833 if (a
.none
=== b
.none
) {
834 if (a
.outdated
=== b
.outdated
) {
837 return b
.outdated
- a
.outdated
;
840 return b
.none
- a
.none
;
843 return b
.failed
- a
.failed
;
846 renderer
: (v
, meta
, record
) => {
847 let i
= (cls
, txt
) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
848 if (v
=== undefined || v
=== null) {
849 return record
.data
.leaf
? '' : i('question-circle-o warning', gettext('None'));
851 let tip
, iconCls
, txt
;
852 if (record
.parentNode
.id
=== 'root') {
853 if (v
.failed
=== 0) {
855 if (v
.outdated
> 0) {
856 tip
= 'All OK, but some snapshots were not verified in last 30 days';
857 iconCls
= 'check warning';
858 txt
= gettext('All OK (old)');
860 tip
= 'All snapshots verified at least once in last 30 days';
861 iconCls
= 'check good';
862 txt
= gettext('All OK');
864 } else if (v
.ok
=== 0) {
865 tip
= `${v.none} not verified yet`;
866 iconCls
= 'question-circle-o warning';
867 txt
= gettext('None');
869 tip
= `${v.ok} OK, ${v.none} not verified yet`;
870 iconCls
= 'check faded';
874 tip
= `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`;
875 iconCls
= 'times critical';
876 txt
= v
.ok
=== 0 && v
.none
=== 0
877 ? gettext('All failed')
878 : `${v.failed} failed`;
880 } else if (!v
.state
) {
881 return record
.data
.leaf
? '' : gettext('None');
883 let verify_time
= Proxmox
.Utils
.render_timestamp(v
.lastTime
);
884 tip
= `Last verify task started on ${verify_time}`;
886 iconCls
= 'times critical';
887 if (v
.state
=== 'ok') {
888 iconCls
= 'check good';
889 let now
= Date
.now() / 1000;
890 if (now
- v
.lastTime
> 30 * 24 * 60 * 60) {
891 tip
= `Last verify task over 30 days ago: ${verify_time}`;
892 iconCls
= 'check warning';
896 return `<span data-qtip="${tip}">
897 <i class="fa fa-fw fa-${iconCls}"></i> ${txt}
901 dblclick: function(view
, el
, row
, col
, ev
, rec
) {
902 let data
= rec
.data
|| {};
903 let verify
= data
.verification
;
904 if (verify
&& verify
.upid
&& rec
.parentNode
.id
!== 'root') {
905 let win
= Ext
.create('Proxmox.window.TaskViewer', {
917 text
: gettext('Reload'),
918 iconCls
: 'fa fa-refresh',
923 xtype
: 'proxmoxButton',
924 text
: gettext('Verify All'),
925 confirmMsg
: gettext('Do you want to verify all snapshots now?'),
926 handler
: 'verifyAll',
929 xtype
: 'proxmoxButton',
930 text
: gettext('Prune All'),
936 html
: gettext('Search'),
940 reference
: 'searchbox',
941 emptyText
: gettext('group, date or owner'),
944 cls
: 'pmx-clear-trigger',
947 handler: function() {
948 this.triggers
.clear
.setVisible(false);