3 console
.log("Starting Backup Server GUI");
5 Ext
.define('PBS.Utils', {
8 missingText
: gettext('missing'),
10 updateLoginData: function(data
) {
11 Proxmox
.Utils
.setAuthData(data
);
14 dataStorePrefix
: 'DataStore-',
37 calculateCryptMode: function(data
) {
38 let mixed
= data
.mixed
;
39 let encrypted
= data
.encrypt
;
40 let signed
= data
['sign-only'];
41 let files
= data
.count
;
43 return PBS
.Utils
.cryptmap
.indexOf('mixed');
44 } else if (files
=== encrypted
&& encrypted
> 0) {
45 return PBS
.Utils
.cryptmap
.indexOf('encrypt');
46 } else if (files
=== signed
&& signed
> 0) {
47 return PBS
.Utils
.cryptmap
.indexOf('sign-only');
48 } else if ((signed
+encrypted
) === 0) {
49 return PBS
.Utils
.cryptmap
.indexOf('none');
51 return PBS
.Utils
.cryptmap
.indexOf('mixed');
55 noSubKeyHtml
: 'You do not have a valid subscription for this server. Please visit <a target="_blank" href="https://www.proxmox.com/proxmox-backup-server/pricing">www.proxmox.com</a> to get a list of available options.',
57 getDataStoreFromPath: function(path
) {
58 return path
.slice(PBS
.Utils
.dataStorePrefix
.length
);
61 isDataStorePath: function(path
) {
62 return path
.indexOf(PBS
.Utils
.dataStorePrefix
) === 0;
65 parsePropertyString: function(value
, defaultKey
) {
69 if (typeof value
!== 'string' || value
=== '') {
73 Ext
.Array
.each(value
.split(','), function(p
) {
74 var kv
= p
.split('=', 2);
75 if (Ext
.isDefined(kv
[1])) {
77 } else if (Ext
.isDefined(defaultKey
)) {
78 if (Ext
.isDefined(res
[defaultKey
])) {
79 error
= 'defaultKey may be only defined once in propertyString';
80 return false; // break
82 res
[defaultKey
] = kv
[0];
84 error
= 'invalid propertyString, not a key=value pair and no defaultKey defined';
85 return false; // break
90 if (error
!== undefined) {
98 printPropertyString: function(data
, defaultKey
) {
100 gotDefaultKeyVal
= false,
103 Ext
.Object
.each(data
, function(key
, value
) {
104 if (defaultKey
!== undefined && key
=== defaultKey
) {
105 gotDefaultKeyVal
= true;
106 defaultKeyVal
= value
;
107 } else if (value
!== '' && value
!== undefined) {
108 stringparts
.push(key
+ '=' + value
);
112 stringparts
= stringparts
.sort();
113 if (gotDefaultKeyVal
) {
114 stringparts
.unshift(defaultKeyVal
);
117 return stringparts
.join(',');
120 // helper for deleting field which are set to there default values
121 delete_if_default: function(values
, fieldname
, default_val
, create
) {
122 if (values
[fieldname
] === '' || values
[fieldname
] === default_val
) {
125 if (Ext
.isArray(values
.delete)) {
126 values
.delete.push(fieldname
);
128 values
.delete += ',' + fieldname
;
131 values
.delete = [fieldname
];
135 delete values
[fieldname
];
140 render_datetime_utc: function(datetime
) {
141 let pad
= (number
) => number
< 10 ? '0' + number
: number
;
142 return datetime
.getUTCFullYear() +
143 '-' + pad(datetime
.getUTCMonth() + 1) +
144 '-' + pad(datetime
.getUTCDate()) +
145 'T' + pad(datetime
.getUTCHours()) +
146 ':' + pad(datetime
.getUTCMinutes()) +
147 ':' + pad(datetime
.getUTCSeconds()) +
151 render_datastore_worker_id: function(id
, what
) {
152 const res
= id
.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
154 let datastore
= res
[1], backupGroup
= `${res[2]}/${res[3]}`;
155 if (res
[4] !== undefined) {
156 let datetime
= Ext
.Date
.parse(parseInt(res
[5], 16), 'U');
157 let utctime
= PBS
.Utils
.render_datetime_utc(datetime
);
158 return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
160 return `Datastore ${datastore} ${what} ${backupGroup}`;
163 return `Datastore ${what} ${id}`;
166 render_prune_job_worker_id: function(id
, what
) {
167 const res
= id
.match(/^(\S+?):(\S+)$/);
169 return `${what} on Datastore ${id}`;
171 let datastore
= res
[1], namespace = res
[2];
172 return `${what} on Datastore ${datastore} Namespace ${namespace}`;
175 render_tape_backup_id: function(id
, what
) {
176 const res
= id
.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/);
178 let datastore
= res
[1];
181 return `${what} ${datastore} (pool ${pool}, drive ${drive})`;
183 return `${what} ${id}`;
186 render_drive_load_media_id: function(id
, what
) {
187 const res
= id
.match(/^(\S+?):(\S+?)$/);
191 return gettext('Drive') + ` ${drive} - ${what} '${label}'`;
194 return `${what} ${id}`;
197 // mimics Display trait in backend
198 renderKeyID: function(fingerprint
) {
199 return fingerprint
.substring(0, 23);
202 render_task_status: function(value
, metadata
, record
) {
203 if (!record
.data
['last-run-upid']) {
207 if (!record
.data
['last-run-endtime']) {
208 metadata
.tdCls
= 'x-grid-row-loading';
212 let parsed
= Proxmox
.Utils
.parse_task_status(value
);
217 icon
= 'question faded';
218 text
= Proxmox
.Utils
.unknownText
;
221 icon
= 'times critical';
222 text
= Proxmox
.Utils
.errorText
+ ': ' + value
;
225 icon
= 'exclamation warning';
229 text
= gettext("OK");
232 return `<i class="fa fa-${icon}"></i> ${text}`;
235 render_next_task_run: function(value
, metadat
, record
) {
236 if (!value
) return '-';
238 let now
= new Date();
239 let next
= new Date(value
*1000);
242 return gettext('pending');
244 return Proxmox
.Utils
.render_timestamp(value
);
247 render_optional_timestamp: function(value
, metadata
, record
) {
248 if (!value
) return '-';
249 return Proxmox
.Utils
.render_timestamp(value
);
252 parse_datastore_worker_id: function(type
, id
) {
255 if (type
.startsWith('verif')) {
256 res
= PBS
.Utils
.VERIFICATION_JOB_ID_RE
.exec(id
);
260 } else if (type
.startsWith('sync')) {
261 res
= PBS
.Utils
.SYNC_JOB_ID_RE
.exec(id
);
265 } else if (type
=== 'backup') {
266 res
= PBS
.Utils
.BACKUP_JOB_ID_RE
.exec(id
);
270 } else if (type
=== 'garbage_collection') {
272 } else if (type
=== 'prune') {
280 extractTokenUser: function(tokenid
) {
281 return tokenid
.match(/^(.+)!([^!]+)$/)[1];
284 extractTokenName: function(tokenid
) {
285 return tokenid
.match(/^(.+)!([^!]+)$/)[2];
288 render_estimate: function(value
, metaData
, record
) {
289 if (record
.data
.avail
=== 0) {
290 return gettext("Full");
293 if (value
=== undefined) {
294 return gettext('Not enough data');
297 let now
= new Date();
298 let estimate
= new Date(value
*1000);
300 let timespan
= (estimate
- now
)/1000;
302 if (Number(estimate
) <= Number(now
) || isNaN(timespan
)) {
303 return gettext('Never');
306 let duration
= Proxmox
.Utils
.format_duration_human(timespan
);
307 return Ext
.String
.format(gettext("in {0}"), duration
);
310 // FIXME: deprecated by Proxmox.Utils.render_size_usage ?!
311 render_size_usage: function(val
, max
) {
313 return gettext('N/A');
315 return (val
*100/max
).toFixed(2) + '% (' +
316 Ext
.String
.format(gettext('{0} of {1}'),
317 Proxmox
.Utils
.format_size(val
), Proxmox
.Utils
.format_size(max
)) + ')';
320 get_help_tool: function(blockid
) {
321 let info
= Proxmox
.Utils
.get_help_info(blockid
);
322 if (info
=== undefined) {
323 info
= Proxmox
.Utils
.get_help_info('pbs_documentation_index');
325 if (info
=== undefined) {
326 throw "get_help_info failed"; // should not happen
329 let docsURI
= window
.location
.origin
+ info
.link
;
330 let title
= info
.title
;
332 title
+= ' - ' + info
.subtitle
;
337 handler: function() {
338 window
.open(docsURI
);
343 calculate_dedup_factor: function(gcstatus
) {
345 if (gcstatus
['disk-bytes'] > 0) {
346 dedup
= (gcstatus
['index-data-bytes'] || 0)/gcstatus
['disk-bytes'];
351 parse_snapshot_id: function(snapshot
) {
353 return [undefined, undefined, undefined];
355 let nsRegex
= /(?:^|\/)(ns\/([^/]+))/g
;
358 snapshot
= snapshot
.replace(nsRegex
, (_
, nsPath
, ns
) => { nsPaths
.push(nsPath
); namespaces
.push(ns
); return ""; });
359 let [_match
, type
, group
, id
] = /^\/?([^/]+)\/([^/]+)\/(.+)$/.exec(snapshot
);
361 return [type
, group
, id
, namespaces
.join('/'), nsPaths
.join('/')];
364 get_type_icon_cls: function(btype
) {
366 if (btype
.startsWith('vm')) {
368 } else if (btype
.startsWith('ct')) {
370 } else if (btype
.startsWith('host')) {
376 constructor: function() {
379 let PROXMOX_SAFE_ID_REGEX
= "([A-Za-z0-9_][A-Za-z0-9._-]*)";
380 me
.SAFE_ID_RE
= new RegExp(`^${PROXMOX_SAFE_ID_REGEX}$`);
381 // only anchored at beginning, only parses datastore for now
382 me
.VERIFICATION_JOB_ID_RE
= new RegExp("^" + PROXMOX_SAFE_ID_REGEX
+ ':?');
383 me
.SYNC_JOB_ID_RE
= new RegExp("^" + PROXMOX_SAFE_ID_REGEX
+ ':' +
384 PROXMOX_SAFE_ID_REGEX
+ ':' + PROXMOX_SAFE_ID_REGEX
+ ':');
385 me
.BACKUP_JOB_ID_RE
= new RegExp("^" + PROXMOX_SAFE_ID_REGEX
+ ':');
387 // do whatever you want here
388 Proxmox
.Utils
.override_task_descriptions({
389 'acme-deactivate': (type
, id
) =>
390 Ext
.String
.format(gettext("Deactivate {0} Account"), 'ACME') + ` '${id || 'default'}'`,
391 'acme-register': (type
, id
) =>
392 Ext
.String
.format(gettext("Register {0} Account"), 'ACME') + ` '${id || 'default'}'`,
393 'acme-update': (type
, id
) =>
394 Ext
.String
.format(gettext("Update {0} Account"), 'ACME') + ` '${id || 'default'}'`,
395 'acme-new-cert': ['', gettext('Order Certificate')],
396 'acme-renew-cert': ['', gettext('Renew Certificate')],
397 'acme-revoke-cert': ['', gettext('Revoke Certificate')],
398 backup
: (type
, id
) => PBS
.Utils
.render_datastore_worker_id(id
, gettext('Backup')),
399 'barcode-label-media': [gettext('Drive'), gettext('Barcode-Label Media')],
400 'catalog-media': [gettext('Drive'), gettext('Catalog Media')],
401 'delete-datastore': [gettext('Datastore'), gettext('Remove Datastore')],
402 'delete-namespace': [gettext('Namespace'), gettext('Remove Namespace')],
403 dircreate
: [gettext('Directory Storage'), gettext('Create')],
404 dirremove
: [gettext('Directory'), gettext('Remove')],
405 'eject-media': [gettext('Drive'), gettext('Eject Media')],
406 "format-media": [gettext('Drive'), gettext('Format media')],
407 "forget-group": [gettext('Group'), gettext('Remove Group')],
408 garbage_collection
: ['Datastore', gettext('Garbage Collect')],
409 'realm-sync': ['Realm', gettext('User Sync')],
410 'inventory-update': [gettext('Drive'), gettext('Inventory Update')],
411 'label-media': [gettext('Drive'), gettext('Label Media')],
412 'load-media': (type
, id
) => PBS
.Utils
.render_drive_load_media_id(id
, gettext('Load Media')),
413 logrotate
: [null, gettext('Log Rotation')],
414 prune
: (type
, id
) => PBS
.Utils
.render_datastore_worker_id(id
, gettext('Prune')),
415 prunejob
: (type
, id
) => PBS
.Utils
.render_prune_job_worker_id(id
, gettext('Prune Job')),
416 reader
: (type
, id
) => PBS
.Utils
.render_datastore_worker_id(id
, gettext('Read Objects')),
417 'rewind-media': [gettext('Drive'), gettext('Rewind Media')],
418 sync
: ['Datastore', gettext('Remote Sync')],
419 syncjob
: [gettext('Sync Job'), gettext('Remote Sync')],
420 'tape-backup': (type
, id
) => PBS
.Utils
.render_tape_backup_id(id
, gettext('Tape Backup')),
421 'tape-backup-job': (type
, id
) => PBS
.Utils
.render_tape_backup_id(id
, gettext('Tape Backup Job')),
422 'tape-restore': ['Datastore', gettext('Tape Restore')],
423 'unload-media': [gettext('Drive'), gettext('Unload Media')],
424 verificationjob
: [gettext('Verify Job'), gettext('Scheduled Verification')],
425 verify
: ['Datastore', gettext('Verification')],
426 verify_group
: ['Group', gettext('Verification')],
427 verify_snapshot
: ['Snapshot', gettext('Verification')],
428 wipedisk
: ['Device', gettext('Wipe Disk')],
429 zfscreate
: [gettext('ZFS Storage'), gettext('Create')],
432 Proxmox
.Schema
.overrideAuthDomains({
434 name
: 'Proxmox Backup authentication server',
443 // Convert an ArrayBuffer to a base64url encoded string.
444 // A `null` value will be preserved for convenience.
445 bytes_to_base64url: function(bytes
) {
446 if (bytes
=== null) {
451 .from(new Uint8Array(bytes
))
452 .map(val
=> String
.fromCharCode(val
))
457 .replace(/[=]/g, '');
460 // Convert an a base64url string to an ArrayBuffer.
461 // A `null` value will be preserved for convenience.
462 base64url_to_bytes: function(b64u
) {
467 return new Uint8Array(
473 .map(val => val.charCodeAt(0)),
477 driveCommand: function(driveid, command, reqOpts) {
478 let params = Ext.apply(reqOpts, {
479 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
481 failure: function(response) {
482 Ext.Msg.alert(gettext('Error
'), response.htmlStatus);
486 Proxmox.Utils.API2Request(params);
489 showMediaLabelWindow: function(response) {
491 for (let [key, val] of Object.entries(response.result.data)) {
492 if (key === 'ctime
' || key === 'media
-set-ctime
') {
493 val = Proxmox.Utils.render_timestamp(val);
495 list.push({ key: key, value: val });
498 Ext.create('Ext
.window
.Window
', {
499 title: gettext('Label Information
'),
513 text: gettext('Property
'),
518 text: gettext('Value
'),
528 showCartridgeMemoryWindow: function(response) {
529 Ext.create('Ext
.window
.Window
', {
530 title: gettext('Cartridge Memory
'),
540 data: response.result.data,
550 text: gettext('Name
'),
555 text: gettext('Value
'),
565 showVolumeStatisticsWindow: function(response) {
567 for (let [key, val] of Object.entries(response.result.data)) {
568 if (key === 'total
-native-capacity
' ||
569 key === 'total
-used
-native-capacity
' ||
570 key === 'lifetime
-bytes
-read
' ||
571 key === 'lifetime
-bytes
-written
' ||
572 key === 'last
-mount
-bytes
-read
' ||
573 key === 'last
-mount
-bytes
-written
') {
574 val = Proxmox.Utils.format_size(val);
576 list.push({ key: key, value: val });
578 Ext.create('Ext
.window
.Window
', {
579 title: gettext('Volume Statistics
'),
593 text: gettext('Property
'),
598 text: gettext('Value
'),
608 showDriveStatusWindow: function(response) {
610 for (let [key, val] of Object.entries(response.result.data)) {
611 if (key === 'manufactured
') {
612 val = Proxmox.Utils.render_timestamp(val);
614 if (key === 'bytes
-read
' || key === 'bytes
-written
') {
615 val = Proxmox.Utils.format_size(val);
617 list.push({ key: key, value: val });
620 Ext.create('Ext
.window
.Window
', {
621 title: gettext('Status
'),
635 text: gettext('Property
'),
640 text: gettext('Value
'),
650 renderDriveState: function(value, md) {
652 return gettext('Idle
');
655 let icon = '<i
class="fa fa-spinner fa-pulse fa-fw"></i
>';
657 if (value.startsWith("UPID")) {
658 let upid = Proxmox.Utils.parse_task_upid(value);
659 md.tdCls = "pointer";
660 return `${icon} ${upid.desc}`;
663 return `${icon} ${value}`;
666 // FIXME: this "parser" is brittle and relies on the order the arguments will appear in
667 parseMaintenanceMode: function(mode) {
668 let [type, message] = mode.split(/,(.+)/);
669 type = type.split("=").pop();
670 message = message ? message.split("=")[1]
671 .replace(/^"(.*)"$/, '$1')
672 .replaceAll('\\"', '"') : null;
673 return [type, message];
676 renderMaintenance: function(mode, activeTasks) {
678 return gettext('None
');
681 let [type, message] = PBS.Utils.parseMaintenanceMode(mode);
685 if (activeTasks !== undefined) {
686 const conflictingTasks = activeTasks.write + (type === 'offline
' ? activeTasks.read : 0);
688 if (conflictingTasks > 0) {
689 extra += '| <i
class="fa fa-spinner fa-pulse fa-fw"></i
> ';
690 extra += Ext.String.format(gettext('{0} conflicting tasks still active
.'), conflictingTasks);
692 extra += '<i
class="fa fa-check"></i
>';
697 extra += ` ("${message}")`;
700 let modeText = Proxmox.Utils.unknownText;
702 case 'read
-only
': modeText = gettext("Read-only");
704 case 'offline
': modeText = gettext("Offline");
707 return `${modeText} ${extra}`;
710 render_optional_namespace: function(value, metadata, record) {
711 if (!value) return `- (${gettext('Root')})`;
712 return Ext.String.htmlEncode(value);
715 render_optional_remote: function(value, metadata, record) {
717 return `- (${gettext('Local')})`;
719 return Ext.String.htmlEncode(value);
724 '__default__
': Proxmox.Utils.defaultText + ` (${gettext('Inode')})`,
725 none: gettext('None
'),
726 inode: gettext('Inode
'),
729 '__default__
': Proxmox.Utils.defaultText + ` (${gettext('Filesystem')})`,
730 none: gettext('None
'),
731 file: gettext('File
'),
732 filesystem: gettext('Filesystem
'),
736 render_tuning_options: function(tuning) {
738 let order = tuning['chunk
-order
'];
739 delete tuning['chunk
-order
'];
740 order = PBS.Utils.tuningOptions['chunk
-order
'][order ?? '__default__
'];
741 options.push(`${gettext('Chunk Order')}: ${order}`);
743 let sync = tuning['sync
-level
'];
744 delete tuning['sync
-level
'];
745 sync = PBS.Utils.tuningOptions['sync
-level
'][sync ?? '__default__
'];
746 options.push(`${gettext('Sync Level')}: ${sync}`);
748 for (const [k, v] of Object.entries(tuning)) {
749 options.push(`${k}: ${v}`);
752 return options.join(', ');