Completed
Push — file-actions ( 6acf28...68b947 )
by Felipe A.
25s
created

clipboard_error()   B

Complexity

Conditions 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
rs 8.5454
cc 5
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import os
5
import os.path
6
import shutil
7
import logging
8
9
from flask import Blueprint, render_template, request, redirect, url_for, \
10
                  make_response
11
from werkzeug.exceptions import NotFound
12
13
from browsepy import get_cookie_browse_sorting, browse_sortkey_reverse
14
from browsepy.file import Node, abspath_to_urlpath, secure_filename, \
15
                          current_restricted_chars, common_path_separators
16
from browsepy.compat import map, re_escape, FileNotFoundError
17
from browsepy.exceptions import OutsideDirectoryBase
18
19
from .clipboard import Clipboard
20
from .exceptions import FileActionsException, \
21
                        InvalidClipboardModeError, \
22
                        InvalidClipboardItemsError, \
23
                        InvalidDirnameError
24
25
26
__basedir__ = os.path.dirname(os.path.abspath(__file__))
27
28
logger = logging.getLogger(__name__)
29
30
actions = Blueprint(
31
    'file_actions',
32
    __name__,
33
    url_prefix='/file-actions',
34
    template_folder=os.path.join(__basedir__, 'templates'),
35
    static_folder=os.path.join(__basedir__, 'static'),
36
    )
37
38
re_basename = '^[^ {0}]([^{0}]*[^ {0}])?$'.format(
39
    re_escape(current_restricted_chars + common_path_separators)
40
    )
41
42
43
@actions.route('/create/directory', methods=('GET', 'POST'),
44
               defaults={'path': ''})
45
@actions.route('/create/directory/<path:path>', methods=('GET', 'POST'))
46
def create_directory(path):
47
    try:
48
        directory = Node.from_urlpath(path)
49
    except OutsideDirectoryBase:
50
        return NotFound()
51
52
    if not directory.is_directory or not directory.can_upload:
53
        return NotFound()
54
55
    if request.method == 'GET':
56
        return render_template(
57
            'create_directory.file_actions.html',
58
            file=directory,
59
            re_basename=re_basename,
60
            )
61
62
    basename = request.form['name']
63
    if secure_filename(basename) != basename or not basename:
64
        raise InvalidDirnameError(
65
            path=directory.path,
66
            name=basename,
67
            )
68
69
    os.mkdir(os.path.join(directory.path, basename))
70
71
    return redirect(url_for('browse', path=directory.urlpath))
72
73
74
@actions.route('/selection', methods=('GET', 'POST'), defaults={'path': ''})
75
@actions.route('/selection/<path:path>', methods=('GET', 'POST'))
76
def selection(path):
77
    sort_property = get_cookie_browse_sorting(path, 'text')
78
    sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
79
80
    try:
81
        directory = Node.from_urlpath(path)
82
    except OutsideDirectoryBase:
83
        return NotFound()
84
85
    if directory.is_excluded or not directory.is_directory:
86
        return NotFound()
87
88
    if request.method == 'POST':
89
        action_fmt = 'action-{}'.format
90
        mode = None
91
        for action in ('cut', 'copy'):
92
            if request.form.get(action_fmt(action)):
93
                mode = action
94
                break
95
96
        if mode in ('cut', 'copy'):
97
            response = redirect(url_for('browse', path=directory.urlpath))
98
            clipboard = Clipboard(request.form.getlist('path'), mode)
99
            clipboard.to_response(response)
100
            return response
101
102
        return redirect(request.path)
103
104
    clipboard = Clipboard.from_request()
105
    clipboard.mode = 'select'  # disables exclusion
106
    return render_template(
107
        'selection.file_actions.html',
108
        file=directory,
109
        clipboard=clipboard,
110
        cut_support=any(node.can_remove for node in directory.listdir()),
111
        sort_property=sort_property,
112
        sort_fnc=sort_fnc,
113
        sort_reverse=sort_reverse,
114
        )
115
116
117
@actions.route('/clipboard/paste', defaults={'path': ''})
118
@actions.route('/clipboard/paste/<path:path>')
119
def clipboard_paste(path):
120
121
    def copy(target, node, join_fnc=os.path.join):
122
        if node.is_excluded:
123
            raise Exception()
124
        dest = join_fnc(
125
            target.path,
126
            target.choose_filename(node.name)
127
            )
128
        if node.is_directory:
