]> git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
e32c6160882188c79824fb6507cc73d064466fd8
[proxmox-backup.git] / www / DataStoreContent.js
1 Ext.define('pbs-data-store-snapshots', {
2 extend: 'Ext.data.Model',
3 fields: [
4 'backup-type',
5 'backup-id',
6 {
7 name: 'backup-time',
8 type: 'date',
9 dateFormat: 'timestamp'
10 },
11 'files',
12 'owner',
13 { name: 'size', type: 'int', allowNull: true, },
14 {
15 name: 'crypt-mode',
16 type: 'boolean',
17 calculate: function(data) {
18 let encrypted = 0;
19 let crypt = {
20 none: 0,
21 mixed: 0,
22 'sign-only': 0,
23 encrypt: 0,
24 count: 0,
25 };
26 let signed = 0;
27 data.files.forEach(file => {
28 if (file.filename === 'index.json.blob') return; // is never encrypted
29 let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
30 if (mode !== -1) {
31 crypt[file['crypt-mode']]++;
32 }
33 crypt.count++;
34 });
35
36 return PBS.Utils.calculateCryptMode(crypt);
37 }
38 },
39 {
40 name: 'matchesFilter',
41 type: 'boolean',
42 defaultValue: true,
43 },
44 ]
45 });
46
47 Ext.define('PBS.DataStoreContent', {
48 extend: 'Ext.tree.Panel',
49 alias: 'widget.pbsDataStoreContent',
50
51 rootVisible: false,
52
53 title: gettext('Content'),
54
55 controller: {
56 xclass: 'Ext.app.ViewController',
57
58 init: function(view) {
59 if (!view.datastore) {
60 throw "no datastore specified";
61 }
62
63 this.store = Ext.create('Ext.data.Store', {
64 model: 'pbs-data-store-snapshots',
65 groupField: 'backup-group',
66 });
67 this.store.on('load', this.onLoad, this);
68
69 view.getStore().setSorters([
70 'backup-group',
71 'text',
72 'backup-time'
73 ]);
74 Proxmox.Utils.monStoreErrors(view, this.store);
75 this.reload(); // initial load
76 },
77
78 reload: function() {
79 let view = this.getView();
80
81 if (!view.store || !this.store) {
82 console.warn('cannot reload, no store(s)');
83 return;
84 }
85
86 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
87 this.store.setProxy({
88 type: 'proxmox',
89 timeout: 300*1000, // 5 minutes, we should make that api call faster
90 url: url
91 });
92
93 this.store.load();
94 },
95
96 getRecordGroups: function(records) {
97 let groups = {};
98
99 for (const item of records) {
100 var btype = item.data["backup-type"];
101 let group = btype + "/" + item.data["backup-id"];
102
103 if (groups[group] !== undefined) {
104 continue;
105 }
106
107 var cls = '';
108 if (btype === 'vm') {
109 cls = 'fa-desktop';
110 } else if (btype === 'ct') {
111 cls = 'fa-cube';
112 } else if (btype === 'host') {
113 cls = 'fa-building';
114 } else {
115 console.warn(`got unknown backup-type '${btype}'`);
116 continue; // FIXME: auto render? what do?
117 }
118
119 groups[group] = {
120 text: group,
121 leaf: false,
122 iconCls: "fa " + cls,
123 expanded: false,
124 backup_type: item.data["backup-type"],
125 backup_id: item.data["backup-id"],
126 children: []
127 };
128 }
129
130 return groups;
131 },
132
133 onLoad: function(store, records, success, operation) {
134 let me = this;
135 let view = this.getView();
136
137 if (!success) {
138 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
139 return;
140 }
141
142 let groups = this.getRecordGroups(records);
143
144 for (const item of records) {
145 let group = item.data["backup-type"] + "/" + item.data["backup-id"];
146 let children = groups[group].children;
147
148 let data = item.data;
149
150 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
151 data.leaf = false;
152 data.cls = 'no-leaf-icons';
153 data.matchesFilter = true;
154
155 data.children = [];
156 for (const file of data.files) {
157 file.text = file.filename,
158 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
159 file.leaf = true;
160 file.matchesFilter = true;
161
162 data.children.push(file);
163 }
164
165 children.push(data);
166 }
167
168 let children = [];
169 for (const [_key, group] of Object.entries(groups)) {
170 let last_backup = 0;
171 let crypt = {
172 none: 0,
173 mixed: 0,
174 'sign-only': 0,
175 encrypt: 0,
176 };
177 for (const item of group.children) {
178 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
179 if (item["backup-time"] > last_backup && item.size !== null) {
180 last_backup = item["backup-time"];
181 group["backup-time"] = last_backup;
182 group.files = item.files;
183 group.size = item.size;
184 group.owner = item.owner;
185 }
186
187 }
188 group.count = group.children.length;
189 group.matchesFilter = true;
190 crypt.count = group.count;
191 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
192 children.push(group);
193 }
194
195 view.setRootNode({
196 expanded: true,
197 children: children
198 });
199 Proxmox.Utils.setErrorMask(view, false);
200 if (view.getStore().getFilters().length > 0) {
201 let searchBox = me.lookup("searchbox");
202 let searchvalue = searchBox.getValue();;
203 me.search(searchBox, searchvalue);
204 }
205 },
206
207 onPrune: function(view, rI, cI, item, e, rec) {
208 var view = this.getView();
209
210 if (!(rec && rec.data)) return;
211 let data = rec.data;
212 if (rec.parentNode.id !== 'root') return;
213
214 if (!view.datastore) return;
215
216 let win = Ext.create('PBS.DataStorePrune', {
217 datastore: view.datastore,
218 backup_type: data.backup_type,
219 backup_id: data.backup_id,
220 });
221 win.on('destroy', this.reload, this);
222 win.show();
223 },
224
225 onVerify: function(view, rI, cI, item, e, rec) {
226 var view = this.getView();
227
228 if (!view.datastore) return;
229
230 if (!(rec && rec.data)) return;
231 let data = rec.data;
232
233 let params;
234
235 if (rec.parentNode.id !== 'root') {
236 params = {
237 "backup-type": data["backup-type"],
238 "backup-id": data["backup-id"],
239 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
240 };
241 } else {
242 params = {
243 "backup-type": data.backup_type,
244 "backup-id": data.backup_id,
245 };
246 }
247
248 Proxmox.Utils.API2Request({
249 params: params,
250 url: `/admin/datastore/${view.datastore}/verify`,
251 method: 'POST',
252 failure: function(response) {
253 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
254 },
255 success: function(response, options) {
256 Ext.create('Proxmox.window.TaskViewer', {
257 upid: response.result.data,
258 }).show();
259 },
260 });
261 },
262
263 onForget: function(view, rI, cI, item, e, rec) {
264 let me = this;
265 var view = this.getView();
266
267 if (!(rec && rec.data)) return;
268 let data = rec.data;
269 if (!view.datastore) return;
270
271 Ext.Msg.show({
272 title: gettext('Confirm'),
273 icon: Ext.Msg.WARNING,
274 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
275 buttons: Ext.Msg.YESNO,
276 defaultFocus: 'no',
277 callback: function(btn) {
278 if (btn !== 'yes') {
279 return;
280 }
281
282 Proxmox.Utils.API2Request({
283 params: {
284 "backup-type": data["backup-type"],
285 "backup-id": data["backup-id"],
286 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
287 },
288 url: `/admin/datastore/${view.datastore}/snapshots`,
289 method: 'DELETE',
290 waitMsgTarget: view,
291 failure: function(response, opts) {
292 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
293 },
294 callback: me.reload.bind(me),
295 });
296 },
297 });
298 },
299
300 downloadFile: function(tV, rI, cI, item, e, rec) {
301 let me = this;
302 let view = me.getView();
303
304 if (!(rec && rec.data)) return;
305 let data = rec.parentNode.data;
306
307 let file = rec.data.filename;
308 let params = {
309 'backup-id': data['backup-id'],
310 'backup-type': data['backup-type'],
311 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
312 'file-name': file,
313 };
314
315 let idx = file.lastIndexOf('.');
316 let filename = file.slice(0, idx);
317 let atag = document.createElement('a');
318 params['file-name'] = file;
319 atag.download = filename;
320 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
321 for (const [key, value] of Object.entries(params)) {
322 url.searchParams.append(key, value);
323 }
324 atag.href = url.href;
325 atag.click();
326 },
327
328 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
329 let me = this;
330 let view = me.getView();
331
332 if (!(rec && rec.data)) return;
333 let data = rec.parentNode.data;
334
335 let id = data['backup-id'];
336 let time = data['backup-time'];
337 let type = data['backup-type'];
338 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
339
340 Ext.create('PBS.window.FileBrowser', {
341 title: `${type}/${id}/${timetext}`,
342 datastore: view.datastore,
343 'backup-id': id,
344 'backup-time': (time.getTime()/1000).toFixed(0),
345 'backup-type': type,
346 archive: rec.data.filename,
347 }).show();
348 },
349
350 filter: function(item, value) {
351 if (item.data.text.indexOf(value) !== -1) {
352 return true;
353 }
354
355 if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
356 return true;
357 }
358
359 return false;
360 },
361
362 search: function(tf, value) {
363 let me = this;
364 let view = me.getView();
365 let store = view.getStore();
366 if (!value && value !== 0) {
367 store.clearFilter();
368 store.getRoot().collapseChildren(true);
369 tf.triggers.clear.setVisible(false);
370 return;
371 }
372 tf.triggers.clear.setVisible(true);
373 if (value.length < 2) return;
374 Proxmox.Utils.setErrorMask(view, true);
375 // we do it a little bit later for the error mask to work
376 setTimeout(function() {
377 store.clearFilter();
378 store.getRoot().collapseChildren(true);
379
380 store.beginUpdate();
381 store.getRoot().cascadeBy({
382 before: function(item) {
383 if(me.filter(item, value)) {
384 item.set('matchesFilter', true);
385 if (item.parentNode && item.parentNode.id !== 'root') {
386 item.parentNode.childmatches = true;
387 }
388 return false;
389 }
390 return true;
391 },
392 after: function(item) {
393 if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
394 item.set('matchesFilter', true);
395 if (item.parentNode && item.parentNode.id !== 'root') {
396 item.parentNode.childmatches = true;
397 }
398 if (item.childmatches) {
399 item.expand();
400 }
401 } else {
402 item.set('matchesFilter', false);
403 }
404 delete item.childmatches;
405 },
406 });
407 store.endUpdate();
408
409 store.filter((item) => !!item.get('matchesFilter'));
410 Proxmox.Utils.setErrorMask(view, false);
411 }, 10);
412 },
413 },
414
415 columns: [
416 {
417 xtype: 'treecolumn',
418 header: gettext("Backup Group"),
419 dataIndex: 'text',
420 flex: 1
421 },
422 {
423 header: gettext('Actions'),
424 xtype: 'actioncolumn',
425 dataIndex: 'text',
426 items: [
427 {
428 handler: 'onVerify',
429 tooltip: gettext('Verify'),
430 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
431 isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
432 },
433 {
434 handler: 'onPrune',
435 tooltip: gettext('Prune'),
436 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
437 isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
438 },
439 {
440 handler: 'onForget',
441 tooltip: gettext('Forget Snapshot'),
442 getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
443 isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
444 },
445 {
446 handler: 'downloadFile',
447 tooltip: gettext('Download'),
448 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
449 isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
450 },
451 {
452 handler: 'openPxarBrowser',
453 tooltip: gettext('Browse'),
454 getClass: (v, m, rec) => {
455 let data = rec.data;
456 if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
457 return 'fa fa-folder-open-o';
458 }
459 return 'pmx-hidden';
460 },
461 isDisabled: (v, r, c, i, rec) => {
462 let data = rec.data;
463 return !(data.leaf &&
464 data.filename &&
465 data.filename.endsWith('pxar.didx') &&
466 data['crypt-mode'] < 2);
467 }
468 },
469 ]
470 },
471 {
472 xtype: 'datecolumn',
473 header: gettext('Backup Time'),
474 sortable: true,
475 dataIndex: 'backup-time',
476 format: 'Y-m-d H:i:s',
477 width: 150
478 },
479 {
480 header: gettext("Size"),
481 sortable: true,
482 dataIndex: 'size',
483 renderer: (v, meta, record) => {
484 if (record.data.text === 'client.log.blob' && v === undefined) {
485 return '';
486 }
487 if (v === undefined || v === null) {
488 meta.tdCls = "x-grid-row-loading";
489 return '';
490 }
491 return Proxmox.Utils.format_size(v);
492 },
493 },
494 {
495 xtype: 'numbercolumn',
496 format: '0',
497 header: gettext("Count"),
498 sortable: true,
499 dataIndex: 'count',
500 },
501 {
502 header: gettext("Owner"),
503 sortable: true,
504 dataIndex: 'owner',
505 },
506 {
507 header: gettext('Encrypted'),
508 dataIndex: 'crypt-mode',
509 renderer: (v, meta, record) => {
510 if (v === -1) {
511 return '';
512 }
513 let iconCls = PBS.Utils.cryptIconCls[v] || '';
514 let iconTxt = "";
515 if (iconCls) {
516 iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
517 }
518 return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
519 }
520 },
521 ],
522
523 tbar: [
524 {
525 text: gettext('Reload'),
526 iconCls: 'fa fa-refresh',
527 handler: 'reload',
528 },
529 '->',
530 {
531 xtype: 'tbtext',
532 html: gettext('Search'),
533 },
534 {
535 xtype: 'textfield',
536 reference: 'searchbox',
537 triggers: {
538 clear: {
539 cls: 'pmx-clear-trigger',
540 weight: -1,
541 hidden: true,
542 handler: function() {
543 this.triggers.clear.setVisible(false);
544 this.setValue('');
545 },
546 }
547 },
548 listeners: {
549 change: {
550 fn: 'search',
551 buffer: 500,
552 },
553 },
554 }
555 ],
556 });