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