129
            shutil.copytree(node.path, dest)
130
        else:
131
            shutil.copy2(node.path, dest)
132
133
    def cut(target, node, join_fnc=os.path.join):
134
        if node.is_excluded or not node.can_remove:
135
            raise Exception()
136
        if node.parent.path != target.path:
137
            dest = join_fnc(
138
                target.path,
139
                target.choose_filename(node.name)
140
                )
141
            shutil.move(node.path, dest)
142
143
    try:
144
        directory = Node.from_urlpath(path)
145
    except OutsideDirectoryBase:
146
        return NotFound()
147
148
    if (
149
      not directory.is_directory or
150
      not directory.can_upload or
151
      directory.is_excluded
152
      ):
153
        return NotFound()
154
155
    clipboard = Clipboard.from_request()
156
    mode = clipboard.mode
157
158
    if mode == 'cut':
159
        paste_fnc = cut
160
    elif mode == 'copy':
161
        paste_fnc = copy
162
    else:
163
        raise InvalidClipboardModeError(
164
            path=directory.path,
165
            clipboard=clipboard,
166
            mode=mode
167
            )
168
169
    issues = []
170
    clipboard.mode = 'paste'  # disables exclusion
171
    for node in map(Node.from_urlpath, clipboard):
172
        try:
173
            paste_fnc(directory, node)
174
        except BaseException as e:
175
            issues.append((node, e))
176
177
    clipboard.mode = mode
178
    if issues:
179
        raise InvalidClipboardItemsError(
180
            path=directory.path,
181
            clipboard=clipboard,
182
            issues=issues
183
            )
184
    else:
185
        response = redirect(url_for('browse', path=directory.urlpath))
186
    clipboard.to_response(response)
187
    return response
188
189
190
@actions.route('/clipboard/clear', defaults={'path': ''})
191
@actions.route('/clipboard/clear/<path:path>')
192
def clipboard_clear(path):
193
    response = redirect(url_for('browse', path=path))
194
    clipboard = Clipboard.from_request()
195
    clipboard.clear()
196
    clipboard.to_response(response)
197
    return response
198
199
200
@actions.errorhandler(FileActionsException)
201
def clipboard_error(e):
202
    file = Node(e.path) if hasattr(e, 'path') else None
203
    clipboard = getattr(e, 'clipboard', None)
204
    issues = getattr(e, 'issues', ())
205
206
    response = make_response((
207
        render_template(
208
            '400.file_actions.html',
209
            error=e, file=file, clipboard=clipboard, issues=issues,
210
            ),
211
        400
212
        ))
213
    if clipboard:
214
        for issue in issues:
215
            if isinstance(issue.error, FileNotFoundError):
216
                clipboard.remove(issue.item.urlpath)
217
        clipboard.to_response(response)
218
    return response
219
220
221
def register_plugin(manager):
222
    '''
223
    Register blueprints and actions using given plugin manager.
224
225
    :param manager: plugin manager
226
    :type manager: browsepy.manager.PluginManager
227
    '''
228
    def detect_upload(directory):
229
        return directory.is_directory and directory.can_upload
230
231
    def detect_clipboard(directory):
232
        return directory.is_directory and Clipboard.from_request()
233
234
    def excluded_clipboard(path):
235
        clipboard = Clipboard.from_request(request)
236
        if clipboard.mode == 'cut':
237
            base = manager.app.config['directory_base']
238
            return abspath_to_urlpath(path, base) in clipboard
239
240
    manager.register_exclude_function(excluded_clipboard)
241
    manager.register_blueprint(actions)
242
    manager.register_widget(
243
        place='styles',
244
        type='stylesheet',
245
        endpoint='file_actions.static',
246
        filename='browse.css',
247
        filter=detect_clipboard,
248
        )
249
    manager.register_widget(
250
        place='header',
251
        type='button',
252
        endpoint='file_actions.create_directory',
253
        text='Create directory',
254
        filter=detect_upload,
255
        )
256
    manager.register_widget(
257
        place='header',
258
        type='button',
259
        endpoint='file_actions.selection',
260
        filter=lambda directory: directory.is_directory,
261
        text='Selection...',
262
        )
263
    manager.register_widget(
264
        place='header',
265
        type='html',
266
        html=lambda file: render_template(
267
            'widget.clipboard.file_actions.html',
268
            file=file,
269
            clipboard=Clipboard.from_request()
270
            ),
271
        filter=detect_clipboard,
272
        )
273