Completed
Push — 0.5.3 ( 2bae48...20e122 )
by Felipe A.
01:13
created

sort()   C

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