Completed
Push — dev-4.1-unstable ( 426fcf...0bd0f2 )
by Felipe A.
01:04
created

browse_sortkey_reverse()   D

Complexity

Conditions 8

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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