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)

Read our documentation on viewing source code .

Loading