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