]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/window/FileBrowser.js
file browser: unify file type schema and avoid switch-case bloat
[proxmox-widget-toolkit.git] / src / window / FileBrowser.js
1 Ext.define('proxmox-file-tree', {
2 extend: 'Ext.data.Model',
3
4 fields: [
5 'filepath', 'text', 'type', 'size',
6 {
7 name: 'sizedisplay',
8 calculate: data => {
9 if (data.size === undefined) {
10 return '';
11 } else if (data.type === 'd') {
12 let fs = data.size === 1 ? gettext('{0} item') : gettext('{0} items');
13 return Ext.String.format(fs, data.size);
14 }
15
16 return Proxmox.Utils.format_size(data.size);
17 },
18 },
19 {
20 name: 'mtime',
21 type: 'date',
22 dateFormat: 'timestamp',
23 },
24 {
25 name: 'iconCls',
26 calculate: function(data) {
27 let icon = Proxmox.Schema.pxarFileTypes[data.type]?.icon ?? 'file-o';
28 if (data.expanded && data.type === 'd') {
29 icon = 'folder-open-o';
30 }
31 return `fa fa-${icon}`;
32 },
33 },
34 ],
35 idProperty: 'filepath',
36 });
37
38 Ext.define("Proxmox.window.FileBrowser", {
39 extend: "Ext.window.Window",
40
41 width: 800,
42 height: 600,
43
44 modal: true,
45
46 config: {
47 // the base-URL to get the list of files. required.
48 listURL: '',
49
50 // the base download URL, e.g., something like '/api2/...'
51 downloadURL: '',
52
53 // extra parameters set as proxy paramns and for an actual download request
54 extraParams: {},
55
56 // the file types for which the download button should be enabled
57 downloadableFileTypes: {
58 'h': true, // hardlinks
59 'f': true, // "normal" files
60 'd': true, // directories
61 },
62
63 // enable tar download, this will add a menu to the
64 // "Download" button when the selection can be downloaded as
65 // .tar files
66 enableTar: false,
67
68 // prefix to prepend to downloaded file names
69 downloadPrefix: '',
70 },
71
72 controller: {
73 xclass: 'Ext.app.ViewController',
74
75 buildUrl: function(baseurl, params) {
76 let url = new URL(baseurl, window.location.origin);
77 for (const [key, value] of Object.entries(params)) {
78 url.searchParams.append(key, value);
79 }
80
81 return url.href;
82 },
83
84 downloadTar: function() {
85 this.downloadFile(true);
86 },
87
88 downloadZip: function() {
89 this.downloadFile(false);
90 },
91
92 downloadFile: function(tar) {
93 let me = this;
94 let view = me.getView();
95 let tree = me.lookup('tree');
96 let selection = tree.getSelection();
97 if (!selection || selection.length < 1) return;
98
99 let data = selection[0].data;
100
101 let params = { ...view.extraParams };
102 params.filepath = data.filepath;
103
104 let atag = document.createElement('a');
105 atag.download = view.downloadPrefix + data.text;
106 if (data.type === 'd') {
107 if (tar) {
108 params.tar = 1;
109 atag.download += ".tar.zst";
110 } else {
111 atag.download += ".zip";
112 }
113 }
114 atag.href = me.buildUrl(view.downloadURL, params);
115 atag.click();
116 },
117
118 fileChanged: function() {
119 let me = this;
120 let view = me.getView();
121 let tree = me.lookup('tree');
122 let selection = tree.getSelection();
123 if (!selection || selection.length < 1) return;
124
125 let data = selection[0].data;
126 let st = Ext.String.format(gettext('Selected "{0}"'), atob(data.filepath));
127 view.lookup('selectText').setText(st);
128
129 let canDownload = view.downloadURL && view.downloadableFileTypes[data.type];
130 let enableMenu = view.enableTar && data.type === 'd';
131
132 let downloadBtn = view.lookup('downloadBtn');
133 downloadBtn.setDisabled(!canDownload || enableMenu);
134 downloadBtn.setHidden(!canDownload || enableMenu);
135
136 let menuBtn = view.lookup('menuBtn');
137 menuBtn.setDisabled(!canDownload || !enableMenu);
138 menuBtn.setHidden(!canDownload || !enableMenu);
139 },
140
141 errorHandler: function(error, msg) {
142 let me = this;
143 if (error?.status === 503) {
144 return false;
145 }
146 me.lookup('downloadBtn').setDisabled(true);
147 me.lookup('menuBtn').setDisabled(true);
148 if (me.initialLoadDone) {
149 Ext.Msg.alert(gettext('Error'), msg);
150 return true;
151 }
152 return false;
153 },
154
155 init: function(view) {
156 let me = this;
157 let tree = me.lookup('tree');
158
159 if (!view.listURL) {
160 throw "no list URL given";
161 }
162
163 let store = tree.getStore();
164 let proxy = store.getProxy();
165
166 let errorCallback = (error, msg) => me.errorHandler(error, msg);
167 proxy.setUrl(view.listURL);
168 proxy.setTimeout(60*1000);
169 proxy.setExtraParams(view.extraParams);
170
171 tree.mon(store, 'beforeload', () => {
172 Proxmox.Utils.setErrorMask(tree, true);
173 });
174 tree.mon(store, 'load', (treestore, rec, success, operation, node) => {
175 if (success) {
176 Proxmox.Utils.setErrorMask(tree, false);
177 return;
178 }
179 if (!node.loadCount) {
180 node.loadCount = 0; // ensure its numeric
181 }
182 // trigger a reload if we got a 503 answer from the proxy
183 if (operation?.error?.status === 503 && node.loadCount < 10) {
184 node.collapse();
185 node.expand();
186 node.loadCount++;
187 return;
188 }
189
190 let error = operation.getError();
191 let msg = Proxmox.Utils.getResponseErrorMessage(error);
192 if (!errorCallback(error, msg)) {
193 Proxmox.Utils.setErrorMask(tree, msg);
194 } else {
195 Proxmox.Utils.setErrorMask(tree, false);
196 }
197 });
198 store.load((rec, op, success) => {
199 let root = store.getRoot();
200 root.expand(); // always expand invisible root node
201 if (view.archive === 'all') {
202 root.expandChildren(false);
203 } else if (view.archive) {
204 let child = root.findChild('text', view.archive);
205 if (child) {
206 child.expand();
207 setTimeout(function() {
208 tree.setSelection(child);
209 tree.getView().focusRow(child);
210 }, 10);
211 }
212 } else if (root.childNodes.length === 1) {
213 root.firstChild.expand();
214 }
215 me.initialLoadDone = success;
216 });
217 },
218
219 control: {
220 'treepanel': {
221 selectionchange: 'fileChanged',
222 },
223 },
224 },
225
226 layout: 'fit',
227 items: [
228 {
229 xtype: 'treepanel',
230 scrollable: true,
231 rootVisible: false,
232 reference: 'tree',
233 store: {
234 autoLoad: false,
235 model: 'proxmox-file-tree',
236 defaultRootId: '/',
237 nodeParam: 'filepath',
238 sorters: 'text',
239 proxy: {
240 appendId: false,
241 type: 'proxmox',
242 },
243 },
244
245 viewConfig: {
246 loadMask: false,
247 },
248
249 columns: [
250 {
251 text: gettext('Name'),
252 xtype: 'treecolumn',
253 flex: 1,
254 dataIndex: 'text',
255 renderer: Ext.String.htmlEncode,
256 },
257 {
258 text: gettext('Size'),
259 dataIndex: 'sizedisplay',
260 sorter: {
261 sorterFn: function(a, b) {
262 if (a.data.type === 'd' && b.data.type !== 'd') {
263 return -1;
264 } else if (a.data.type !== 'd' && b.data.type === 'd') {
265 return 1;
266 }
267
268 let asize = a.data.size || 0;
269 let bsize = b.data.size || 0;
270
271 return asize - bsize;
272 },
273 },
274 },
275 {
276 text: gettext('Modified'),
277 dataIndex: 'mtime',
278 minWidth: 200,
279 },
280 {
281 text: gettext('Type'),
282 dataIndex: 'type',
283 renderer: (v) => Proxmox.Schema.pxarFileTypes[v]?.label ?? Proxmox.Utils.unknownText,
284 },
285 ],
286 },
287 ],
288
289 fbar: [
290 {
291 text: '',
292 xtype: 'label',
293 reference: 'selectText',
294 },
295 {
296 text: gettext('Download'),
297 xtype: 'button',
298 handler: 'downloadZip',
299 reference: 'downloadBtn',
300 disabled: true,
301 hidden: true,
302 },
303 {
304 text: gettext('Download as'),
305 xtype: 'button',
306 reference: 'menuBtn',
307 menu: {
308 items: [
309 {
310 iconCls: 'fa fa-fw fa-file-zip-o',
311 text: gettext('.zip'),
312 handler: 'downloadZip',
313 reference: 'downloadZip',
314 },
315 {
316 iconCls: 'fa fa-fw fa-archive',
317 text: gettext('.tar.zst'),
318 handler: 'downloadTar',
319 reference: 'downloadTar',
320 },
321 ],
322 },
323 },
324 ],
325 });