Completed
Push — file-actions ( c6a1d5...3503b9 )
by Felipe A.
38s
created

cut()   B

Complexity

Conditions 5

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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