1
|
|
# -*- coding: utf-8 -*-
|
2
|
6
|
"""
|
3
|
|
Defines a FileSelector widget which allows selecting files and
|
4
|
|
directories on the server.
|
5
|
|
"""
|
6
|
6
|
from __future__ import absolute_import, division, unicode_literals
|
7
|
|
|
8
|
6
|
import os
|
9
|
|
|
10
|
6
|
from collections import OrderedDict
|
11
|
6
|
from fnmatch import fnmatch
|
12
|
|
|
13
|
6
|
import param
|
14
|
|
|
15
|
6
|
from ..layout import Column, Divider, Row
|
16
|
6
|
from ..viewable import Layoutable
|
17
|
6
|
from .base import CompositeWidget
|
18
|
6
|
from .button import Button
|
19
|
6
|
from .input import TextInput
|
20
|
6
|
from .select import CrossSelector
|
21
|
|
|
22
|
|
|
23
|
6
|
def scan_path(path, file_pattern='*'):
|
24
|
|
"""
|
25
|
|
Scans the supplied path for files and directories and optionally
|
26
|
|
filters the files with the file keyword, returning a list of sorted
|
27
|
|
paths of all directories and files.
|
28
|
|
|
29
|
|
Arguments
|
30
|
|
---------
|
31
|
|
path: str
|
32
|
|
The path to search
|
33
|
|
file_pattern: str
|
34
|
|
A glob-like pattern to filter the files
|
35
|
|
|
36
|
|
Returns
|
37
|
|
-------
|
38
|
|
A sorted list of paths
|
39
|
|
"""
|
40
|
6
|
paths = [os.path.join(path, p) for p in os.listdir(path)]
|
41
|
6
|
dirs = [p for p in paths if os.path.isdir(p)]
|
42
|
6
|
files = [p for p in paths if os.path.isfile(p) and
|
43
|
|
fnmatch(os.path.basename(p), file_pattern)]
|
44
|
6
|
for p in paths:
|
45
|
6
|
if not os.path.islink(p):
|
46
|
6
|
continue
|
47
|
0
|
path = os.path.realpath(p)
|
48
|
0
|
if os.path.isdir(path):
|
49
|
0
|
dirs.append(p)
|
50
|
0
|
elif os.path.isfile(path):
|
51
|
0
|
dirs.append(p)
|
52
|
|
else:
|
53
|
0
|
continue
|
54
|
6
|
return dirs, files
|
55
|
|
|
56
|
|
|
57
|
6
|
class FileSelector(CompositeWidget):
|
58
|
|
|
59
|
6
|
directory = param.String(default=os.getcwd(), doc="""
|
60
|
|
The directory to explore.""")
|
61
|
|
|
62
|
6
|
file_pattern = param.String(default='*', doc="""
|
63
|
|
A glob-like pattern to filter the files.""")
|
64
|
|
|
65
|
6
|
only_files = param.Boolean(default=False, doc="""
|
66
|
|
Whether to only allow selecting files.""")
|
67
|
|
|
68
|
6
|
margin = param.Parameter(default=(5, 10, 20, 10), doc="""
|
69
|
|
Allows to create additional space around the component. May
|
70
|
|
be specified as a two-tuple of the form (vertical, horizontal)
|
71
|
|
or a four-tuple (top, right, bottom, left).""")
|
72
|
|
|
73
|
6
|
show_hidden = param.Boolean(default=False, doc="""
|
74
|
|
Whether to show hidden files and directories (starting with
|
75
|
|
a period).""")
|
76
|
|
|
77
|
6
|
size = param.Integer(default=10, doc="""
|
78
|
|
The number of options shown at once (note this is the only
|
79
|
|
way to control the height of this widget)""")
|
80
|
|
|
81
|
6
|
value = param.List(default=[], doc="""
|
82
|
|
List of selected files.""")
|
83
|
|
|
84
|
6
|
_composite_type = Column
|
85
|
|
|
86
|
6
|
def __init__(self, directory=None, **params):
|
87
|
6
|
from ..pane import Markdown
|
88
|
6
|
if directory is not None:
|
89
|
6
|
params['directory'] = os.path.abspath(os.path.expanduser(directory))
|
90
|
6
|
if params.get('width') and params.get('height') and 'sizing_mode' not in params:
|
91
|
0
|
params['sizing_mode'] = None
|
92
|
|
|
93
|
6
|
super(FileSelector, self).__init__(**params)
|
94
|
|
|
95
|
|
# Set up layout
|
96
|
6
|
layout = {p: getattr(self, p) for p in Layoutable.param
|
97
|
|
if p not in ('name', 'height', 'margin') and getattr(self, p) is not None}
|
98
|
6
|
sel_layout = dict(layout, sizing_mode='stretch_both', height=None, margin=0)
|
99
|
6
|
self._selector = CrossSelector(filter_fn=lambda p, f: fnmatch(f, p),
|
100
|
|
size=self.size, **sel_layout)
|
101
|
6
|
self._back = Button(name='◀', width=25, margin=(5, 10, 0, 0), disabled=True)
|
102
|
6
|
self._forward = Button(name='▶', width=25, margin=(5, 10), disabled=True)
|
103
|
6
|
self._up = Button(name='⬆', width=25, margin=(5, 10), disabled=True)
|
104
|
6
|
self._directory = TextInput(value=self.directory, margin=(5, 10), width_policy='max')
|
105
|
6
|
self._go = Button(name='⬇', disabled=True, width=25, margin=(5, 15, 0, 0))
|
106
|
6
|
self._nav_bar = Row(
|
107
|
|
self._back, self._forward, self._up, self._directory, self._go,
|
108
|
|
**dict(layout, width=None, margin=0, width_policy='max')
|
109
|
|
)
|
110
|
6
|
self._composite[:] = [self._nav_bar, Divider(margin=0), self._selector]
|
111
|
6
|
self._selector._selected.insert(0, Markdown('### Selected files', margin=0))
|
112
|
6
|
self._selector._unselected.insert(0, Markdown('### File Browser', margin=0))
|
113
|
6
|
self.link(self._selector, size='size')
|
114
|
|
|
115
|
|
# Set up state
|
116
|
6
|
self._stack = []
|
117
|
6
|
self._cwd = None
|
118
|
6
|
self._position = -1
|
119
|
6
|
self._update_files(True)
|
120
|
|
|
121
|
|
# Set up callback
|
122
|
6
|
self.link(self._directory, directory='value')
|
123
|
6
|
self._selector.param.watch(self._update_value, 'value')
|
124
|
6
|
self._go.on_click(self._update_files)
|
125
|
6
|
self._up.on_click(self._go_up)
|
126
|
6
|
self._back.on_click(self._go_back)
|
127
|
6
|
self._forward.on_click(self._go_forward)
|
128
|
6
|
self._directory.param.watch(self._dir_change, 'value')
|
129
|
6
|
self._selector._lists[False].param.watch(self._select, 'value')
|
130
|
6
|
self._selector._lists[False].param.watch(self._filter_blacklist, 'options')
|
131
|
|
|
132
|
6
|
def _update_value(self, event):
|
133
|
6
|
value = [v for v in event.new if not self.only_files or os.path.isfile(v)]
|
134
|
6
|
self._selector.value = value
|
135
|
6
|
self.value = value
|
136
|
|
|
137
|
6
|
def _dir_change(self, event):
|
138
|
6
|
path = os.path.abspath(os.path.expanduser(self._directory.value))
|
139
|
6
|
if not path.startswith(self.directory):
|
140
|
0
|
self._directory.value = self.directory
|
141
|
0
|
return
|
142
|
6
|
elif path != self._directory.value:
|
143
|
0
|
self._directory.value = path
|
144
|
6
|
self._go.disabled = path == self._cwd
|
145
|
|
|
146
|
6
|
def _update_files(self, event=None):
|
147
|
6
|
path = os.path.abspath(self._directory.value)
|
148
|
6
|
if not os.path.isdir(path):
|
149
|
0
|
self._selector.options = ['Entered path is not valid']
|
150
|
0
|
self._selector.disabled = True
|
151
|
0
|
return
|
152
|
6
|
elif event is not None and (not self._stack or path != self._stack[-1]):
|
153
|
6
|
self._stack.append(path)
|
154
|
6
|
self._position += 1
|
155
|
|
|
156
|
6
|
self._cwd = path
|
157
|
6
|
self._go.disabled = True
|
158
|
6
|
self._up.disabled = path == self.directory
|
159
|
6
|
if self._position == len(self._stack)-1:
|
160
|
6
|
self._forward.disabled = True
|
161
|
6
|
if 0 <= self._position and len(self._stack) > 1:
|
162
|
6
|
self._back.disabled = False
|
163
|
|
|
164
|
6
|
selected = self.value
|
165
|
6
|
dirs, files = scan_path(path, self.file_pattern)
|
166
|
6
|
for s in selected:
|
167
|
6
|
check = os.path.realpath(s) if os.path.islink(s) else s
|
168
|
6
|
if os.path.isdir(check):
|
169
|
6
|
dirs.append(s)
|
170
|
0
|
elif os.path.isfile(check):
|
171
|
0
|
dirs.append(s)
|
172
|
|
|
173
|
6
|
paths = [p for p in sorted(dirs)+sorted(files)
|
174
|
|
if self.show_hidden or not os.path.basename(p).startswith('.')]
|
175
|
6
|
abbreviated = [('📁' if f in dirs else '')+os.path.relpath(f, self._cwd) for f in paths]
|
176
|
6
|
options = OrderedDict(zip(abbreviated, paths))
|
177
|
6
|
self._selector.options = options
|
178
|
6
|
self._selector.value = selected
|
179
|
|
|
180
|
6
|
def _filter_blacklist(self, event):
|
181
|
|
"""
|
182
|
|
Ensure that if unselecting a currently selected path and it
|
183
|
|
is not in the current working directory then it is removed
|
184
|
|
from the blacklist.
|
185
|
|
"""
|
186
|
6
|
dirs, files = scan_path(self._cwd, self.file_pattern)
|
187
|
6
|
paths = [('📁' if p in dirs else '')+os.path.relpath(p, self._cwd) for p in dirs+files]
|
188
|
6
|
blacklist = self._selector._lists[False]
|
189
|
6
|
options = OrderedDict(self._selector._items)
|
190
|
6
|
self._selector.options.clear()
|
191
|
6
|
self._selector.options.update([
|
192
|
|
(k, v) for k, v in options.items() if k in paths or v in self.value
|
193
|
|
])
|
194
|
6
|
blacklist.options = [o for o in blacklist.options if o in paths]
|
195
|
|
|
196
|
6
|
def _select(self, event):
|
197
|
6
|
if len(event.new) != 1:
|
198
|
6
|
self._directory.value = self._cwd
|
199
|
6
|
return
|
200
|
|
|
201
|
6
|
relpath = event.new[0].replace('📁', '')
|
202
|
6
|
sel = os.path.abspath(os.path.join(self._cwd, relpath))
|
203
|
6
|
if os.path.isdir(sel):
|
204
|
6
|
self._directory.value = sel
|
205
|
|
else:
|
206
|
6
|
self._directory.value = self._cwd
|
207
|
|
|
208
|
6
|
def _go_back(self, event):
|
209
|
6
|
self._position -= 1
|
210
|
6
|
self._directory.value = self._stack[self._position]
|
211
|
6
|
self._update_files()
|
212
|
6
|
self._forward.disabled = False
|
213
|
6
|
if self._position == 0:
|
214
|
6
|
self._back.disabled = True
|
215
|
|
|
216
|
6
|
def _go_forward(self, event):
|
217
|
6
|
self._position += 1
|
218
|
6
|
self._directory.value = self._stack[self._position]
|
219
|
6
|
self._update_files()
|
220
|
|
|
221
|
6
|
def _go_up(self, event=None):
|
222
|
6
|
path = self._cwd.split(os.path.sep)
|
223
|
6
|
self._directory.value = os.path.sep.join(path[:-1])
|
224
|
6
|
self._update_files(True)
|