sort()   C
last analyzed

Complexity

Conditions 7

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
rs 5.5
cc 7
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import logging
5
import os
6
import os.path
7
import json
8
import base64
9
10
from flask import Response, request, render_template, redirect, \
11
                  url_for, send_from_directory, stream_with_context, \
12
                  make_response
13
from werkzeug.exceptions import NotFound
14
15
from .appconfig import Flask
16
from .manager import PluginManager
17
from .file import Node, secure_filename
18
from .exceptions import OutsideRemovableBase, OutsideDirectoryBase, \
19
                        InvalidFilenameError, InvalidPathError
20
from . import compat
21
from . import __meta__ as meta
22
23
__app__ = meta.app  # noqa
24
__version__ = meta.version  # noqa
25
__license__ = meta.license  # noqa
26
__author__ = meta.author  # noqa
27
__basedir__ = os.path.abspath(os.path.dirname(compat.fsdecode(__file__)))
28
29
logger = logging.getLogger(__name__)
30
31
app = Flask(
32
    __name__,
33
    static_url_path='/static',
34
    static_folder=os.path.join(__basedir__, "static"),
35
    template_folder=os.path.join(__basedir__, "templates")
36
    )
37
app.config.update(
38
    directory_base=compat.getcwd(),
39
    directory_start=None,
40
    directory_remove=None,
41
    directory_upload=None,
42
    directory_tar_buffsize=262144,
43
    directory_downloadable=True,
44
    use_binary_multiples=True,
45
    plugin_modules=[],
46
    plugin_namespaces=(
47
        'browsepy.plugin',
48
        'browsepy_',
49
        '',
50
        ),
51
    exclude_fnc=None,
52
    )
53
app.jinja_env.add_extension('browsepy.transform.htmlcompress.HTMLCompress')
54
55
if 'BROWSEPY_SETTINGS' in os.environ:
56
    app.config.from_envvar('BROWSEPY_SETTINGS')
57
58
plugin_manager = PluginManager(app)
59
60
61
def iter_cookie_browse_sorting(cookies):
62
    '''
63
    Get sorting-cookie from cookies dictionary.
64
65
    :yields: tuple of path and sorting property
66
    :ytype: 2-tuple of strings
67
    '''
68
    try:
69
        data = cookies.get('browse-sorting', 'e30=').encode('ascii')
70
        for path, prop in json.loads(base64.b64decode(data).decode('utf-8')):
71
            yield path, prop
72
    except (ValueError, TypeError, KeyError) as e:
73
        logger.exception(e)
74
75
76
def get_cookie_browse_sorting(path, default):
77
    '''
78
    Get sorting-cookie data for path of current request.
79
80
    :returns: sorting property
81
    :rtype: string
82
    '''
83
    if request:
84
        for cpath, cprop in iter_cookie_browse_sorting(request.cookies):
85
            if path == cpath:
86
                return cprop
87
    return default
88
89
90
def browse_sortkey_reverse(prop):
91
    '''
92
    Get sorting function for directory listing based on given attribute
93
    name, with some caveats:
94
    * Directories will be first.
95
    * If *name* is given, link widget lowercase text will be used istead.
96
    * If *size* is given, bytesize will be used.
97
98
    :param prop: file attribute name
99
    :returns: tuple with sorting gunction and reverse bool
100
    :rtype: tuple of a dict and a bool
101
    '''
102
    if prop.startswith('-'):
103
        prop = prop[1:]
104
        reverse = True
105
    else:
106
        reverse = False
107
108
    if prop == 'text':
109
        return (
110
            lambda x: (
111
                x.is_directory == reverse,
112
                x.link.text.lower() if x.link and x.link.text else x.name
113
                ),
114
            reverse
115
            )
116
    if prop == 'size':
117
        return (
118
            lambda x: (
119
                x.is_directory == reverse,
120
                x.stats.st_size
121
                ),
122
            reverse
123
            )
124
    return (
125
        lambda x: (
126
            x.is_directory == reverse,
127
            getattr(x, prop, None)
128
            ),
129
        reverse
130
        )
131
132
133
def stream_template(template_name, **context):
134
    '''
135
    Some templates can be huge, this function returns an streaming response,
136
    sending the content in chunks and preventing from timeout.
137
138
    :param template_name: template
139
    :param **context: parameters for templates.
140
    :yields: HTML strings
141
    '''
142
    app.update_template_context(context)
143
    template = app.jinja_env.get_template(template_name)
144
    stream = template.generate(context)
145
    return Response(stream_with_context(stream))
146
147
148
@app.context_processor
149
def template_globals():
150
    return {
151
        'manager': app.extensions['plugin_manager'],
152
        'len': len,
153
        }
154
155
156
@app.route('/sort/<string:property>', defaults={"path": ""})
157
@app.route('/sort/<string:property>/<path:path>')
158
def sort(property, path):
159
    try:
160
        directory = Node.from_urlpath(path)
161
    except OutsideDirectoryBase:
162
        return NotFound()
