Completed
Push — dev-4.1 ( 3cf8c0...4d900b )
by Felipe A.
01:21
created

cookie_browse_sorting()   A

Complexity

Conditions 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 13
rs 9.4285
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
        '',
43
        ),
44
    )
45
46
if "BROWSEPY_SETTINGS" in os.environ:
47
    app.config.from_envvar("BROWSEPY_SETTINGS")
48
49
plugin_manager = PluginManager(app)
50
51
52
def cookie_browse_sorting():
53
    '''
54
    Get sorting-cookie data of current request.
55
56
    :returns: sorting-cookie data as dict
57
    :rtype: dict
58
    '''
59
    try:
60
        data = request.cookies.get('browse-sorting', 'e30=').encode('ascii')
61
        return json.loads(base64.b64decode(data).decode('utf-8'))
62
    except (ValueError, TypeError, KeyError) as e:
63
        print(e)
64
        return {}
65
66
67
def browse_sortkey_reverse(prop):
68
    '''
69
    Get sorting function for browse
70
71
    :returns: tuple with sorting gunction and reverse bool
72
    :rtype: tuple of a dict and a bool
73
    '''
74
    if prop.startswith('-'):
75
        prop = prop[1:]
76
        reverse = True
77
    elif prop.startswith('+'):
78
        prop = prop[1:]
79
        reverse = False
80
    else:
81
        reverse = False
82
83
    if prop == 'text':
84
        return (
85
            lambda x: (
86
                x.is_directory == reverse,
87
                x.default_action[1].text.lower()
88
                ),
89
            reverse
90
            )
91
    if prop == 'size':
92
        return (
93
            lambda x: (
94
                x.is_directory == reverse,
95
                x.stats.st_size
96
                ),
97
            reverse
98
            )
99
    return (
100
        lambda x: (
101
            x.is_directory == reverse,
102
            getattr(x, prop, None)
103
            ),
104
        reverse
105
        )
106
107
108
def stream_template(template_name, **context):
109
    '''
110
    Some templates can be huge, this function returns an streaming response,
111
    sending the content in chunks and preventing from timeout.
112
113
    :param template_name: template
114
    :param **context: parameters for templates.
115
    :yields: HTML strings
116
    '''
117
    app.update_template_context(context)
118
    template = app.jinja_env.get_template(template_name)
119
    stream = template.generate(context)
120
    return Response(stream_with_context(stream))
121
122
123
@app.context_processor
124
def template_globals():
125
    return {
126
        'manager': app.extensions['plugin_manager'],
127
        'len': len,
128
        }
129
130
131
@app.route('/sort/<string:property>', defaults={"path": ""})
132
@app.route('/sort/<string:property>/<path:path>')
133
def sort(property, path):
134
    try:
135
        directory = Node.from_urlpath(path)
136
    except OutsideDirectoryBase:
137
        return NotFound()
138
139
    if not directory.is_directory:
140
        return NotFound()
141
142
    data = cookie_browse_sorting()
143
    data[path] = property
144
    raw_data = json.dumps(data).encode('utf-8')
145
146
    response = redirect(url_for(".browse", path=directory.urlpath))
147
    response.set_cookie('browse-sorting', base64.b64encode(raw_data))
148
    return response
149
150
151
@app.route("/browse", defaults={"path": ""})
152
@app.route('/browse/<path:path>')
153
def browse(path):
154
    sort_property = cookie_browse_sorting().get(path, 'text')
155
    sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
156
157
    try:
158
        directory = Node.from_urlpath(path)
159
        if directory.is_directory:
160
            return stream_template(
161
                'browse.html',
162
                file=directory,
163
                sort_property=sort_property,
164
                sort_fnc=sort_fnc,
165
                sort_reverse=sort_reverse
166
                )
167
    except OutsideDirectoryBase:
168
        pass
169
    return NotFound()
170
171
172
@app.route('/open/<path:path>', endpoint="open")
173
def open_file(path):
174
    try:
175
        file = Node.from_urlpath(path)
176
        if file.is_file:
177
            return send_from_directory(file.parent.path, file.name)
178
    except OutsideDirectoryBase:
179
        pass
180
    return NotFound()
181
182
183
@app.route("/download/file/<path:path>")
184
def download_file(path):
185
    try:
186
        file = Node.from_urlpath(path)
187
        if file.is_file:
188
            return file.download()
189
    except OutsideDirectoryBase:
190
        pass
191
    return NotFound()
192
193
194
@app.route("/download/directory/<path:path>.tgz")
195
def download_directory(path):
196
    try:
197
        directory = Node.from_urlpath(path)
198
        if directory.is_directory:
199
            return directory.download()
200
    except OutsideDirectoryBase:
201
        pass
202
    return NotFound()
203
204
205
@app.route("/remove/<path:path>", methods=("GET", "POST"))
206
def remove(path):
207
    try:
208
        file = Node.from_urlpath(path)
209
    except OutsideDirectoryBase:
210
        return NotFound()
211
    if request.method == 'GET':
212
        if not file.can_remove:
213
            return NotFound()
214
        return render_template('remove.html', file=file)
215
    parent = file.parent
216
    if parent is None:
217
        # base is not removable
218
        return NotFound()
219
220
    try:
221
        file.remove()
222
    except OutsideRemovableBase:
223
        return NotFound()
224
225
    return redirect(url_for(".browse", path=parent.urlpath))
226
227
228
@app.route("/upload", defaults={'path': ''}, methods=("POST",))
229
@app.route("/upload/<path:path>", methods=("POST",))
230
def upload(path):
231
    try:
232
        directory = Node.from_urlpath(path)
233
    except OutsideDirectoryBase:
234
        return NotFound()
235
236
    if not directory.is_directory or not directory.can_upload:
237
        return NotFound()
238
239
    for f in request.files.values():
240
        filename = secure_filename(f.filename)
241
        if filename:
242
            filename = directory.choose_filename(filename)
243
            filepath = os.path.join(directory.path, filename)
244
            f.save(filepath)
245
    return redirect(url_for(".browse", path=directory.urlpath))
246
247
248
@app.route("/")
249
def index():
250
    path = app.config["directory_start"] or app.config["directory_base"]
251
    try:
252
        urlpath = Node(path).urlpath
253
    except OutsideDirectoryBase:
254
        return NotFound()
255
    return browse(urlpath)
256
257
258
@app.after_request
259
def page_not_found(response):
260
    if response.status_code == 404:
261
        return make_response((render_template('404.html'), 404))
262
    return response
263
264
265
@app.errorhandler(404)
266
def page_not_found_error(e):
267
    return render_template('404.html'), 404
268
269
270
@app.errorhandler(500)
271
def internal_server_error(e):  # pragma: no cover
272
    logger.exception(e)
273
    return getattr(e, 'message', 'Internal server error'), 500
274