Completed
Push — file-actions ( 67012e...c6a1d5 )
by Felipe A.
55s
created

create_directory()   D

Complexity

Conditions 8

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 36
rs 4
c 1
b 0
f 0
cc 8
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 Exception()
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
            raise Exception()
144
        if node.parent.path != target.path:
145
            dest = join_fnc(
146
                target.path,
147
                target.choose_filename(node.name)
148
                )
149
            shutil.move(node.path, dest)
150
151
    try:
152
        directory = Node.from_urlpath(path)
153
    except OutsideDirectoryBase:
154
        return NotFound()
155
156
    if (
157
      not directory.is_directory or
158
      not directory.can_upload or
159
      directory.is_excluded
160
      ):
161
        return NotFound()
162
163
    clipboard = Clipboard.from_request()
164
    mode = clipboard.mode
165
166
    if mode == 'cut':
167
        paste_fnc = cut
168
    elif mode == 'copy':
169
        paste_fnc = copy
170
    else:
171
        raise InvalidClipboardModeError(
172
            path=directory.path,
173
            clipboard=clipboard,
174
            mode=mode
175
            )
176
177
    issues = []
178
    clipboard.mode = 'paste'  # disables exclusion
179
    for node in map(Node.from_urlpath, clipboard):
180
        try:
181
            paste_fnc(directory, node)
182
        except BaseException as e:
183
            issues.append((node, e))
184
185
    clipboard.mode = mode
186
    if issues:
187
        raise InvalidClipboardItemsError(
188
            path=directory.path,
189
            clipboard=clipboard,
190
            issues=issues
191
            )
192
193
    if mode == 'cut':
194
        clipboard.clear()
195
196
    response = redirect(url_for('browse', path=directory.urlpath))
197
    clipboard.to_response(response)
198
    return response
199
200
201
@actions.route('/clipboard/clear', defaults={'path': ''})
202
@actions.route('/clipboard/clear/<path:path>')
203
def clipboard_clear(path):
204
    response = redirect(url_for('browse', path=path))
205
    clipboard = Clipboard.from_request()
206
    clipboard.clear()
207
    clipboard.to_response(response)
208
    return response
209
210
211
@actions.errorhandler(FileActionsException)
212
def clipboard_error(e):
213
    file = Node(e.path) if hasattr(e, 'path') else None
214
    clipboard = getattr(e, 'clipboard', None)
215
    issues = getattr(e, 'issues', ())
216
217
    response = make_response((
218
        render_template(
219
            '400.file_actions.html',
220
            error=e, file=file, clipboard=clipboard, issues=issues,
221
            ),
222
        400
223
        ))
224
    if clipboard:
225
        for issue in issues:
226
            if isinstance(issue.error, FileNotFoundError):
227
                clipboard.remove(issue.item.urlpath)
228
        clipboard.to_response(response)
229
    return response
230
231
232
def register_plugin(manager):
233
    '''
234
    Register blueprints and actions using given plugin manager.
235
236
    :param manager: plugin manager
237
    :type manager: browsepy.manager.PluginManager
238
    '''
239
    def detect_upload(directory):
240
        return directory.is_directory and directory.can_upload
241
242
    def detect_clipboard(directory):
243
        return directory.is_directory and Clipboard.from_request()
244
245
    def excluded_clipboard(path):
246
        clipboard = Clipboard.from_request(request)
247
        if clipboard.mode == 'cut':
248
            base = manager.app.config['directory_base']
249
            return abspath_to_urlpath(path, base) in clipboard
250
251
    manager.register_exclude_function(excluded_clipboard)
252
    manager.register_blueprint(actions)
253
    manager.register_widget(
254
        place='styles',
255
        type='stylesheet',
256
        endpoint='file_actions.static',
257
        filename='browse.css',
258
        filter=detect_clipboard,
259
        )
260
    manager.register_widget(
261
        place='header',
262
        type='button',
263
        endpoint='file_actions.create_directory',
264
        text='Create directory',
265
        filter=detect_upload,
266
        )
267
    manager.register_widget(
268
        place='header',
269
        type='button',
270
        endpoint='file_actions.selection',
271
        filter=lambda directory: directory.is_directory,
272
        text='Selection...',
273
        )
274
    manager.register_widget(
275
        place='header',
276
        type='html',
277
        html=lambda file: render_template(
278
            'widget.clipboard.file_actions.html',
279
            file=file,
280
            clipboard=Clipboard.from_request()
281
            ),
282
        filter=detect_clipboard,
283
        )
284