diff --git a/www/DataStoreContent.js b/www/DataStoreContent.js index 097e037e..a8bd5b74 100644 --- a/www/DataStoreContent.js +++ b/www/DataStoreContent.js @@ -214,6 +214,43 @@ Ext.define('PBS.DataStoreContent', { }, files: data.files, }).show(); + }, + + openPxarBrowser: function() { + let me = this; + let view = me.getView(); + + let rec = view.selModel.getSelection()[0]; + if (!(rec && rec.data)) return; + let data = rec.data; + + let encrypted = false; + data.files.forEach(file => { + if (file.filename === 'catalog.pcat1.didx' && file.encrypted) { + encrypted = true; + } + }); + + if (encrypted) { + Ext.Msg.alert( + gettext('Cannot open Catalog'), + gettext('Only unencrypted Backups can be opened on the server. Please use the client with the decryption key instead.'), + ); + return; + } + + let id = data['backup-id']; + let time = data['backup-time']; + let type = data['backup-type']; + let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]); + + Ext.create('PBS.window.FileBrowser', { + title: `${type}/${id}/${timetext}`, + datastore: view.datastore, + 'backup-id': id, + 'backup-time': (time.getTime()/1000).toFixed(0), + 'backup-type': type, + }).show(); } }, @@ -306,6 +343,16 @@ Ext.define('PBS.DataStoreContent', { enableFn: function(record) { return !!record.data.leaf; }, + }, + { + xtype: "proxmoxButton", + text: gettext('PXAR File Browser'), + disabled: true, + handler: 'openPxarBrowser', + parentXType: 'pbsDataStoreContent', + enableFn: function(record) { + return !!record.data.leaf && record.data.files.some(el => el.filename.endsWith('pxar.didx')); + }, } ], }); diff --git a/www/Makefile b/www/Makefile index be3117a2..76a23806 100644 --- a/www/Makefile +++ b/www/Makefile @@ -19,6 +19,7 @@ JSSRC= \ window/ACLEdit.js \ window/DataStoreEdit.js \ window/CreateDirectory.js \ + window/FileBrowser.js \ window/BackupFileDownloader.js \ dashboard/DataStoreStatistics.js \ dashboard/LongestTasks.js \ diff --git a/www/window/FileBrowser.js b/www/window/FileBrowser.js new file mode 100644 index 00000000..7fafd4f6 --- /dev/null +++ b/www/window/FileBrowser.js @@ -0,0 +1,229 @@ +Ext.define('pbs-file-tree', { + extend: 'Ext.data.Model', + + fields: [ 'filepath', 'text', 'type', 'size', + { + name: 'mtime', + type: 'date', + dateFormat: 'timestamp', + }, + { + name: 'iconCls', + calculate: function(data) { + let icon = 'file-o'; + switch (data.type) { + case 'b': // block device + icon = 'cube'; + break; + case 'c': // char device + icon = 'tty'; + break; + case 'd': + icon = data.expanded ? 'folder-open-o' : 'folder-o'; + break; + case 'f': //regular file + icon = 'file-text-o'; + break; + case 'h': // hardlink + icon = 'file-o'; + break; + case 'l': // softlink + icon = 'link'; + break; + case 'p': // pipe/fifo + icon = 'exchange'; + break; + case 's': // socket + icon = 'plug'; + break; + default: + icon = 'file-o'; + break; + } + + return `fa fa-${icon}`; + }, + } + ], + idProperty: 'filepath', +}); + +Ext.define("PBS.window.FileBrowser", { + extend: "Ext.window.Window", + + width: 800, + height: 400, + + modal: true, + + controller: { + xclass: 'Ext.app.ViewController', + + buildUrl: function(baseurl, params) { + let url = new URL(baseurl, window.location.origin); + for (const [key, value] of Object.entries(params)) { + url.searchParams.append(key, value); + } + + return url.href; + }, + + downloadFile: function() { + let me = this; + let view = me.getView(); + let tree = me.lookup('tree'); + let selection = tree.getSelection(); + if (!selection || selection.length < 1) return; + + let data = selection[0].data; + + let atag = document.createElement('a'); + + atag.download = data.text; + let params = { + 'backup-id': view['backup-id'], + 'backup-type': view['backup-type'], + 'backup-time': view['backup-time'], + }; + params['filepath'] = data.filepath; + atag.download = data.text; + atag.href = me.buildUrl(`/api2/json/admin/datastore/${view.datastore}/pxar-file-download`, params); + atag.click(); + }, + + fileChanged: function() { + let me = this; + let view = me.getView(); + let tree = me.lookup('tree'); + let selection = tree.getSelection(); + if (!selection || selection.length < 1) return; + + let data = selection[0].data; + + let canDownload = false; + switch (data.type) { + case 'h': + case 'f': + canDownload = true; + break; + default: break; + } + + me.lookup('downloadBtn').setDisabled(!canDownload); + }, + + init: function(view) { + let me = this; + let tree = me.lookup('tree'); + + if (!view['backup-id']) { + throw "no backup-id given"; + } + + if (!view['backup-type']) { + throw "no backup-id given"; + } + + if (!view['backup-time']) { + throw "no backup-id given"; + } + + if (!view.datastore) { + throw "no datastore given"; + } + + let store = tree.getStore(); + let proxy = store.getProxy(); + + Proxmox.Utils.monStoreErrors(tree, store, true); + proxy.setUrl(`/api2/json/admin/datastore/${view.datastore}/catalog`); + proxy.setExtraParams({ + 'backup-id': view['backup-id'], + 'backup-type': view['backup-type'], + 'backup-time': view['backup-time'], + }); + store.load(); + store.getRoot().expand(); + }, + + control: { + 'treepanel': { + selectionchange: 'fileChanged', + }, + }, + }, + + layout: 'fit', + items: [ + { + xtype: 'treepanel', + scrollable: true, + rootVisible: false, + reference: 'tree', + store: { + autoLoad: false, + model: 'pbs-file-tree', + nodeParam: 'filepath', + sorters: 'text', + proxy: { + appendId: false, + type: 'proxmox', + }, + }, + + columns: [ + { + text: gettext('Name'), + xtype: 'treecolumn', + flex: 1, + dataIndex: 'text', + renderer: Ext.String.htmlEncode, + }, + { + text: gettext('Size'), + dataIndex: 'size', + renderer: value => value === undefined ? '' : Proxmox.Utils.format_size(value), + sorter: { + sorterFn: function(a, b) { + let asize = a.data.size || 0; + let bsize = b.data.size || 0; + + return asize - bsize; + }, + } + }, + { + text: gettext('Modified'), + dataIndex: 'mtime', + minWidth: 200, + }, + { + text: gettext('Type'), + dataIndex: 'type', + renderer: function(value) { + switch (value) { + case 'b': return gettext('Block Device'); + case 'c': return gettext('Character Device'); + case 'd': return gettext('Directory'); + case 'f': return gettext('File'); + case 'h': return gettext('Hardlink'); + case 'l': return gettext('Softlink'); + case 'p': return gettext('Pipe/Fifo'); + case 's': return gettext('Socket'); + default: return Proxmox.Utils.unknownText; + } + } + }, + ] + } + ], + + buttons: [ + { + text: gettext('Download'), + handler: 'downloadFile', + reference: 'downloadBtn', + disabled: true, + } + ], +});