163
164
    if not directory.is_directory or directory.is_excluded:
165
        return NotFound()
166
167
    data = [
168
        (cpath, cprop)
169
        for cpath, cprop in iter_cookie_browse_sorting(request.cookies)
170
        if cpath != path
171
        ]
172
    data.append((path, property))
173
    raw_data = base64.b64encode(json.dumps(data).encode('utf-8'))
174
175
    # prevent cookie becoming too large
176
    while len(raw_data) > 3975:  # 4000 - len('browse-sorting=""; Path=/')
177
        data.pop(0)
178
        raw_data = base64.b64encode(json.dumps(data).encode('utf-8'))
179
180
    response = redirect(url_for(".browse", path=directory.urlpath))
181
    response.set_cookie('browse-sorting', raw_data)
182
    return response
183
184
185 View Code Duplication
@app.route("/browse", defaults={"path": ""})
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
186
@app.route('/browse/<path:path>')
187
def browse(path):
188
    sort_property = get_cookie_browse_sorting(path, 'text')
189
    sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
190
191
    try:
192
        directory = Node.from_urlpath(path)
193
        if directory.is_directory and not directory.is_excluded:
194
            return stream_template(
195
                'browse.html',
196
                file=directory,
197
                sort_property=sort_property,
198
                sort_fnc=sort_fnc,
199
                sort_reverse=sort_reverse
200
                )
201
    except OutsideDirectoryBase:
202
        pass
203
    return NotFound()
204
205
206
@app.route('/open/<path:path>', endpoint="open")
207
def open_file(path):
208
    try:
209
        file = Node.from_urlpath(path)
210
        if file.is_file and not file.is_excluded:
211
            return send_from_directory(file.parent.path, file.name)
212
    except OutsideDirectoryBase:
213
        pass
214
    return NotFound()
215
216
217
@app.route("/download/file/<path:path>")
218
def download_file(path):
219
    try:
220
        file = Node.from_urlpath(path)
221
        if file.is_file and not file.is_excluded:
222
            return file.download()
223
    except OutsideDirectoryBase:
224
        pass
225
    return NotFound()
226
227
228
@app.route("/download/directory/<path:path>.tgz")
229
def download_directory(path):
230
    try:
231
        directory = Node.from_urlpath(path)
232
        if directory.is_directory and not directory.is_excluded:
233
            return directory.download()
234
    except OutsideDirectoryBase:
235
        pass
236
    return NotFound()
237
238
239
@app.route("/remove/<path:path>", methods=("GET", "POST"))
240
def remove(path):
241
    try:
242
        file = Node.from_urlpath(path)
243
    except OutsideDirectoryBase:
244
        return NotFound()
245
246
    if not file.can_remove or file.is_excluded or not file.parent:
247
        return NotFound()
248
249
    if request.method == 'GET':
250
        return render_template('remove.html', file=file)
251
252
    file.remove()
253
    return redirect(url_for(".browse", path=file.parent.urlpath))
254
255
256
@app.route("/upload", defaults={'path': ''}, methods=("POST",))
257
@app.route("/upload/<path:path>", methods=("POST",))
258
def upload(path):
259
    try:
260
        directory = Node.from_urlpath(path)
261
    except OutsideDirectoryBase:
262
        return NotFound()
263
264
    if (
265
      not directory.is_directory or
266
      not directory.can_upload or
267
      directory.is_excluded
268
      ):
269
        return NotFound()
270
271
    for v in request.files.listvalues():
272
        for f in v:
273
            filename = secure_filename(f.filename)
274
            if filename:
275
                filename = directory.choose_filename(filename)
276
                filepath = os.path.join(directory.path, filename)
277
                f.save(filepath)
278
            else:
279
                raise InvalidFilenameError(
280
                    path=directory.path,
281
                    filename=f.filename
282
                    )
283
    return redirect(url_for(".browse", path=directory.urlpath))
284
285
286
@app.route("/")
287
def index():
288
    path = app.config["directory_start"] or app.config["directory_base"]
289
    try:
290
        urlpath = Node(path).urlpath
291
    except OutsideDirectoryBase:
292
        return NotFound()
293
    return browse(urlpath)
294
295
296
@app.after_request
297
def page_not_found(response):
298
    if response.status_code == 404:
299
        return make_response((render_template('404.html'), 404))
300
    return response
301
302
303
@app.errorhandler(InvalidPathError)
304
def bad_request_error(e):
305
    file = None
306
    if hasattr(e, 'path'):
307
        if isinstance(e, InvalidFilenameError):
308
            file = Node(e.path)
309
        else:
310
            file = Node(e.path).parent
311
    return render_template('400.html', file=file, error=e), 400
312
313
314
@app.errorhandler(OutsideRemovableBase)
315
@app.errorhandler(404)
316
def page_not_found_error(e):
317
    return render_template('404.html'), 404
318
319
320
@app.errorhandler(500)
321
def internal_server_error(e):  # pragma: no cover
322
    logger.exception(e)
323
    return getattr(e, 'message', 'Internal server error'), 500
324